groove-dev 0.27.77 → 0.27.78
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/CLAUDE.md +0 -7
- package/MOE_TRAINING_PIPELINE.md +216 -12
- package/moe-training/DEPLOY_CENTRAL_COMMAND.md +413 -0
- package/moe-training/client/consent.js +96 -0
- package/moe-training/client/envelope-builder.js +56 -0
- package/moe-training/client/index.js +10 -0
- package/moe-training/client/parsers/claude-code.js +110 -0
- package/moe-training/client/parsers/codex.js +80 -0
- package/moe-training/client/parsers/gemini.js +80 -0
- package/moe-training/client/parsers/grok.js +16 -0
- package/moe-training/client/parsers/index.js +20 -0
- package/moe-training/client/scrubber.js +126 -0
- package/moe-training/client/session-attestation.js +114 -0
- package/moe-training/client/step-classifier.js +51 -0
- package/moe-training/client/trajectory-capture.js +227 -0
- package/moe-training/client/transmission-queue.js +93 -0
- package/moe-training/package-lock.json +1266 -0
- package/moe-training/package.json +20 -0
- package/moe-training/server/enrichment.js +24 -0
- package/moe-training/server/index.js +119 -0
- package/moe-training/server/ledger.js +110 -0
- package/moe-training/server/routes/ingest.js +96 -0
- package/moe-training/server/routes/sessions.js +43 -0
- package/moe-training/server/routes/stats.js +31 -0
- package/moe-training/server/scoring.js +63 -0
- package/moe-training/server/session-registry.js +156 -0
- package/moe-training/server/stats.js +129 -0
- package/moe-training/server/stitcher.js +69 -0
- package/moe-training/server/storage.js +147 -0
- package/moe-training/server/verifier.js +102 -0
- package/moe-training/shared/constants.js +30 -0
- package/moe-training/shared/crypto.js +45 -0
- package/moe-training/shared/envelope-schema.js +220 -0
- package/moe-training/test/client/consent.test.js +121 -0
- package/moe-training/test/client/envelope-builder.test.js +107 -0
- package/moe-training/test/client/parsers/claude-code.test.js +119 -0
- package/moe-training/test/client/parsers/codex.test.js +83 -0
- package/moe-training/test/client/parsers/gemini.test.js +99 -0
- package/moe-training/test/client/scrubber.test.js +133 -0
- package/moe-training/test/client/session-attestation-security.test.js +95 -0
- package/moe-training/test/client/step-classifier.test.js +88 -0
- package/moe-training/test/integration/handshake.test.js +260 -0
- package/moe-training/test/server/ingest-security.test.js +166 -0
- package/moe-training/test/server/ledger.test.js +131 -0
- package/moe-training/test/server/scoring.test.js +242 -0
- package/moe-training/test/server/session-registry.test.js +125 -0
- package/moe-training/test/server/stitcher.test.js +157 -0
- package/moe-training/test/server/verifier.test.js +232 -0
- package/moe-training/test/shared/crypto.test.js +87 -0
- package/moe-training/test/shared/envelope-schema.test.js +351 -0
- 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/agent-loop.js +48 -5
- package/node_modules/@groove-dev/daemon/src/api.js +77 -0
- package/node_modules/@groove-dev/daemon/src/index.js +61 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +64 -21
- package/node_modules/@groove-dev/daemon/src/process.js +199 -0
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +15 -0
- package/node_modules/@groove-dev/daemon/src/state.js +20 -1
- package/node_modules/@groove-dev/gui/dist/assets/{index-BbmPDhuW.js → index-BJgEJ9lZ.js} +1677 -1677
- 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/stores/groove.js +32 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +167 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/agent-loop.js +48 -5
- package/packages/daemon/src/api.js +77 -0
- package/packages/daemon/src/index.js +61 -0
- package/packages/daemon/src/journalist.js +64 -21
- package/packages/daemon/src/process.js +199 -0
- package/packages/daemon/src/providers/grok.js +15 -0
- package/packages/daemon/src/state.js +20 -1
- package/packages/gui/dist/assets/{index-BbmPDhuW.js → index-BJgEJ9lZ.js} +1677 -1677
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/stores/groove.js +32 -0
- package/packages/gui/src/views/settings.jsx +167 -1
|
@@ -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-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BJgEJ9lZ.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
|
|
@@ -122,6 +122,10 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
122
122
|
networkWallet: { connected: false, address: null, balance: '0.00', token: 'GROOVE', chain: 'base-l2' },
|
|
123
123
|
networkEarnings: { today: 0, thisWeek: 0, allTime: 0, history: [] },
|
|
124
124
|
|
|
125
|
+
// ── Training Data ──────────────────────────────────────────
|
|
126
|
+
trainingOptIn: false,
|
|
127
|
+
trainingStats: null,
|
|
128
|
+
|
|
125
129
|
// ── Marketplace Auth ───────────────────────────────────────
|
|
126
130
|
marketplaceUser: null, // { id, displayName, avatar, ... } or null
|
|
127
131
|
marketplaceAuthenticated: false,
|
|
@@ -197,6 +201,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
197
201
|
get().fetchTunnels();
|
|
198
202
|
get().fetchBetaStatus();
|
|
199
203
|
get().fetchNetworkInstallStatus();
|
|
204
|
+
get().fetchTrainingStatus();
|
|
200
205
|
if (!get().onboardingComplete) get().fetchOnboardingStatus();
|
|
201
206
|
if (window.groove?.auth?.onSubscriptionStatus) {
|
|
202
207
|
window.groove.auth.onSubscriptionStatus((data) => {
|
|
@@ -853,6 +858,14 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
853
858
|
break;
|
|
854
859
|
}
|
|
855
860
|
|
|
861
|
+
case 'training:status': {
|
|
862
|
+
set({
|
|
863
|
+
trainingOptIn: msg.data?.optedIn ?? false,
|
|
864
|
+
trainingStats: msg.data,
|
|
865
|
+
});
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
|
|
856
869
|
case 'config:updated':
|
|
857
870
|
get().fetchBetaStatus();
|
|
858
871
|
get().fetchNetworkInstallStatus();
|
|
@@ -2320,6 +2333,25 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
2320
2333
|
}
|
|
2321
2334
|
},
|
|
2322
2335
|
|
|
2336
|
+
// ── Training Data ─────────────────────────────────────────
|
|
2337
|
+
|
|
2338
|
+
async setTrainingOptIn(enabled) {
|
|
2339
|
+
try {
|
|
2340
|
+
await api.post('/training/opt-in', { enabled });
|
|
2341
|
+
set({ trainingOptIn: enabled });
|
|
2342
|
+
if (!enabled) set({ trainingStats: null });
|
|
2343
|
+
} catch (e) {
|
|
2344
|
+
get().addToast('error', 'Failed to update training preference');
|
|
2345
|
+
}
|
|
2346
|
+
},
|
|
2347
|
+
|
|
2348
|
+
async fetchTrainingStatus() {
|
|
2349
|
+
try {
|
|
2350
|
+
const data = await api.get('/training/status');
|
|
2351
|
+
set({ trainingOptIn: data.optedIn, trainingStats: data });
|
|
2352
|
+
} catch { /* endpoint may not exist on older daemons */ }
|
|
2353
|
+
},
|
|
2354
|
+
|
|
2323
2355
|
// ── Network (Early Access) ────────────────────────────────
|
|
2324
2356
|
|
|
2325
2357
|
async fetchBetaStatus() {
|
|
@@ -13,10 +13,11 @@ import { Sheet, SheetContent } from '../components/ui/sheet';
|
|
|
13
13
|
import { api } from '../lib/api';
|
|
14
14
|
import { cn } from '../lib/cn';
|
|
15
15
|
import { fmtUptime } from '../lib/format';
|
|
16
|
+
import { Dialog, DialogContent } from '../components/ui/dialog';
|
|
16
17
|
import {
|
|
17
18
|
Key, Eye, EyeOff, Check, Cpu, Download, Loader2, RefreshCw, Terminal, Copy,
|
|
18
19
|
FolderOpen, FolderSearch, Users, Gauge, ChevronRight,
|
|
19
|
-
ShieldCheck, Settings, Lock,
|
|
20
|
+
ShieldCheck, Settings, Lock, Database, Shield,
|
|
20
21
|
Newspaper, Radio, Send, MessageSquare, MessageCircle,
|
|
21
22
|
Plus, Trash2, Plug, PlugZap, TestTube, X, HelpCircle, ExternalLink,
|
|
22
23
|
} from 'lucide-react';
|
|
@@ -1304,6 +1305,168 @@ function AddGatewayCard({ existingTypes, onAdd }) {
|
|
|
1304
1305
|
);
|
|
1305
1306
|
}
|
|
1306
1307
|
|
|
1308
|
+
/* ── Training Data Section ────────────────────────────────── */
|
|
1309
|
+
|
|
1310
|
+
function TrainingDataSection() {
|
|
1311
|
+
const trainingOptIn = useGrooveStore((s) => s.trainingOptIn);
|
|
1312
|
+
const trainingStats = useGrooveStore((s) => s.trainingStats);
|
|
1313
|
+
const setTrainingOptIn = useGrooveStore((s) => s.setTrainingOptIn);
|
|
1314
|
+
const fetchTrainingStatus = useGrooveStore((s) => s.fetchTrainingStatus);
|
|
1315
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
1316
|
+
const [consentOpen, setConsentOpen] = useState(false);
|
|
1317
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
1318
|
+
const [deleting, setDeleting] = useState(false);
|
|
1319
|
+
|
|
1320
|
+
useEffect(() => { fetchTrainingStatus(); }, []);
|
|
1321
|
+
|
|
1322
|
+
function handleToggle(next) {
|
|
1323
|
+
if (next) {
|
|
1324
|
+
setConsentOpen(true);
|
|
1325
|
+
} else {
|
|
1326
|
+
setTrainingOptIn(false);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
async function handleAgree() {
|
|
1331
|
+
await setTrainingOptIn(true);
|
|
1332
|
+
setConsentOpen(false);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
async function handleDelete() {
|
|
1336
|
+
setDeleting(true);
|
|
1337
|
+
try {
|
|
1338
|
+
await api.post('/training/opt-in/delete');
|
|
1339
|
+
useGrooveStore.setState({ trainingOptIn: false, trainingStats: null });
|
|
1340
|
+
addToast('info', 'Training data deleted');
|
|
1341
|
+
setDeleteOpen(false);
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
addToast('error', 'Failed to delete data', err.message);
|
|
1344
|
+
} finally {
|
|
1345
|
+
setDeleting(false);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const sessions = trainingStats?.sessionsCaptured ?? 0;
|
|
1350
|
+
const envelopes = trainingStats?.envelopesSent ?? 0;
|
|
1351
|
+
const active = trainingStats?.captureActive ?? false;
|
|
1352
|
+
|
|
1353
|
+
return (
|
|
1354
|
+
<div>
|
|
1355
|
+
<div className="flex items-center gap-2 mb-2.5 px-0.5">
|
|
1356
|
+
<span className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider">Training Data</span>
|
|
1357
|
+
<div className="flex-1 h-px bg-border-subtle" />
|
|
1358
|
+
</div>
|
|
1359
|
+
|
|
1360
|
+
{/* Opt-in card */}
|
|
1361
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 px-4 py-3.5 max-w-md space-y-3">
|
|
1362
|
+
<div className="flex items-center gap-2.5">
|
|
1363
|
+
<div className="w-6 h-6 rounded bg-accent/8 flex items-center justify-center flex-shrink-0">
|
|
1364
|
+
<Database size={12} className="text-accent" />
|
|
1365
|
+
</div>
|
|
1366
|
+
<div className="flex-1">
|
|
1367
|
+
<div className="text-[13px] font-medium text-text-0 font-sans leading-tight">Share Usage Data</div>
|
|
1368
|
+
<div className="text-2xs text-text-4 font-sans leading-relaxed mt-0.5">
|
|
1369
|
+
Contribute anonymized session data to train Groove's MoE models. All data is PII-scrubbed before leaving your machine.
|
|
1370
|
+
</div>
|
|
1371
|
+
</div>
|
|
1372
|
+
<Toggle value={trainingOptIn} onChange={handleToggle} />
|
|
1373
|
+
</div>
|
|
1374
|
+
|
|
1375
|
+
{/* Stats panel — only when opted in */}
|
|
1376
|
+
{trainingOptIn && (
|
|
1377
|
+
<div className="border-t border-border-subtle pt-3 space-y-2.5">
|
|
1378
|
+
{sessions === 0 && envelopes === 0 ? (
|
|
1379
|
+
<div className="text-2xs text-text-3 font-sans">
|
|
1380
|
+
No data captured yet — stats will appear after your first agent session.
|
|
1381
|
+
</div>
|
|
1382
|
+
) : (
|
|
1383
|
+
<div className="flex items-center gap-4">
|
|
1384
|
+
<div>
|
|
1385
|
+
<div className="text-2xs text-text-2 font-sans">Sessions</div>
|
|
1386
|
+
<div className="text-sm font-semibold text-text-0 font-mono">{sessions}</div>
|
|
1387
|
+
</div>
|
|
1388
|
+
<div>
|
|
1389
|
+
<div className="text-2xs text-text-2 font-sans">Envelopes</div>
|
|
1390
|
+
<div className="text-sm font-semibold text-text-0 font-mono">{envelopes}</div>
|
|
1391
|
+
</div>
|
|
1392
|
+
<div className="flex items-center gap-1.5">
|
|
1393
|
+
<StatusDot status={active ? 'running' : 'crashed'} size="sm" />
|
|
1394
|
+
<span className="text-2xs text-text-2 font-sans">{active ? 'Capturing' : 'Idle'}</span>
|
|
1395
|
+
</div>
|
|
1396
|
+
</div>
|
|
1397
|
+
)}
|
|
1398
|
+
|
|
1399
|
+
{/* Delete data */}
|
|
1400
|
+
<div>
|
|
1401
|
+
<Button variant="ghost" size="sm" onClick={() => setDeleteOpen(true)} className="h-7 text-2xs text-danger hover:text-danger gap-1.5">
|
|
1402
|
+
<Trash2 size={10} />
|
|
1403
|
+
Delete My Data
|
|
1404
|
+
</Button>
|
|
1405
|
+
</div>
|
|
1406
|
+
</div>
|
|
1407
|
+
)}
|
|
1408
|
+
</div>
|
|
1409
|
+
|
|
1410
|
+
{/* Consent dialog */}
|
|
1411
|
+
<Dialog open={consentOpen} onOpenChange={setConsentOpen}>
|
|
1412
|
+
<DialogContent title="Data Collection Consent" description="Review what data is collected and how it is used.">
|
|
1413
|
+
<div className="px-5 py-4 space-y-3">
|
|
1414
|
+
<div className="space-y-2.5 text-xs text-text-1 font-sans leading-relaxed">
|
|
1415
|
+
<div className="flex items-start gap-2.5">
|
|
1416
|
+
<Database size={13} className="text-accent flex-shrink-0 mt-0.5" />
|
|
1417
|
+
<div><span className="font-semibold text-text-0">What is collected:</span> Agent session trajectories — thoughts, tool calls, observations, and outcomes.</div>
|
|
1418
|
+
</div>
|
|
1419
|
+
<div className="flex items-start gap-2.5">
|
|
1420
|
+
<Cpu size={13} className="text-accent flex-shrink-0 mt-0.5" />
|
|
1421
|
+
<div><span className="font-semibold text-text-0">How it is used:</span> Training MoE expert models that become Groove agents.</div>
|
|
1422
|
+
</div>
|
|
1423
|
+
<div className="flex items-start gap-2.5">
|
|
1424
|
+
<Shield size={13} className="text-accent flex-shrink-0 mt-0.5" />
|
|
1425
|
+
<div><span className="font-semibold text-text-0">PII protection:</span> 13 categories of sensitive data (emails, API keys, file paths, etc.) are automatically scrubbed before transmission.</div>
|
|
1426
|
+
</div>
|
|
1427
|
+
<div className="flex items-start gap-2.5">
|
|
1428
|
+
<Settings size={13} className="text-accent flex-shrink-0 mt-0.5" />
|
|
1429
|
+
<div><span className="font-semibold text-text-0">Opt out anytime:</span> Toggle the setting OFF to stop collection immediately.</div>
|
|
1430
|
+
</div>
|
|
1431
|
+
<div className="flex items-start gap-2.5">
|
|
1432
|
+
<Trash2 size={13} className="text-danger flex-shrink-0 mt-0.5" />
|
|
1433
|
+
<div><span className="font-semibold text-text-0">Delete data:</span> Permanently delete all previously collected data at any time.</div>
|
|
1434
|
+
</div>
|
|
1435
|
+
</div>
|
|
1436
|
+
</div>
|
|
1437
|
+
<div className="flex items-center gap-2 px-5 py-4 border-t border-border-subtle">
|
|
1438
|
+
<Button variant="primary" size="sm" onClick={handleAgree} className="flex-1 h-8 text-xs">
|
|
1439
|
+
I Agree — Start Contributing
|
|
1440
|
+
</Button>
|
|
1441
|
+
<Button variant="ghost" size="sm" onClick={() => setConsentOpen(false)} className="h-8 text-xs px-3">
|
|
1442
|
+
Cancel
|
|
1443
|
+
</Button>
|
|
1444
|
+
</div>
|
|
1445
|
+
</DialogContent>
|
|
1446
|
+
</Dialog>
|
|
1447
|
+
|
|
1448
|
+
{/* Delete confirmation dialog */}
|
|
1449
|
+
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
|
1450
|
+
<DialogContent title="Delete Training Data" description="Confirm permanent deletion of all collected training data.">
|
|
1451
|
+
<div className="px-5 py-4">
|
|
1452
|
+
<p className="text-xs text-text-1 font-sans leading-relaxed">
|
|
1453
|
+
This will permanently delete all your collected training data and opt you out. This cannot be undone.
|
|
1454
|
+
</p>
|
|
1455
|
+
</div>
|
|
1456
|
+
<div className="flex items-center gap-2 px-5 py-4 border-t border-border-subtle">
|
|
1457
|
+
<Button variant="danger" size="sm" onClick={handleDelete} disabled={deleting} className="flex-1 h-8 text-xs">
|
|
1458
|
+
{deleting ? 'Deleting...' : 'Delete All Data'}
|
|
1459
|
+
</Button>
|
|
1460
|
+
<Button variant="ghost" size="sm" onClick={() => setDeleteOpen(false)} className="h-8 text-xs px-3">
|
|
1461
|
+
Cancel
|
|
1462
|
+
</Button>
|
|
1463
|
+
</div>
|
|
1464
|
+
</DialogContent>
|
|
1465
|
+
</Dialog>
|
|
1466
|
+
</div>
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1307
1470
|
/* ── Early Access Section ─────────────────────────────────── */
|
|
1308
1471
|
|
|
1309
1472
|
function EarlyAccessSection() {
|
|
@@ -1619,6 +1782,9 @@ export default function SettingsView() {
|
|
|
1619
1782
|
{/* ═══════ EARLY ACCESS ═══════ */}
|
|
1620
1783
|
<EarlyAccessSection />
|
|
1621
1784
|
|
|
1785
|
+
{/* ═══════ TRAINING DATA ═══════ */}
|
|
1786
|
+
<TrainingDataSection />
|
|
1787
|
+
|
|
1622
1788
|
</div>
|
|
1623
1789
|
</ScrollArea>
|
|
1624
1790
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.78",
|
|
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)",
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// existing GROOVE orchestration (rotation, journalist, token tracking, routing).
|
|
7
7
|
|
|
8
8
|
import { EventEmitter } from 'events';
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
10
|
+
import { resolve, dirname } from 'path';
|
|
9
11
|
import { TOOL_DEFINITIONS, ToolExecutor } from './tool-executor.js';
|
|
10
12
|
|
|
11
13
|
export class AgentLoop extends EventEmitter {
|
|
@@ -36,11 +38,19 @@ export class AgentLoop extends EventEmitter {
|
|
|
36
38
|
agent.id,
|
|
37
39
|
);
|
|
38
40
|
|
|
39
|
-
//
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// Session persistence
|
|
42
|
+
this.sessionPath = resolve(daemon.grooveDir, 'sessions', `${agent.id}.json`);
|
|
43
|
+
|
|
44
|
+
// Load existing session or initialize with system prompt
|
|
45
|
+
const savedMessages = AgentLoop.loadSession(this.sessionPath);
|
|
46
|
+
if (savedMessages && savedMessages.length > 0) {
|
|
47
|
+
this.messages = savedMessages;
|
|
48
|
+
} else {
|
|
49
|
+
this.messages.push({
|
|
50
|
+
role: 'system',
|
|
51
|
+
content: this._buildSystemPrompt(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
// --- Lifecycle ---
|
|
@@ -68,6 +78,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
68
78
|
this.emit('error', { message: err.message });
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
this._saveSession();
|
|
71
82
|
this.idle = true;
|
|
72
83
|
}
|
|
73
84
|
|
|
@@ -444,4 +455,36 @@ export class AgentLoop extends EventEmitter {
|
|
|
444
455
|
uptime: Date.now() - this.startedAt,
|
|
445
456
|
};
|
|
446
457
|
}
|
|
458
|
+
|
|
459
|
+
// --- Session Persistence ---
|
|
460
|
+
|
|
461
|
+
_saveSession() {
|
|
462
|
+
try {
|
|
463
|
+
mkdirSync(dirname(this.sessionPath), { recursive: true });
|
|
464
|
+
let toSave = this.messages;
|
|
465
|
+
if (toSave.length > 201) {
|
|
466
|
+
const hasSystem = toSave[0]?.role === 'system';
|
|
467
|
+
toSave = hasSystem
|
|
468
|
+
? [toSave[0], ...toSave.slice(-200)]
|
|
469
|
+
: toSave.slice(-200);
|
|
470
|
+
}
|
|
471
|
+
writeFileSync(this.sessionPath, JSON.stringify(toSave), { mode: 0o600 });
|
|
472
|
+
} catch { /* non-fatal */ }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
static loadSession(sessionPath) {
|
|
476
|
+
try {
|
|
477
|
+
if (!existsSync(sessionPath)) return null;
|
|
478
|
+
const data = readFileSync(sessionPath, 'utf8');
|
|
479
|
+
const messages = JSON.parse(data);
|
|
480
|
+
if (Array.isArray(messages) && messages.length > 0) return messages;
|
|
481
|
+
} catch { /* corrupted session */ }
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
clearSession() {
|
|
486
|
+
try {
|
|
487
|
+
if (existsSync(this.sessionPath)) unlinkSync(this.sessionPath);
|
|
488
|
+
} catch { /* non-fatal */ }
|
|
489
|
+
}
|
|
447
490
|
}
|
|
@@ -15,6 +15,7 @@ import { listProviders, getProvider, clearInstallCache, getProviderMetadata, get
|
|
|
15
15
|
import { OllamaProvider } from './providers/ollama.js';
|
|
16
16
|
import { ClaudeCodeProvider } from './providers/claude-code.js';
|
|
17
17
|
import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
|
|
18
|
+
import { ConsentManager } from '../../../moe-training/client/index.js';
|
|
18
19
|
import { validateAgentConfig } from './validate.js';
|
|
19
20
|
import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from './process.js';
|
|
20
21
|
|
|
@@ -4402,6 +4403,81 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4402
4403
|
res.json({ ok: true });
|
|
4403
4404
|
});
|
|
4404
4405
|
|
|
4406
|
+
// --- Training Data ---
|
|
4407
|
+
|
|
4408
|
+
app.get('/api/training/status', (req, res) => {
|
|
4409
|
+
let userId = null;
|
|
4410
|
+
try { userId = ConsentManager.isCaptureEnabled() ? ConsentManager.getOrCreateUserId() : null; } catch (e) { /* no db yet */ }
|
|
4411
|
+
res.json({
|
|
4412
|
+
optedIn: !!daemon.config.training_opt_in,
|
|
4413
|
+
userId: userId ? userId.substring(0, 8) + '...' : null,
|
|
4414
|
+
captureActive: !!daemon.trajectoryCapture,
|
|
4415
|
+
sessionsCaptured: daemon.state.get('training_sessions_captured') || 0,
|
|
4416
|
+
envelopesSent: daemon.state.get('training_envelopes_sent') || 0,
|
|
4417
|
+
});
|
|
4418
|
+
});
|
|
4419
|
+
|
|
4420
|
+
app.post('/api/training/opt-in', async (req, res) => {
|
|
4421
|
+
const { enabled } = req.body;
|
|
4422
|
+
if (typeof enabled !== 'boolean') return res.status(400).json({ error: 'enabled must be boolean' });
|
|
4423
|
+
|
|
4424
|
+
daemon.config.training_opt_in = enabled;
|
|
4425
|
+
const { saveConfig } = await import('./firstrun.js');
|
|
4426
|
+
saveConfig(daemon.grooveDir, daemon.config);
|
|
4427
|
+
|
|
4428
|
+
if (enabled) {
|
|
4429
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4430
|
+
const consent = new ConsentManager();
|
|
4431
|
+
try {
|
|
4432
|
+
consent.recordConsent(userId, true, '1.0');
|
|
4433
|
+
} finally {
|
|
4434
|
+
consent.close();
|
|
4435
|
+
}
|
|
4436
|
+
await daemon._initTrajectoryCapture();
|
|
4437
|
+
daemon.state.set('training_enrolled_at', new Date().toISOString());
|
|
4438
|
+
} else {
|
|
4439
|
+
if (daemon.trajectoryCapture) {
|
|
4440
|
+
try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
|
|
4441
|
+
daemon.trajectoryCapture = null;
|
|
4442
|
+
}
|
|
4443
|
+
try {
|
|
4444
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4445
|
+
const consent = new ConsentManager();
|
|
4446
|
+
try {
|
|
4447
|
+
consent.revokeConsent(userId);
|
|
4448
|
+
} finally {
|
|
4449
|
+
consent.close();
|
|
4450
|
+
}
|
|
4451
|
+
} catch (e) { /* no user_id yet */ }
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
daemon.broadcast({ type: 'training:status', data: { optedIn: enabled, captureActive: !!daemon.trajectoryCapture } });
|
|
4455
|
+
if (daemon.audit) daemon.audit.log('training.consent', { opt_in: enabled });
|
|
4456
|
+
res.json({ ok: true, optedIn: enabled });
|
|
4457
|
+
});
|
|
4458
|
+
|
|
4459
|
+
app.post('/api/training/opt-in/delete', async (req, res) => {
|
|
4460
|
+
try {
|
|
4461
|
+
daemon.config.training_opt_in = false;
|
|
4462
|
+
const { saveConfig } = await import('./firstrun.js');
|
|
4463
|
+
saveConfig(daemon.grooveDir, daemon.config);
|
|
4464
|
+
if (daemon.trajectoryCapture) {
|
|
4465
|
+
try { await daemon.trajectoryCapture.shutdown(); } catch (e) { /* */ }
|
|
4466
|
+
daemon.trajectoryCapture = null;
|
|
4467
|
+
}
|
|
4468
|
+
try {
|
|
4469
|
+
const userId = ConsentManager.getOrCreateUserId();
|
|
4470
|
+
const consent = new ConsentManager();
|
|
4471
|
+
try { consent.revokeConsent(userId); } finally { consent.close(); }
|
|
4472
|
+
} catch (e) { /* */ }
|
|
4473
|
+
daemon.broadcast({ type: 'training:status', data: { optedIn: false, captureActive: false } });
|
|
4474
|
+
if (daemon.audit) daemon.audit.log('training.delete', {});
|
|
4475
|
+
res.json({ ok: true, deleted: true });
|
|
4476
|
+
} catch (e) {
|
|
4477
|
+
res.status(500).json({ error: 'Failed to delete data' });
|
|
4478
|
+
}
|
|
4479
|
+
});
|
|
4480
|
+
|
|
4405
4481
|
// --- Config ---
|
|
4406
4482
|
|
|
4407
4483
|
app.get('/api/config', (req, res) => {
|
|
@@ -4419,6 +4495,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
4419
4495
|
'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
|
|
4420
4496
|
'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
|
|
4421
4497
|
'onboardingDismissed', 'defaultModel', 'defaultChatProvider', 'defaultChatModel',
|
|
4498
|
+
'training_opt_in',
|
|
4422
4499
|
];
|
|
4423
4500
|
for (const key of Object.keys(req.body)) {
|
|
4424
4501
|
if (!ALLOWED_KEYS.includes(key)) {
|
|
@@ -43,6 +43,7 @@ import { LlamaServerManager } from './llama-server.js';
|
|
|
43
43
|
import { RepoImporter } from './repo-import.js';
|
|
44
44
|
import { ConversationManager } from './conversations.js';
|
|
45
45
|
import { Toys } from './toys.js';
|
|
46
|
+
import { TrajectoryCapture, ConsentManager } from '../../../moe-training/client/index.js';
|
|
46
47
|
import { isFirstRun, runFirstTimeSetup, loadConfig, saveConfig, printWelcome } from './firstrun.js';
|
|
47
48
|
import { bindDaemon as bindGrooveNetworkDaemon } from './providers/groove-network.js';
|
|
48
49
|
import { setProviderPaths } from './providers/index.js';
|
|
@@ -151,6 +152,17 @@ export class Daemon {
|
|
|
151
152
|
this.tunnelManager = new TunnelManager(this);
|
|
152
153
|
this.repoImporter = new RepoImporter(this);
|
|
153
154
|
this.toys = new Toys(this);
|
|
155
|
+
this.trajectoryCapture = null;
|
|
156
|
+
|
|
157
|
+
// Hook teams.delete to clean up agent-loop session files
|
|
158
|
+
const originalTeamDelete = this.teams.delete.bind(this.teams);
|
|
159
|
+
this.teams.delete = (id) => {
|
|
160
|
+
const agents = this.registry.getAll().filter(a => a.teamId === id);
|
|
161
|
+
const agentIds = agents.map(a => a.id);
|
|
162
|
+
const result = originalTeamDelete(id);
|
|
163
|
+
if (agentIds.length > 0) this.state.cleanupSessions(agentIds);
|
|
164
|
+
return result;
|
|
165
|
+
};
|
|
154
166
|
|
|
155
167
|
// Subscription state (populated by Electron IPC or direct auth)
|
|
156
168
|
this.authToken = null;
|
|
@@ -390,6 +402,20 @@ export class Daemon {
|
|
|
390
402
|
client.send(payload);
|
|
391
403
|
}
|
|
392
404
|
}
|
|
405
|
+
if (this.trajectoryCapture && message.type) {
|
|
406
|
+
try {
|
|
407
|
+
if (['approval:request', 'approval:resolved', 'conflict:detected', 'qc:activated'].includes(message.type)) {
|
|
408
|
+
const agentId = message.data?.agentId || message.agentId;
|
|
409
|
+
if (agentId) {
|
|
410
|
+
this.trajectoryCapture.onCoordinationEvent(agentId, {
|
|
411
|
+
type: message.type,
|
|
412
|
+
data: message.data,
|
|
413
|
+
timestamp: Date.now(),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch (e) { /* fail silent */ }
|
|
418
|
+
}
|
|
393
419
|
}
|
|
394
420
|
|
|
395
421
|
async setAuthToken(token) {
|
|
@@ -527,6 +553,17 @@ export class Daemon {
|
|
|
527
553
|
const purged = this.locks.purgeOrphans(runningIds);
|
|
528
554
|
if (purged > 0) console.log(` Purged ${purged} orphaned lock(s) from previous session`);
|
|
529
555
|
|
|
556
|
+
// Mark agents with saved agent-loop sessions as resumable
|
|
557
|
+
const resumableIds = new Set(this.state.getResumableSessions());
|
|
558
|
+
if (resumableIds.size > 0) {
|
|
559
|
+
for (const agent of this.registry.getAll()) {
|
|
560
|
+
if (resumableIds.has(agent.id) && (agent.status === 'running' || agent.status === 'idle' || agent.status === 'completed')) {
|
|
561
|
+
this.registry.update(agent.id, { status: 'completed', hasSession: true, pid: null });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
console.log(` ${resumableIds.size} agent-loop session(s) marked as resumable`);
|
|
565
|
+
}
|
|
566
|
+
|
|
530
567
|
// Migrate old agents without teamId to default team
|
|
531
568
|
this.teams.migrateAgents();
|
|
532
569
|
|
|
@@ -542,6 +579,7 @@ export class Daemon {
|
|
|
542
579
|
printWelcome(this.port, this.host, this._firstRun);
|
|
543
580
|
|
|
544
581
|
// Start background services
|
|
582
|
+
this._initTrajectoryCapture().catch(() => {});
|
|
545
583
|
this.journalist.start();
|
|
546
584
|
this.rotator.start();
|
|
547
585
|
this.scheduler.start();
|
|
@@ -627,6 +665,23 @@ export class Daemon {
|
|
|
627
665
|
});
|
|
628
666
|
}
|
|
629
667
|
|
|
668
|
+
async _initTrajectoryCapture() {
|
|
669
|
+
if (!this.config.training_opt_in) return;
|
|
670
|
+
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
|
+
}
|
|
680
|
+
} catch (e) {
|
|
681
|
+
// Training capture is never critical
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
630
685
|
_startGarbageCollector() {
|
|
631
686
|
// Run once on startup, then every 24 hours
|
|
632
687
|
this._gc();
|
|
@@ -737,6 +792,12 @@ export class Daemon {
|
|
|
737
792
|
// Disconnect all SSH tunnels
|
|
738
793
|
this.tunnelManager.shutdown();
|
|
739
794
|
|
|
795
|
+
// Shut down training capture
|
|
796
|
+
if (this.trajectoryCapture) {
|
|
797
|
+
try { await this.trajectoryCapture.shutdown(); } catch (e) { /* fail silent */ }
|
|
798
|
+
this.trajectoryCapture = null;
|
|
799
|
+
}
|
|
800
|
+
|
|
740
801
|
// Kill all agent processes, stop MCP servers, and stop inference servers
|
|
741
802
|
await this.processes.killAll();
|
|
742
803
|
if (this.preview) await this.preview.killAll();
|