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
|
@@ -540,27 +540,76 @@ export class Journalist {
|
|
|
540
540
|
}
|
|
541
541
|
|
|
542
542
|
async callHeadless(prompt, { trackAs = '__journalist__' } = {}) {
|
|
543
|
-
|
|
544
|
-
// Priority: claude-code (cheapest via Haiku) > gemini > codex > ollama
|
|
545
|
-
const priority = ['claude-code', 'gemini', 'codex', 'ollama'];
|
|
543
|
+
const priority = ['claude-code', 'gemini', 'codex', 'grok', 'ollama'];
|
|
546
544
|
const installed = getInstalledProviders();
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
545
|
+
|
|
546
|
+
for (const providerId of priority) {
|
|
547
|
+
if (!installed.some((i) => i.id === providerId)) continue;
|
|
548
|
+
|
|
549
|
+
const provider = getProvider(providerId);
|
|
550
|
+
if (!provider) continue;
|
|
551
|
+
|
|
552
|
+
const selectedModel = provider.constructor.models?.find((m) => m.tier === 'medium')
|
|
553
|
+
|| provider.constructor.models?.find((m) => m.tier === 'light')
|
|
554
|
+
|| provider.constructor.models?.[0];
|
|
555
|
+
const modelId = selectedModel?.id || null;
|
|
556
|
+
|
|
557
|
+
// Try CLI headless command first
|
|
558
|
+
const headlessCmd = provider.buildHeadlessCommand(prompt, modelId);
|
|
559
|
+
if (headlessCmd) {
|
|
560
|
+
try {
|
|
561
|
+
return await this._execHeadlessCmd(headlessCmd, trackAs, modelId);
|
|
562
|
+
} catch {
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Fallback: streamChat for API-only providers (e.g. Grok)
|
|
568
|
+
if (typeof provider.streamChat === 'function') {
|
|
569
|
+
const apiKey = this.daemon.credentials?.getKey(providerId);
|
|
570
|
+
if (!apiKey) continue;
|
|
571
|
+
|
|
572
|
+
try {
|
|
573
|
+
const text = await new Promise((resolve, reject) => {
|
|
574
|
+
let collected = '';
|
|
575
|
+
const ctrl = provider.streamChat(
|
|
576
|
+
[{ role: 'user', content: prompt }],
|
|
577
|
+
modelId,
|
|
578
|
+
apiKey,
|
|
579
|
+
(chunk) => { collected += chunk; },
|
|
580
|
+
() => resolve(collected),
|
|
581
|
+
(err) => reject(err),
|
|
582
|
+
);
|
|
583
|
+
if (!ctrl) reject(new Error('streamChat unavailable'));
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
if (text) {
|
|
587
|
+
const estimatedInputTokens = Math.ceil(prompt.length / 4);
|
|
588
|
+
const estimatedOutputTokens = Math.ceil(text.length / 4);
|
|
589
|
+
if (this.daemon?.tokens) {
|
|
590
|
+
this.daemon.tokens.record(trackAs, {
|
|
591
|
+
tokens: estimatedInputTokens + estimatedOutputTokens,
|
|
592
|
+
inputTokens: estimatedInputTokens,
|
|
593
|
+
outputTokens: estimatedOutputTokens,
|
|
594
|
+
model: modelId,
|
|
595
|
+
estimatedCostUsd: 0,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return text;
|
|
599
|
+
}
|
|
600
|
+
} catch {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
550
604
|
}
|
|
551
|
-
const provider = getProvider(providerId);
|
|
552
605
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|| provider.constructor.models?.find((m) => m.tier === 'light')
|
|
556
|
-
|| provider.constructor.models?.[0];
|
|
557
|
-
const modelId = selectedModel?.id || null;
|
|
606
|
+
throw new Error('No provider available for synthesis');
|
|
607
|
+
}
|
|
558
608
|
|
|
559
|
-
|
|
609
|
+
_execHeadlessCmd(headlessCmd, trackAs, modelId) {
|
|
560
610
|
const { command, args, env, stdin: stdinData } = headlessCmd;
|
|
561
611
|
|
|
562
612
|
return new Promise((resolve, reject) => {
|
|
563
|
-
// Use spawn with stdin pipe if provider needs it (e.g., Ollama)
|
|
564
613
|
if (stdinData) {
|
|
565
614
|
let stdout = '';
|
|
566
615
|
const proc = cpSpawn(command, args, {
|
|
@@ -576,7 +625,6 @@ export class Journalist {
|
|
|
576
625
|
clearTimeout(timer);
|
|
577
626
|
if (code !== 0) return reject(new Error(`Headless exited with code ${code}`));
|
|
578
627
|
this._recordHeadlessUsage(stdout, trackAs, modelId);
|
|
579
|
-
// Process stdout same as execFile path below
|
|
580
628
|
const lines = stdout.split('\n');
|
|
581
629
|
for (const line of lines) {
|
|
582
630
|
try {
|
|
@@ -590,17 +638,14 @@ export class Journalist {
|
|
|
590
638
|
return;
|
|
591
639
|
}
|
|
592
640
|
|
|
593
|
-
|
|
641
|
+
execFile(command, args, {
|
|
594
642
|
env: { ...process.env, ...env },
|
|
595
643
|
cwd: this.daemon.projectDir,
|
|
596
644
|
maxBuffer: 1024 * 1024 * 5,
|
|
597
645
|
timeout: 60_000,
|
|
598
646
|
}, (err, stdout, stderr) => {
|
|
599
647
|
if (err) return reject(err);
|
|
600
|
-
|
|
601
648
|
this._recordHeadlessUsage(stdout, trackAs, modelId);
|
|
602
|
-
|
|
603
|
-
// Parse stream-json output to extract the result text
|
|
604
649
|
const lines = stdout.split('\n');
|
|
605
650
|
for (const line of lines) {
|
|
606
651
|
try {
|
|
@@ -610,8 +655,6 @@ export class Journalist {
|
|
|
610
655
|
}
|
|
611
656
|
} catch { /* skip */ }
|
|
612
657
|
}
|
|
613
|
-
|
|
614
|
-
// Fallback: return raw stdout
|
|
615
658
|
resolve(stdout);
|
|
616
659
|
});
|
|
617
660
|
});
|
|
@@ -466,6 +466,15 @@ export class ProcessManager {
|
|
|
466
466
|
model: isAutoRouted ? null : config.model, // Set after routing
|
|
467
467
|
});
|
|
468
468
|
|
|
469
|
+
if (this.daemon.trajectoryCapture) {
|
|
470
|
+
try {
|
|
471
|
+
const teamSize = registry.getAll().filter(a => a.status === 'active' || a.status === 'running' || a.status === 'starting').length;
|
|
472
|
+
this.daemon.trajectoryCapture.onAgentSpawn(
|
|
473
|
+
agent.id, providerName, config.model || null, config.role, teamSize
|
|
474
|
+
).catch(() => {});
|
|
475
|
+
} catch (e) { /* fail silent */ }
|
|
476
|
+
}
|
|
477
|
+
|
|
469
478
|
// Auto-route: let the router pick the model based on role/complexity
|
|
470
479
|
if (isAutoRouted) {
|
|
471
480
|
const { router } = this.daemon;
|
|
@@ -669,6 +678,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
669
678
|
const logPath = resolve(logDir, `${sanitizeFilename(agent.name)}.log`);
|
|
670
679
|
const logStream = createWriteStream(logPath, { flags: 'a', mode: 0o600 });
|
|
671
680
|
|
|
681
|
+
// Inject API key from credential store for agent-loop providers
|
|
682
|
+
if (provider.constructor.useAgentLoop && this.daemon.credentials) {
|
|
683
|
+
const storedKey = this.daemon.credentials.getKey(providerName);
|
|
684
|
+
if (storedKey) spawnConfig.apiKey = storedKey;
|
|
685
|
+
}
|
|
686
|
+
|
|
672
687
|
// ─── Agent Loop path (local models with built-in agentic runtime) ───
|
|
673
688
|
if (provider.constructor.useAgentLoop) {
|
|
674
689
|
const loopConfig = provider.getLoopConfig(spawnConfig);
|
|
@@ -878,6 +893,22 @@ For normal file edits within your scope, proceed without review.
|
|
|
878
893
|
});
|
|
879
894
|
}
|
|
880
895
|
|
|
896
|
+
if (this.daemon.trajectoryCapture) {
|
|
897
|
+
try {
|
|
898
|
+
if (finalStatus === 'completed') {
|
|
899
|
+
this.daemon.trajectoryCapture.onAgentComplete(agent.id, {
|
|
900
|
+
status: 'SUCCESS', exit_code: code, signal,
|
|
901
|
+
});
|
|
902
|
+
} else {
|
|
903
|
+
this.daemon.trajectoryCapture.onAgentCrash(agent.id,
|
|
904
|
+
signal ? 'Killed by signal ' + signal : 'Exit code ' + code
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
const count = (this.daemon.state.get('training_sessions_captured') || 0) + 1;
|
|
908
|
+
this.daemon.state.set('training_sessions_captured', count);
|
|
909
|
+
} catch (e) { /* fail silent */ }
|
|
910
|
+
}
|
|
911
|
+
|
|
881
912
|
this.daemon.broadcast({
|
|
882
913
|
type: 'agent:exit',
|
|
883
914
|
agentId: agent.id,
|
|
@@ -1025,6 +1056,12 @@ For normal file edits within your scope, proceed without review.
|
|
|
1025
1056
|
} catch (err) {
|
|
1026
1057
|
console.error(`[Groove] parseOutput error for ${agentId}: ${err.message}`);
|
|
1027
1058
|
}
|
|
1059
|
+
if (this.daemon.trajectoryCapture) {
|
|
1060
|
+
try {
|
|
1061
|
+
const parsed = JSON.parse(line);
|
|
1062
|
+
this.daemon.trajectoryCapture.onStdoutLine(agentId, parsed);
|
|
1063
|
+
} catch (e) { /* fail silent — non-JSON lines are expected */ }
|
|
1064
|
+
}
|
|
1028
1065
|
}
|
|
1029
1066
|
}
|
|
1030
1067
|
|
|
@@ -1500,6 +1537,15 @@ For normal file edits within your scope, proceed without review.
|
|
|
1500
1537
|
const agent = registry.get(agentId);
|
|
1501
1538
|
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
|
1502
1539
|
|
|
1540
|
+
// Agent-loop providers: resume from saved session file
|
|
1541
|
+
const resumeProvider = getProvider(agent.provider || 'claude-code');
|
|
1542
|
+
if (resumeProvider?.constructor?.useAgentLoop) {
|
|
1543
|
+
const sessionPath = resolve(this.daemon.grooveDir, 'sessions', `${agentId}.json`);
|
|
1544
|
+
if (existsSync(sessionPath)) {
|
|
1545
|
+
return this._resumeAgentLoop(agentId, agent, message, resumeProvider);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1503
1549
|
// If no session ID, fall back to rotation (handoff brief)
|
|
1504
1550
|
if (!agent.sessionId) {
|
|
1505
1551
|
return this.daemon.rotator.rotate(agentId, { additionalPrompt: message });
|
|
@@ -1645,6 +1691,155 @@ For normal file edits within your scope, proceed without review.
|
|
|
1645
1691
|
return newAgent;
|
|
1646
1692
|
}
|
|
1647
1693
|
|
|
1694
|
+
async _resumeAgentLoop(agentId, agent, message, provider) {
|
|
1695
|
+
const { registry, locks } = this.daemon;
|
|
1696
|
+
const config = { ...agent };
|
|
1697
|
+
|
|
1698
|
+
if (this.handles.has(agentId)) {
|
|
1699
|
+
await this.kill(agentId);
|
|
1700
|
+
}
|
|
1701
|
+
registry.remove(agentId);
|
|
1702
|
+
locks.release(agentId);
|
|
1703
|
+
|
|
1704
|
+
const newAgent = registry.add({
|
|
1705
|
+
role: config.role,
|
|
1706
|
+
scope: config.scope,
|
|
1707
|
+
provider: config.provider,
|
|
1708
|
+
model: config.model,
|
|
1709
|
+
prompt: config.prompt,
|
|
1710
|
+
permission: config.permission,
|
|
1711
|
+
workingDir: config.workingDir || undefined,
|
|
1712
|
+
name: config.name,
|
|
1713
|
+
teamId: config.teamId,
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
if (config.tokensUsed > 0) {
|
|
1717
|
+
registry.update(newAgent.id, { tokensUsed: config.tokensUsed });
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
if (newAgent.scope?.length > 0) {
|
|
1721
|
+
locks.register(newAgent.id, newAgent.scope, newAgent.workingDir);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// Move session file from old agent ID to new agent ID
|
|
1725
|
+
const sessionsDir = resolve(this.daemon.grooveDir, 'sessions');
|
|
1726
|
+
const oldSessionPath = resolve(sessionsDir, `${agentId}.json`);
|
|
1727
|
+
const newSessionPath = resolve(sessionsDir, `${newAgent.id}.json`);
|
|
1728
|
+
if (oldSessionPath !== newSessionPath && existsSync(oldSessionPath)) {
|
|
1729
|
+
try {
|
|
1730
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
1731
|
+
copyFileSync(oldSessionPath, newSessionPath);
|
|
1732
|
+
unlinkSync(oldSessionPath);
|
|
1733
|
+
} catch { /* AgentLoop will start fresh if file is missing */ }
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
const logDir = resolve(this.daemon.grooveDir, 'logs');
|
|
1737
|
+
mkdirSync(logDir, { recursive: true });
|
|
1738
|
+
const logPath = resolve(logDir, `${sanitizeFilename(newAgent.name)}.log`);
|
|
1739
|
+
const logStream = createWriteStream(logPath, { flags: 'a', mode: 0o600 });
|
|
1740
|
+
logStream.write(`[${new Date().toISOString()}] GROOVE resuming agent-loop session\n`);
|
|
1741
|
+
|
|
1742
|
+
// Inject API key
|
|
1743
|
+
const spawnConfig = { ...newAgent, ...config };
|
|
1744
|
+
if (this.daemon.credentials) {
|
|
1745
|
+
const storedKey = this.daemon.credentials.getKey(newAgent.provider);
|
|
1746
|
+
if (storedKey) spawnConfig.apiKey = storedKey;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
const loopConfig = provider.getLoopConfig(spawnConfig);
|
|
1750
|
+
const loop = new AgentLoop({ daemon: this.daemon, agent: newAgent, loopConfig, logStream });
|
|
1751
|
+
|
|
1752
|
+
this.handles.set(newAgent.id, { loop, logStream });
|
|
1753
|
+
registry.update(newAgent.id, { status: 'running' });
|
|
1754
|
+
|
|
1755
|
+
if (this.daemon.timeline) {
|
|
1756
|
+
this.daemon.timeline.recordEvent('spawn', {
|
|
1757
|
+
agentId: newAgent.id, agentName: newAgent.name, role: newAgent.role,
|
|
1758
|
+
provider: newAgent.provider, model: loopConfig.model,
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
loop.on('output', (output) => {
|
|
1763
|
+
this._handleAgentOutput(newAgent.id, output);
|
|
1764
|
+
});
|
|
1765
|
+
|
|
1766
|
+
loop.on('exit', ({ code, signal, status }) => {
|
|
1767
|
+
logStream.write(`[${new Date().toISOString()}] Agent loop exited: status=${status}\n`);
|
|
1768
|
+
logStream.end();
|
|
1769
|
+
this.handles.delete(newAgent.id);
|
|
1770
|
+
|
|
1771
|
+
const throttle = this._streamThrottle.get(newAgent.id);
|
|
1772
|
+
if (throttle?.timer) clearTimeout(throttle.timer);
|
|
1773
|
+
this._streamThrottle.delete(newAgent.id);
|
|
1774
|
+
this.peakContextUsage.delete(newAgent.id);
|
|
1775
|
+
this.pendingMessages.delete(newAgent.id);
|
|
1776
|
+
registry.update(newAgent.id, { status, pid: null });
|
|
1777
|
+
|
|
1778
|
+
const agentData = registry.get(newAgent.id);
|
|
1779
|
+
|
|
1780
|
+
if (this.daemon.timeline) {
|
|
1781
|
+
const evtType = status === 'completed' ? 'complete' : status === 'crashed' ? 'crash' : 'kill';
|
|
1782
|
+
this.daemon.timeline.recordEvent(evtType, {
|
|
1783
|
+
agentId: newAgent.id, agentName: newAgent.name, role: newAgent.role,
|
|
1784
|
+
finalTokens: agentData?.tokensUsed || 0, costUsd: agentData?.costUsd || 0,
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
this.daemon.broadcast({ type: 'agent:exit', agentId: newAgent.id, code: code || 0, signal, status });
|
|
1789
|
+
if (this.daemon.integrations) this.daemon.integrations.refreshMcpJson();
|
|
1790
|
+
|
|
1791
|
+
if (status === 'completed' && this.daemon.journalist) {
|
|
1792
|
+
const turns = agentData?.turns || 0;
|
|
1793
|
+
const tok = agentData?.tokensUsed || 0;
|
|
1794
|
+
if (turns > 1 || tok >= 100) this.daemon.journalist.requestSynthesis('completion');
|
|
1795
|
+
}
|
|
1796
|
+
this._checkPhase2(newAgent.id);
|
|
1797
|
+
|
|
1798
|
+
if (status === 'completed') {
|
|
1799
|
+
const files = this.daemon.journalist?.getAgentFiles(newAgent) || [];
|
|
1800
|
+
if (files.length > 0) this._triggerIdleQC(newAgent);
|
|
1801
|
+
this._processHandoffs(newAgent);
|
|
1802
|
+
if (this._rotatingAgents.has(newAgent.id)) {
|
|
1803
|
+
this._rotatingAgents.delete(newAgent.id);
|
|
1804
|
+
} else {
|
|
1805
|
+
this._writeCompletionHandoff(newAgent).catch(err =>
|
|
1806
|
+
console.error(`[Groove] Completion handoff failed for ${newAgent.name}:`, err.message));
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
loop.on('error', ({ message: errMsg }) => {
|
|
1812
|
+
this.daemon.broadcast({
|
|
1813
|
+
type: 'agent:output', agentId: newAgent.id,
|
|
1814
|
+
data: { type: 'activity', subtype: 'error', data: errMsg },
|
|
1815
|
+
});
|
|
1816
|
+
});
|
|
1817
|
+
|
|
1818
|
+
loop.start();
|
|
1819
|
+
loop.sendMessage(message);
|
|
1820
|
+
|
|
1821
|
+
this.daemon.broadcast({
|
|
1822
|
+
type: 'rotation:complete',
|
|
1823
|
+
agentId: newAgent.id,
|
|
1824
|
+
agentName: newAgent.name,
|
|
1825
|
+
oldAgentId: agentId,
|
|
1826
|
+
reason: 'resume',
|
|
1827
|
+
tokensSaved: 0,
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
return newAgent;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
cleanupTeamSessions(teamId) {
|
|
1834
|
+
const { registry } = this.daemon;
|
|
1835
|
+
const teamAgents = registry.getAll().filter(a => a.teamId === teamId);
|
|
1836
|
+
const sessionsDir = resolve(this.daemon.grooveDir, 'sessions');
|
|
1837
|
+
for (const agent of teamAgents) {
|
|
1838
|
+
const sessionPath = resolve(sessionsDir, `${agent.id}.json`);
|
|
1839
|
+
try { if (existsSync(sessionPath)) unlinkSync(sessionPath); } catch { /* non-fatal */ }
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1648
1843
|
/**
|
|
1649
1844
|
* Stop the agent's current work without killing the agent.
|
|
1650
1845
|
* The process is terminated but the agent stays in the registry with its
|
|
@@ -1752,6 +1947,10 @@ For normal file edits within your scope, proceed without review.
|
|
|
1752
1947
|
const agent = this.daemon.registry.get(agentId);
|
|
1753
1948
|
const wrapped = agent ? wrapWithRoleReminder(agent.role, message) : message;
|
|
1754
1949
|
|
|
1950
|
+
if (this.daemon.trajectoryCapture) {
|
|
1951
|
+
try { this.daemon.trajectoryCapture.onUserMessage(agentId, message); } catch (e) { /* fail silent */ }
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1755
1954
|
loop.sendMessage(wrapped).catch(() => {});
|
|
1756
1955
|
return true;
|
|
1757
1956
|
}
|
|
@@ -37,6 +37,7 @@ export class GrokProvider extends Provider {
|
|
|
37
37
|
{ id: 'grok-3-mini', name: 'Grok 3 Mini', tier: 'light', maxContext: 131072, pricing: { input: 0.0003, output: 0.0005 } },
|
|
38
38
|
{ id: 'grok-imagine-image', name: 'Grok Imagine', tier: 'medium', type: 'image', pricing: { perImage: 0.07 } },
|
|
39
39
|
];
|
|
40
|
+
static useAgentLoop = true;
|
|
40
41
|
|
|
41
42
|
static isInstalled() {
|
|
42
43
|
return true; // API-only, no CLI needed
|
|
@@ -54,6 +55,20 @@ export class GrokProvider extends Provider {
|
|
|
54
55
|
return null; // No CLI
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
getLoopConfig(agent) {
|
|
59
|
+
return {
|
|
60
|
+
apiBase: 'https://api.x.ai/v1',
|
|
61
|
+
model: agent.model || 'grok-4-1-fast',
|
|
62
|
+
contextWindow: 131072,
|
|
63
|
+
temperature: 0.1,
|
|
64
|
+
maxResponseTokens: 16384,
|
|
65
|
+
stream: true,
|
|
66
|
+
apiKey: agent.apiKey,
|
|
67
|
+
headers: {},
|
|
68
|
+
introContext: agent.introContext || '',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
switchModel() {
|
|
58
73
|
return false;
|
|
59
74
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// GROOVE — State Persistence
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
-
import { readFileSync, existsSync } from 'fs';
|
|
4
|
+
import { readFileSync, existsSync, readdirSync, unlinkSync } from 'fs';
|
|
5
5
|
import { writeFile } from 'node:fs/promises';
|
|
6
6
|
import { resolve } from 'path';
|
|
7
7
|
|
|
8
8
|
export class StateManager {
|
|
9
9
|
constructor(grooveDir) {
|
|
10
|
+
this.grooveDir = grooveDir;
|
|
10
11
|
this.path = resolve(grooveDir, 'state.json');
|
|
11
12
|
this.data = {};
|
|
12
13
|
}
|
|
@@ -32,4 +33,22 @@ export class StateManager {
|
|
|
32
33
|
set(key, value) {
|
|
33
34
|
this.data[key] = value;
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
getResumableSessions() {
|
|
38
|
+
const sessionsDir = resolve(this.grooveDir, 'sessions');
|
|
39
|
+
if (!existsSync(sessionsDir)) return [];
|
|
40
|
+
try {
|
|
41
|
+
return readdirSync(sessionsDir)
|
|
42
|
+
.filter(f => f.endsWith('.json'))
|
|
43
|
+
.map(f => f.replace('.json', ''));
|
|
44
|
+
} catch { return []; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
cleanupSessions(agentIds) {
|
|
48
|
+
const sessionsDir = resolve(this.grooveDir, 'sessions');
|
|
49
|
+
for (const id of agentIds) {
|
|
50
|
+
const sessionPath = resolve(sessionsDir, `${id}.json`);
|
|
51
|
+
try { if (existsSync(sessionPath)) unlinkSync(sessionPath); } catch { /* non-fatal */ }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
35
54
|
}
|