groove-dev 0.27.140 → 0.27.141
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/integrations-registry.json +12 -44
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +82 -16
- package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
- package/node_modules/@groove-dev/daemon/src/keeper.js +3 -3
- package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
- package/node_modules/@groove-dev/daemon/src/process.js +76 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +8 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +7 -2
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
- package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -49
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
- package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +10 -10
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/node_modules/@groove-dev/gui/src/stores/groove.js +110 -32
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +114 -56
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +9 -9
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
- package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/integrations-registry.json +12 -44
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +82 -16
- package/packages/daemon/src/integrations.js +10 -0
- package/packages/daemon/src/journalist.js +169 -0
- package/packages/daemon/src/keeper.js +3 -3
- package/packages/daemon/src/model-lab.js +11 -0
- package/packages/daemon/src/process.js +76 -0
- package/packages/daemon/src/validate.js +8 -0
- package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
- package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-chat.jsx +3 -3
- package/packages/gui/src/components/agents/agent-file-tree.jsx +7 -2
- package/packages/gui/src/components/agents/code-review.jsx +5 -4
- package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
- package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
- package/packages/gui/src/components/editor/file-tree.jsx +2 -49
- package/packages/gui/src/components/editor/terminal.jsx +15 -4
- package/packages/gui/src/components/keeper/global-modals.jsx +10 -10
- package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/packages/gui/src/stores/groove.js +110 -32
- package/packages/gui/src/views/agents.jsx +114 -56
- package/packages/gui/src/views/memory.jsx +9 -9
- package/packages/gui/src/views/model-lab.jsx +1 -6
- package/packages/gui/src/views/models.jsx +658 -565
- package/plan_files/keeper-manual.md +53 -42
- package/node_modules/@groove-dev/gui/dist/assets/index-BV9CAiw1.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DK6UIz0n.js +0 -8698
- package/packages/gui/dist/assets/index-BV9CAiw1.css +0 -1
- package/packages/gui/dist/assets/index-DK6UIz0n.js +0 -8698
|
@@ -432,10 +432,10 @@
|
|
|
432
432
|
"icon": "elevenlabs",
|
|
433
433
|
"tags": ["voice", "tts", "audio", "speech"],
|
|
434
434
|
"roles": ["creative", "cmo", "support"],
|
|
435
|
-
"npmPackage": "elevenlabs-mcp",
|
|
435
|
+
"npmPackage": "@angelogiacco/elevenlabs-mcp-server",
|
|
436
436
|
"transport": "stdio",
|
|
437
437
|
"command": "npx",
|
|
438
|
-
"args": ["-y", "elevenlabs-mcp"],
|
|
438
|
+
"args": ["-y", "@angelogiacco/elevenlabs-mcp-server"],
|
|
439
439
|
"authType": "api-key",
|
|
440
440
|
"envKeys": [
|
|
441
441
|
{ "key": "ELEVENLABS_API_KEY", "label": "API Key", "required": true }
|
|
@@ -491,10 +491,10 @@
|
|
|
491
491
|
"icon": "hubspot",
|
|
492
492
|
"tags": ["crm", "sales", "contacts", "marketing"],
|
|
493
493
|
"roles": ["cmo", "analyst", "support"],
|
|
494
|
-
"npmPackage": "hubspot-
|
|
494
|
+
"npmPackage": "@hubspot/mcp-server",
|
|
495
495
|
"transport": "stdio",
|
|
496
496
|
"command": "npx",
|
|
497
|
-
"args": ["-y", "hubspot-
|
|
497
|
+
"args": ["-y", "@hubspot/mcp-server"],
|
|
498
498
|
"authType": "api-key",
|
|
499
499
|
"envKeys": [
|
|
500
500
|
{ "key": "HUBSPOT_ACCESS_TOKEN", "label": "Private App Token", "required": true }
|
|
@@ -701,10 +701,10 @@
|
|
|
701
701
|
"icon": "mixpanel",
|
|
702
702
|
"tags": ["analytics", "events", "funnels", "users"],
|
|
703
703
|
"roles": ["analyst", "cmo", "cfo"],
|
|
704
|
-
"npmPackage": "mixpanel
|
|
704
|
+
"npmPackage": "@mercuryml/mcp-mixpanel",
|
|
705
705
|
"transport": "stdio",
|
|
706
706
|
"command": "npx",
|
|
707
|
-
"args": ["-y", "mixpanel
|
|
707
|
+
"args": ["-y", "@mercuryml/mcp-mixpanel"],
|
|
708
708
|
"authType": "api-key",
|
|
709
709
|
"envKeys": [
|
|
710
710
|
{ "key": "MIXPANEL_API_SECRET", "label": "API Secret", "required": true }
|
|
@@ -761,10 +761,10 @@
|
|
|
761
761
|
"icon": "airtable",
|
|
762
762
|
"tags": ["database", "spreadsheet", "content", "crm"],
|
|
763
763
|
"roles": ["ea", "cmo", "analyst"],
|
|
764
|
-
"npmPackage": "airtable-mcp",
|
|
764
|
+
"npmPackage": "airtable-mcp-server",
|
|
765
765
|
"transport": "stdio",
|
|
766
766
|
"command": "npx",
|
|
767
|
-
"args": ["-y", "airtable-mcp"],
|
|
767
|
+
"args": ["-y", "airtable-mcp-server"],
|
|
768
768
|
"authType": "api-key",
|
|
769
769
|
"envKeys": [
|
|
770
770
|
{ "key": "AIRTABLE_API_KEY", "label": "Personal Access Token", "required": true }
|
|
@@ -823,10 +823,10 @@
|
|
|
823
823
|
"icon": "intercom",
|
|
824
824
|
"tags": ["chat", "support", "messaging", "customers"],
|
|
825
825
|
"roles": ["support", "cmo"],
|
|
826
|
-
"npmPackage": "intercom
|
|
826
|
+
"npmPackage": "@pipeworx/mcp-intercom",
|
|
827
827
|
"transport": "stdio",
|
|
828
828
|
"command": "npx",
|
|
829
|
-
"args": ["-y", "intercom
|
|
829
|
+
"args": ["-y", "@pipeworx/mcp-intercom"],
|
|
830
830
|
"authType": "api-key",
|
|
831
831
|
"envKeys": [
|
|
832
832
|
{ "key": "INTERCOM_ACCESS_TOKEN", "label": "Access Token", "required": true }
|
|
@@ -853,10 +853,10 @@
|
|
|
853
853
|
"icon": "twilio",
|
|
854
854
|
"tags": ["sms", "voice", "phone", "whatsapp"],
|
|
855
855
|
"roles": ["ea", "support", "cmo"],
|
|
856
|
-
"npmPackage": "twilio-mcp",
|
|
856
|
+
"npmPackage": "@twilio-alpha/mcp",
|
|
857
857
|
"transport": "stdio",
|
|
858
858
|
"command": "npx",
|
|
859
|
-
"args": ["-y", "twilio-mcp"],
|
|
859
|
+
"args": ["-y", "@twilio-alpha/mcp"],
|
|
860
860
|
"authType": "api-key",
|
|
861
861
|
"envKeys": [
|
|
862
862
|
{ "key": "TWILIO_ACCOUNT_SID", "label": "Account SID", "placeholder": "AC...", "required": true },
|
|
@@ -939,37 +939,5 @@
|
|
|
939
939
|
"rating": 0,
|
|
940
940
|
"ratingCount": 0,
|
|
941
941
|
"verified": "community"
|
|
942
|
-
},
|
|
943
|
-
{
|
|
944
|
-
"id": "plaid",
|
|
945
|
-
"name": "Plaid",
|
|
946
|
-
"description": "Bank connections and financial data",
|
|
947
|
-
"category": "finance",
|
|
948
|
-
"icon": "plaid",
|
|
949
|
-
"tags": ["banking", "transactions", "finance", "accounts"],
|
|
950
|
-
"roles": ["cfo", "analyst"],
|
|
951
|
-
"npmPackage": "plaid-mcp",
|
|
952
|
-
"transport": "stdio",
|
|
953
|
-
"command": "npx",
|
|
954
|
-
"args": ["-y", "plaid-mcp"],
|
|
955
|
-
"authType": "api-key",
|
|
956
|
-
"envKeys": [
|
|
957
|
-
{ "key": "PLAID_CLIENT_ID", "label": "Client ID", "required": true },
|
|
958
|
-
{ "key": "PLAID_SECRET", "label": "Secret", "required": true },
|
|
959
|
-
{ "key": "PLAID_ENV", "label": "Environment", "placeholder": "sandbox", "required": false }
|
|
960
|
-
],
|
|
961
|
-
"setupUrl": "https://dashboard.plaid.com/team/keys",
|
|
962
|
-
"setupSteps": [
|
|
963
|
-
"Click the link below to open Plaid team keys",
|
|
964
|
-
"Copy your Client ID and Secret",
|
|
965
|
-
"Use 'sandbox' environment for testing"
|
|
966
|
-
],
|
|
967
|
-
"requiresApproval": [],
|
|
968
|
-
"agentInstructions": "## Plaid Integration\n\nYou have Plaid connected via GROOVE.\n\n### API\n`POST http://localhost:31415/api/integrations/plaid/exec`\nBody: `{\"tool\": \"<tool>\", \"params\": {...}}`\n\n### Common Tools\n- `get_accounts` — params: {access_token}\n- `get_transactions` — params: {access_token, start_date, end_date}\n- `get_balance` — params: {access_token}\n\n### Rules\n- Use sandbox environment for testing\n- Never expose access_tokens in output",
|
|
969
|
-
"featured": false,
|
|
970
|
-
"downloads": 0,
|
|
971
|
-
"rating": 0,
|
|
972
|
-
"ratingCount": 0,
|
|
973
|
-
"verified": "community"
|
|
974
942
|
}
|
|
975
943
|
]
|
|
@@ -3231,8 +3231,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3231
3231
|
const raw = readdirSync(fullPath, { withFileTypes: true });
|
|
3232
3232
|
const entries = [];
|
|
3233
3233
|
|
|
3234
|
+
const HIDDEN_DIRS = new Set(['.git', 'node_modules', '.groove', '.next', '.nuxt', '__pycache__', '.venv', 'dist', '.cache']);
|
|
3235
|
+
const HIDDEN_FILES = new Set(['.DS_Store']);
|
|
3236
|
+
|
|
3234
3237
|
const dirs = raw.filter((e) => {
|
|
3235
|
-
if (e.name
|
|
3238
|
+
if (HIDDEN_FILES.has(e.name) || HIDDEN_DIRS.has(e.name)) return false;
|
|
3236
3239
|
if (e.isDirectory()) return true;
|
|
3237
3240
|
if (e.isSymbolicLink()) {
|
|
3238
3241
|
try { return statSync(resolve(fullPath, e.name)).isDirectory(); }
|
|
@@ -3241,7 +3244,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
3241
3244
|
return false;
|
|
3242
3245
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
3243
3246
|
const files = raw.filter((e) => {
|
|
3244
|
-
if (e.name
|
|
3247
|
+
if (HIDDEN_FILES.has(e.name)) return false;
|
|
3245
3248
|
if (e.isFile()) return true;
|
|
3246
3249
|
if (e.isSymbolicLink()) {
|
|
3247
3250
|
try { return statSync(resolve(fullPath, e.name)).isFile(); }
|
|
@@ -6986,30 +6989,93 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
6986
6989
|
app.post('/api/lab/inference', async (req, res) => {
|
|
6987
6990
|
try {
|
|
6988
6991
|
const params = validateLabInferenceParams(req.body);
|
|
6992
|
+
const rt = daemon.modelLab.getRuntime(params.runtimeId);
|
|
6993
|
+
if (!rt) throw new Error('Runtime not found');
|
|
6994
|
+
|
|
6995
|
+
const url = new URL(`${rt.endpoint}/v1/chat/completions`);
|
|
6996
|
+
const reqHeaders = { 'Content-Type': 'application/json' };
|
|
6997
|
+
if (rt.apiKey) reqHeaders['Authorization'] = `Bearer ${rt.apiKey}`;
|
|
6998
|
+
|
|
6999
|
+
const body = {
|
|
7000
|
+
model: params.model,
|
|
7001
|
+
messages: params.messages,
|
|
7002
|
+
stream: true,
|
|
7003
|
+
};
|
|
7004
|
+
const pb = params.parameters || {};
|
|
7005
|
+
if (pb.temperature !== undefined) body.temperature = pb.temperature;
|
|
7006
|
+
if (pb.top_p !== undefined) body.top_p = pb.top_p;
|
|
7007
|
+
if (pb.top_k !== undefined) body.top_k = pb.top_k;
|
|
7008
|
+
if (pb.repeat_penalty !== undefined) body.repeat_penalty = pb.repeat_penalty;
|
|
7009
|
+
if (pb.max_tokens !== undefined) body.max_tokens = pb.max_tokens;
|
|
7010
|
+
if (pb.stop !== undefined) body.stop = pb.stop;
|
|
7011
|
+
if (pb.frequency_penalty !== undefined) body.frequency_penalty = pb.frequency_penalty;
|
|
7012
|
+
if (pb.presence_penalty !== undefined) body.presence_penalty = pb.presence_penalty;
|
|
7013
|
+
|
|
7014
|
+
const payload = JSON.stringify(body);
|
|
7015
|
+
|
|
7016
|
+
// Use Node http module directly — Electron's fetch has stream issues
|
|
7017
|
+
const { request: httpRequest } = await import('http');
|
|
7018
|
+
const upstream = await new Promise((resolve, reject) => {
|
|
7019
|
+
const r = httpRequest({
|
|
7020
|
+
hostname: url.hostname,
|
|
7021
|
+
port: url.port,
|
|
7022
|
+
path: url.pathname,
|
|
7023
|
+
method: 'POST',
|
|
7024
|
+
headers: { ...reqHeaders, 'Content-Length': Buffer.byteLength(payload) },
|
|
7025
|
+
timeout: 300000,
|
|
7026
|
+
}, resolve);
|
|
7027
|
+
r.on('error', reject);
|
|
7028
|
+
r.on('timeout', () => { r.destroy(); reject(new Error('Upstream timeout')); });
|
|
7029
|
+
r.write(payload);
|
|
7030
|
+
r.end();
|
|
7031
|
+
});
|
|
7032
|
+
|
|
7033
|
+
if (upstream.statusCode !== 200) {
|
|
7034
|
+
let errMsg = `HTTP ${upstream.statusCode}`;
|
|
7035
|
+
try {
|
|
7036
|
+
const chunks = [];
|
|
7037
|
+
for await (const c of upstream) chunks.push(c);
|
|
7038
|
+
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
7039
|
+
errMsg = data.error?.message || errMsg;
|
|
7040
|
+
} catch { /* ignore */ }
|
|
7041
|
+
throw new Error(errMsg);
|
|
7042
|
+
}
|
|
6989
7043
|
|
|
7044
|
+
// Pipe raw OpenAI-compatible SSE straight to client
|
|
6990
7045
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
6991
7046
|
res.setHeader('Cache-Control', 'no-cache');
|
|
6992
7047
|
res.setHeader('Connection', 'keep-alive');
|
|
6993
7048
|
res.setHeader('X-Accel-Buffering', 'no');
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7049
|
+
upstream.pipe(res);
|
|
7050
|
+
|
|
7051
|
+
// Collect content for session persistence
|
|
7052
|
+
if (params.sessionId) {
|
|
7053
|
+
let full = '';
|
|
7054
|
+
upstream.on('data', (chunk) => {
|
|
7055
|
+
const text = chunk.toString('utf8');
|
|
7056
|
+
for (const line of text.split('\n')) {
|
|
7057
|
+
const trimmed = line.trim();
|
|
7058
|
+
if (!trimmed.startsWith('data: ')) continue;
|
|
7059
|
+
const d = trimmed.slice(6);
|
|
7060
|
+
if (d === '[DONE]') continue;
|
|
7061
|
+
try {
|
|
7062
|
+
const p = JSON.parse(d);
|
|
7063
|
+
const c = p.choices?.[0]?.delta?.content;
|
|
7064
|
+
if (c) full += c;
|
|
7065
|
+
} catch { /* skip */ }
|
|
7066
|
+
}
|
|
7067
|
+
});
|
|
7068
|
+
upstream.on('end', () => {
|
|
7069
|
+
if (full) daemon.modelLab._appendToSession(params.sessionId, params.messages, { role: 'assistant', content: full });
|
|
7070
|
+
});
|
|
7006
7071
|
}
|
|
7072
|
+
|
|
7073
|
+
req.on('close', () => { upstream.destroy(); });
|
|
7007
7074
|
} catch (err) {
|
|
7008
7075
|
if (!res.headersSent) {
|
|
7009
7076
|
res.status(400).json({ error: err.message });
|
|
7010
7077
|
} else {
|
|
7011
|
-
res.
|
|
7012
|
-
res.end();
|
|
7078
|
+
try { res.end(); } catch { /* ignore */ }
|
|
7013
7079
|
}
|
|
7014
7080
|
}
|
|
7015
7081
|
});
|
|
@@ -127,6 +127,16 @@ export class IntegrationStore {
|
|
|
127
127
|
if (this._isInstalled(integrationId)) throw new Error(`Integration already installed: ${integrationId}`);
|
|
128
128
|
|
|
129
129
|
if (entry.npmPackage) {
|
|
130
|
+
// Pre-validate: check the package exists on npm before attempting install
|
|
131
|
+
try {
|
|
132
|
+
execFileSync('npm', ['view', entry.npmPackage, 'version'], {
|
|
133
|
+
stdio: 'pipe',
|
|
134
|
+
timeout: 10_000,
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
137
|
+
throw new Error(`Package ${entry.npmPackage} is not available on npm. Use the agent-assisted install flow instead.`);
|
|
138
|
+
}
|
|
139
|
+
|
|
130
140
|
try {
|
|
131
141
|
execFileSync('npm', ['install', '--legacy-peer-deps', entry.npmPackage], {
|
|
132
142
|
cwd: this.integrationsDir,
|
|
@@ -994,6 +994,175 @@ export class Journalist {
|
|
|
994
994
|
return brief;
|
|
995
995
|
}
|
|
996
996
|
|
|
997
|
+
// --- Conversation Thread Extraction (for idle resume) ---
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Extract the actual user↔assistant conversation from stream-json logs.
|
|
1001
|
+
* Returns the dialogue in chronological order — user messages interleaved
|
|
1002
|
+
* with Claude's text responses. This preserves the "why" context that
|
|
1003
|
+
* handoff briefs lose through summarization.
|
|
1004
|
+
*
|
|
1005
|
+
* Budget: keeps recent turns verbatim, summarizes oldest if over maxChars.
|
|
1006
|
+
*/
|
|
1007
|
+
extractConversationThread(agent, { maxChars = 60000 } = {}) {
|
|
1008
|
+
const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.name}.log`);
|
|
1009
|
+
if (!existsSync(logPath)) return null;
|
|
1010
|
+
|
|
1011
|
+
let content;
|
|
1012
|
+
try {
|
|
1013
|
+
content = readFileSync(logPath, 'utf8');
|
|
1014
|
+
} catch { return null; }
|
|
1015
|
+
|
|
1016
|
+
const lines = content.split('\n');
|
|
1017
|
+
const turns = []; // { role: 'user'|'assistant', text, timestamp }
|
|
1018
|
+
|
|
1019
|
+
for (const line of lines) {
|
|
1020
|
+
if (!line.trim() || line.startsWith('[')) continue;
|
|
1021
|
+
try {
|
|
1022
|
+
const data = JSON.parse(line);
|
|
1023
|
+
|
|
1024
|
+
// User messages
|
|
1025
|
+
if (data.type === 'user' && data.message?.content) {
|
|
1026
|
+
const msgContent = data.message.content;
|
|
1027
|
+
let text = '';
|
|
1028
|
+
if (typeof msgContent === 'string') {
|
|
1029
|
+
text = msgContent;
|
|
1030
|
+
} else if (Array.isArray(msgContent)) {
|
|
1031
|
+
// Extract text blocks, skip tool_result blocks (noise)
|
|
1032
|
+
text = msgContent
|
|
1033
|
+
.filter((b) => b.type === 'text' && b.text)
|
|
1034
|
+
.map((b) => b.text)
|
|
1035
|
+
.join('\n');
|
|
1036
|
+
}
|
|
1037
|
+
if (text.trim().length > 5) {
|
|
1038
|
+
turns.push({ role: 'user', text: text.trim(), timestamp: data.timestamp });
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Assistant text responses (what Claude said — the reasoning/explanations)
|
|
1043
|
+
if (data.type === 'assistant' && data.message?.content) {
|
|
1044
|
+
const blocks = Array.isArray(data.message.content) ? data.message.content : [];
|
|
1045
|
+
const textParts = blocks
|
|
1046
|
+
.filter((b) => b.type === 'text' && b.text && b.text.trim().length > 20)
|
|
1047
|
+
.map((b) => b.text.trim());
|
|
1048
|
+
if (textParts.length > 0) {
|
|
1049
|
+
turns.push({ role: 'assistant', text: textParts.join('\n'), timestamp: data.timestamp });
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
} catch { /* skip non-JSON */ }
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (turns.length === 0) return null;
|
|
1056
|
+
|
|
1057
|
+
// Deduplicate consecutive same-role turns (merge them)
|
|
1058
|
+
const merged = [];
|
|
1059
|
+
for (const turn of turns) {
|
|
1060
|
+
const last = merged[merged.length - 1];
|
|
1061
|
+
if (last && last.role === turn.role) {
|
|
1062
|
+
last.text += '\n' + turn.text;
|
|
1063
|
+
} else {
|
|
1064
|
+
merged.push({ ...turn });
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Build the thread — keep recent turns verbatim, truncate old ones if over budget
|
|
1069
|
+
let thread = '';
|
|
1070
|
+
const formatted = merged.map((t) => {
|
|
1071
|
+
const label = t.role === 'user' ? 'USER' : 'CLAUDE';
|
|
1072
|
+
return `[${label}]:\n${t.text}`;
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
// Start from the end (most recent) and work backwards to fill budget
|
|
1076
|
+
const recentFirst = [...formatted].reverse();
|
|
1077
|
+
const kept = [];
|
|
1078
|
+
let totalLen = 0;
|
|
1079
|
+
|
|
1080
|
+
for (const entry of recentFirst) {
|
|
1081
|
+
if (totalLen + entry.length > maxChars) {
|
|
1082
|
+
// Truncate this entry to fit remaining budget
|
|
1083
|
+
const remaining = maxChars - totalLen;
|
|
1084
|
+
if (remaining > 200) {
|
|
1085
|
+
kept.push(entry.slice(0, remaining) + '\n[...truncated]');
|
|
1086
|
+
}
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
kept.push(entry);
|
|
1090
|
+
totalLen += entry.length;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Reverse back to chronological order
|
|
1094
|
+
kept.reverse();
|
|
1095
|
+
thread = kept.join('\n\n---\n\n');
|
|
1096
|
+
|
|
1097
|
+
return thread;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Build a full context-resume prompt that preserves the conversation
|
|
1102
|
+
* thread so a fresh agent picks up where the previous session left off.
|
|
1103
|
+
*/
|
|
1104
|
+
buildConversationResumePrompt(agent, userMessage) {
|
|
1105
|
+
const thread = this.extractConversationThread(agent);
|
|
1106
|
+
if (!thread) return null;
|
|
1107
|
+
|
|
1108
|
+
const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
|
|
1109
|
+
const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 5, 1000) || '';
|
|
1110
|
+
|
|
1111
|
+
let prompt = [
|
|
1112
|
+
`# Session Context Resume`,
|
|
1113
|
+
``,
|
|
1114
|
+
`You are continuing a session that went idle. Below is the full conversation`,
|
|
1115
|
+
`from your previous session — your actual exchanges with the user. Pick up`,
|
|
1116
|
+
`exactly where you left off. The user's new message follows at the end.`,
|
|
1117
|
+
``,
|
|
1118
|
+
`Role: ${agent.role} | Provider: ${agent.provider} | Scope: ${agent.scope?.join(', ') || 'unrestricted'}`,
|
|
1119
|
+
agent.workingDir ? `Working directory: ${agent.workingDir}` : '',
|
|
1120
|
+
``,
|
|
1121
|
+
constraints ? `## Project Constraints\n\n${constraints}\n` : '',
|
|
1122
|
+
discoveries ? `## Known Issues & Fixes\n\n${discoveries}\n` : '',
|
|
1123
|
+
`## Previous Conversation\n\n${thread}`,
|
|
1124
|
+
``,
|
|
1125
|
+
`---`,
|
|
1126
|
+
``,
|
|
1127
|
+
`## New Message From User`,
|
|
1128
|
+
``,
|
|
1129
|
+
userMessage,
|
|
1130
|
+
``,
|
|
1131
|
+
`Continue seamlessly from the conversation above. You have the full context of what was discussed, what was tried, what worked and what didn't. Do not ask the user to repeat anything.`,
|
|
1132
|
+
].filter(Boolean).join('\n');
|
|
1133
|
+
|
|
1134
|
+
// Hard cap at 80K chars (~20K tokens) to leave plenty of room in context window
|
|
1135
|
+
if (prompt.length > 80000) {
|
|
1136
|
+
// Re-extract with smaller budget and rebuild
|
|
1137
|
+
const smallerThread = this.extractConversationThread(agent, { maxChars: 40000 });
|
|
1138
|
+
if (smallerThread) {
|
|
1139
|
+
prompt = [
|
|
1140
|
+
`# Session Context Resume`,
|
|
1141
|
+
``,
|
|
1142
|
+
`You are continuing a session that went idle. Below is the conversation`,
|
|
1143
|
+
`from your previous session (older turns summarized to fit). Pick up`,
|
|
1144
|
+
`exactly where you left off.`,
|
|
1145
|
+
``,
|
|
1146
|
+
`Role: ${agent.role} | Scope: ${agent.scope?.join(', ') || 'unrestricted'}`,
|
|
1147
|
+
agent.workingDir ? `Working directory: ${agent.workingDir}` : '',
|
|
1148
|
+
``,
|
|
1149
|
+
constraints ? `## Project Constraints\n\n${constraints}\n` : '',
|
|
1150
|
+
`## Previous Conversation\n\n${smallerThread}`,
|
|
1151
|
+
``,
|
|
1152
|
+
`---`,
|
|
1153
|
+
``,
|
|
1154
|
+
`## New Message From User`,
|
|
1155
|
+
``,
|
|
1156
|
+
userMessage,
|
|
1157
|
+
``,
|
|
1158
|
+
`Continue seamlessly. Do not ask the user to repeat anything.`,
|
|
1159
|
+
].filter(Boolean).join('\n');
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return prompt;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
997
1166
|
// --- Workspace Grouping ---
|
|
998
1167
|
|
|
999
1168
|
/**
|
|
@@ -254,10 +254,10 @@ export class Keeper {
|
|
|
254
254
|
// ── Command parser ────────────────────────────────────────
|
|
255
255
|
|
|
256
256
|
static parseCommand(text) {
|
|
257
|
-
const cmdMatch = text.match(
|
|
257
|
+
const cmdMatch = text.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]|\b(save|append|update|delete|view|doc|link|read)\b(?=\s+#[\w/.-])/i);
|
|
258
258
|
if (!cmdMatch) return null;
|
|
259
|
-
const command = cmdMatch[1].toLowerCase();
|
|
260
|
-
const rest = text.slice(cmdMatch[0].length).trim();
|
|
259
|
+
const command = (cmdMatch[1] || cmdMatch[2]).toLowerCase();
|
|
260
|
+
const rest = text.slice(cmdMatch.index + cmdMatch[0].length).trim();
|
|
261
261
|
|
|
262
262
|
if (command === 'instruct') {
|
|
263
263
|
return { command, tags: [], extra: null };
|
|
@@ -95,6 +95,17 @@ export class ModelLab {
|
|
|
95
95
|
removeRuntime(id) {
|
|
96
96
|
const rt = this.runtimes.get(id);
|
|
97
97
|
if (!rt) return null;
|
|
98
|
+
|
|
99
|
+
// Stop the llama-server process if this is a local model runtime
|
|
100
|
+
if (rt._localModelId) {
|
|
101
|
+
const mm = this.daemon.modelManager;
|
|
102
|
+
const ls = this.daemon.llamaServer;
|
|
103
|
+
if (mm && ls) {
|
|
104
|
+
const modelPath = mm.getModelPath(rt._localModelId);
|
|
105
|
+
if (modelPath) ls.stopServer(modelPath).catch(() => {});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
98
109
|
this.runtimes.delete(id);
|
|
99
110
|
this._saveRuntimes();
|
|
100
111
|
this.daemon.broadcast({ type: 'lab:runtime:removed', data: { id } });
|
|
@@ -1976,6 +1976,10 @@ For normal file edits within your scope, proceed without review.
|
|
|
1976
1976
|
* Resume a completed agent's session with a new message.
|
|
1977
1977
|
* Uses --resume SESSION_ID for zero cold-start continuation.
|
|
1978
1978
|
* Falls back to full spawn if no session ID available.
|
|
1979
|
+
*
|
|
1980
|
+
* If the agent has been idle for longer than IDLE_CONTEXT_THRESHOLD,
|
|
1981
|
+
* spawns fresh with the full conversation thread instead of --resume.
|
|
1982
|
+
* This avoids degraded context from internal compaction.
|
|
1979
1983
|
*/
|
|
1980
1984
|
async resume(agentId, message) {
|
|
1981
1985
|
const { registry, locks } = this.daemon;
|
|
@@ -1996,6 +2000,23 @@ For normal file edits within your scope, proceed without review.
|
|
|
1996
2000
|
return this.daemon.rotator.rotate(agentId, { additionalPrompt: message });
|
|
1997
2001
|
}
|
|
1998
2002
|
|
|
2003
|
+
// Context-aware idle resume: if the agent has been idle long enough for
|
|
2004
|
+
// internal compaction to degrade context, spawn fresh with the full
|
|
2005
|
+
// conversation thread instead of resuming the compacted session.
|
|
2006
|
+
const IDLE_CONTEXT_THRESHOLD_MS = 5 * 60_000; // 5 minutes (matches prompt cache TTL)
|
|
2007
|
+
const idleMs = agent.lastActivity
|
|
2008
|
+
? Date.now() - new Date(agent.lastActivity).getTime()
|
|
2009
|
+
: Infinity;
|
|
2010
|
+
|
|
2011
|
+
if (idleMs > IDLE_CONTEXT_THRESHOLD_MS && this.daemon.journalist) {
|
|
2012
|
+
const resumePrompt = this.daemon.journalist.buildConversationResumePrompt(agent, message);
|
|
2013
|
+
if (resumePrompt) {
|
|
2014
|
+
console.log(`[Groove] Agent ${agent.name} idle ${Math.round(idleMs / 60000)}min — using conversation-thread resume`);
|
|
2015
|
+
// Use rotation machinery but with our richer prompt instead of handoff brief
|
|
2016
|
+
return this._conversationResume(agentId, agent, resumePrompt);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1999
2020
|
const provider = getProvider(agent.provider || 'claude-code');
|
|
2000
2021
|
if (!provider?.buildResumeCommand) {
|
|
2001
2022
|
return this.daemon.rotator.rotate(agentId, { additionalPrompt: message });
|
|
@@ -2125,6 +2146,61 @@ For normal file edits within your scope, proceed without review.
|
|
|
2125
2146
|
return newAgent;
|
|
2126
2147
|
}
|
|
2127
2148
|
|
|
2149
|
+
/**
|
|
2150
|
+
* Conversation-thread resume: spawns a fresh agent with the full
|
|
2151
|
+
* user↔assistant conversation as context instead of using --resume
|
|
2152
|
+
* on a potentially compacted session. Used when idle > threshold.
|
|
2153
|
+
*/
|
|
2154
|
+
async _conversationResume(agentId, agent, resumePrompt) {
|
|
2155
|
+
const { registry, locks } = this.daemon;
|
|
2156
|
+
const config = { ...agent };
|
|
2157
|
+
|
|
2158
|
+
if (this.handles.has(agentId)) {
|
|
2159
|
+
await this.kill(agentId);
|
|
2160
|
+
}
|
|
2161
|
+
registry.remove(agentId);
|
|
2162
|
+
locks.release(agentId);
|
|
2163
|
+
|
|
2164
|
+
const newAgent = await this.spawn({
|
|
2165
|
+
role: config.role,
|
|
2166
|
+
scope: config.scope,
|
|
2167
|
+
provider: config.provider,
|
|
2168
|
+
model: config.model,
|
|
2169
|
+
prompt: resumePrompt,
|
|
2170
|
+
permission: config.permission || 'full',
|
|
2171
|
+
workingDir: config.workingDir,
|
|
2172
|
+
name: config.name,
|
|
2173
|
+
teamId: config.teamId,
|
|
2174
|
+
isRotation: true,
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
// Carry cumulative token count for tracking
|
|
2178
|
+
if (config.tokensUsed > 0) {
|
|
2179
|
+
registry.update(newAgent.id, { tokensUsed: config.tokensUsed });
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
if (this.daemon.timeline) {
|
|
2183
|
+
this.daemon.timeline.recordEvent('conversation_resume', {
|
|
2184
|
+
agentId: newAgent.id,
|
|
2185
|
+
oldAgentId: agentId,
|
|
2186
|
+
agentName: newAgent.name,
|
|
2187
|
+
role: config.role,
|
|
2188
|
+
idleMinutes: Math.round((Date.now() - new Date(config.lastActivity).getTime()) / 60000),
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
this.daemon.broadcast({
|
|
2193
|
+
type: 'rotation:complete',
|
|
2194
|
+
agentId: newAgent.id,
|
|
2195
|
+
agentName: newAgent.name,
|
|
2196
|
+
oldAgentId: agentId,
|
|
2197
|
+
reason: 'conversation_resume',
|
|
2198
|
+
tokensSaved: 0,
|
|
2199
|
+
});
|
|
2200
|
+
|
|
2201
|
+
return newAgent;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2128
2204
|
async _resumeAgentLoop(agentId, agent, message, provider) {
|
|
2129
2205
|
const { registry, locks } = this.daemon;
|
|
2130
2206
|
const config = { ...agent };
|
|
@@ -112,6 +112,12 @@ export function validateAgentConfig(config) {
|
|
|
112
112
|
if (!isNaN(v) && v >= 0 && v <= 100) verbosity = Math.round(v);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
const validEffort = ['min', 'low', 'default', 'high', 'max'];
|
|
116
|
+
const effort = validEffort.includes(config.effort) ? config.effort : undefined;
|
|
117
|
+
|
|
118
|
+
const validRouting = ['fixed', 'auto', 'auto-floor'];
|
|
119
|
+
const routingMode = validRouting.includes(config.routingMode) ? config.routingMode : undefined;
|
|
120
|
+
|
|
115
121
|
// Return sanitized config (only known fields)
|
|
116
122
|
return {
|
|
117
123
|
role: config.role,
|
|
@@ -131,6 +137,8 @@ export function validateAgentConfig(config) {
|
|
|
131
137
|
reasoningEffort: numericReasoningEffort ?? reasoningEffort,
|
|
132
138
|
temperature,
|
|
133
139
|
verbosity,
|
|
140
|
+
effort,
|
|
141
|
+
routingMode,
|
|
134
142
|
labPresetId: (typeof config.labPresetId === 'string' && config.labPresetId.length <= 64) ? config.labPresetId : undefined,
|
|
135
143
|
keeperTags: Array.isArray(config.keeperTags) ? config.keeperTags.filter(t => typeof t === 'string').slice(0, 20) : undefined,
|
|
136
144
|
};
|