groove-dev 0.27.139 → 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 +225 -16
- package/node_modules/@groove-dev/daemon/src/index.js +2 -0
- package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +17 -1
- package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
- package/node_modules/@groove-dev/daemon/src/keeper.js +277 -0
- 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 +9 -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/app.jsx +4 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +41 -17
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +10 -3
- 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 -86
- 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 +177 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +11 -6
- 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 +302 -20
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +118 -60
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +67 -219
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +460 -0
- 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 +225 -16
- package/packages/daemon/src/index.js +2 -0
- package/packages/daemon/src/integrations.js +10 -0
- package/packages/daemon/src/introducer.js +17 -1
- package/packages/daemon/src/journalist.js +169 -0
- package/packages/daemon/src/keeper.js +277 -0
- package/packages/daemon/src/model-lab.js +11 -0
- package/packages/daemon/src/process.js +76 -0
- package/packages/daemon/src/validate.js +9 -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/app.jsx +4 -0
- package/packages/gui/src/components/agents/agent-chat.jsx +41 -17
- package/packages/gui/src/components/agents/agent-file-tree.jsx +10 -3
- 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 -86
- package/packages/gui/src/components/editor/terminal.jsx +15 -4
- package/packages/gui/src/components/keeper/global-modals.jsx +177 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +11 -6
- 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 +302 -20
- package/packages/gui/src/views/agents.jsx +118 -60
- package/packages/gui/src/views/editor.jsx +67 -219
- package/packages/gui/src/views/memory.jsx +460 -0
- 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 +295 -0
- package/plan_files/keeper-memory-system.md +223 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +0 -8694
- package/packages/gui/dist/assets/index-AkOtskHS.css +0 -1
- package/packages/gui/dist/assets/index-B4uYLR57.js +0 -8694
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
// Keeper — lightweight tagged memory system.
|
|
3
|
+
// Flat file-backed storage: one tag = one markdown file.
|
|
4
|
+
// Hierarchical namespacing via "/" in tag names (groove/memory-system → groove/memory-system.md).
|
|
5
|
+
|
|
6
|
+
import { resolve, dirname } from 'path';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
8
|
+
|
|
9
|
+
export const KEEPER_COMMANDS = {
|
|
10
|
+
save: { syntax: '[save] #tag', description: 'Save the current message as a tagged memory' },
|
|
11
|
+
append: { syntax: '[append] #tag', description: 'Add content to an existing memory without overwriting' },
|
|
12
|
+
update: { syntax: '[update] #tag', description: 'Open the memory editor to edit in place' },
|
|
13
|
+
delete: { syntax: '[delete] #tag', description: 'Delete a tagged memory' },
|
|
14
|
+
read: { syntax: '[read] #tag1 #tag2 ...', description: 'Send memory content to the agent — agent reads it, chat stays clean' },
|
|
15
|
+
view: { syntax: '[view] #tag', description: 'Open a memory in the viewer' },
|
|
16
|
+
doc: { syntax: '[doc] #tag', description: 'AI reads the full conversation and generates a robust document' },
|
|
17
|
+
link: { syntax: '[link] #tag path/to/doc', description: 'Link a memory tag to a NORTHSTAR or external document' },
|
|
18
|
+
instruct: { syntax: '[instruct]', description: 'Show all Keeper commands and usage instructions' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class Keeper {
|
|
22
|
+
constructor(grooveDir) {
|
|
23
|
+
this.dir = resolve(grooveDir, 'keeper');
|
|
24
|
+
this.indexPath = resolve(this.dir, 'index.json');
|
|
25
|
+
mkdirSync(this.dir, { recursive: true });
|
|
26
|
+
this._index = this._loadIndex();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_loadIndex() {
|
|
30
|
+
try {
|
|
31
|
+
if (existsSync(this.indexPath)) {
|
|
32
|
+
return JSON.parse(readFileSync(this.indexPath, 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
} catch { /* corrupted — rebuild */ }
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_saveIndex() {
|
|
39
|
+
writeFileSync(this.indexPath, JSON.stringify(this._index, null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_tagToPath(tag) {
|
|
43
|
+
const safe = tag.replace(/[^a-zA-Z0-9/_-]/g, '');
|
|
44
|
+
if (!safe) throw new Error('Invalid tag name');
|
|
45
|
+
return resolve(this.dir, `${safe}.md`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_ensureParentDir(filePath) {
|
|
49
|
+
const dir = dirname(filePath);
|
|
50
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_normalize(tag) {
|
|
54
|
+
return tag.replace(/^#/, '').trim().toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── CRUD ──────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
save(tag, content) {
|
|
60
|
+
if (!tag || typeof tag !== 'string') throw new Error('Tag is required');
|
|
61
|
+
if (content === undefined || content === null) throw new Error('Content is required');
|
|
62
|
+
const normalized = this._normalize(tag);
|
|
63
|
+
if (!normalized) throw new Error('Tag is required');
|
|
64
|
+
const filePath = this._tagToPath(normalized);
|
|
65
|
+
this._ensureParentDir(filePath);
|
|
66
|
+
writeFileSync(filePath, String(content));
|
|
67
|
+
this._index[normalized] = {
|
|
68
|
+
tag: normalized,
|
|
69
|
+
type: this._index[normalized]?.type || 'manual',
|
|
70
|
+
links: this._index[normalized]?.links || [],
|
|
71
|
+
createdAt: this._index[normalized]?.createdAt || new Date().toISOString(),
|
|
72
|
+
updatedAt: new Date().toISOString(),
|
|
73
|
+
size: String(content).length,
|
|
74
|
+
};
|
|
75
|
+
this._saveIndex();
|
|
76
|
+
return { tag: normalized, ...this._index[normalized] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
append(tag, content) {
|
|
80
|
+
if (!tag || typeof tag !== 'string') throw new Error('Tag is required');
|
|
81
|
+
if (!content) throw new Error('Content is required');
|
|
82
|
+
const normalized = this._normalize(tag);
|
|
83
|
+
if (!normalized) throw new Error('Tag is required');
|
|
84
|
+
const filePath = this._tagToPath(normalized);
|
|
85
|
+
if (!existsSync(filePath)) {
|
|
86
|
+
return this.save(normalized, content);
|
|
87
|
+
}
|
|
88
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
89
|
+
const updated = existing + '\n\n---\n\n' + String(content);
|
|
90
|
+
writeFileSync(filePath, updated);
|
|
91
|
+
this._index[normalized] = {
|
|
92
|
+
...this._index[normalized],
|
|
93
|
+
updatedAt: new Date().toISOString(),
|
|
94
|
+
size: updated.length,
|
|
95
|
+
};
|
|
96
|
+
this._saveIndex();
|
|
97
|
+
return { tag: normalized, ...this._index[normalized] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get(tag) {
|
|
101
|
+
const normalized = this._normalize(tag);
|
|
102
|
+
if (!normalized) throw new Error('Tag is required');
|
|
103
|
+
const filePath = this._tagToPath(normalized);
|
|
104
|
+
if (!existsSync(filePath)) return null;
|
|
105
|
+
const content = readFileSync(filePath, 'utf8');
|
|
106
|
+
const meta = this._index[normalized] || {};
|
|
107
|
+
return { tag: normalized, content, ...meta };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
pull(tags) {
|
|
111
|
+
const items = [];
|
|
112
|
+
for (const tag of tags) {
|
|
113
|
+
const normalized = this._normalize(tag);
|
|
114
|
+
if (!normalized) continue;
|
|
115
|
+
const item = this.get(normalized);
|
|
116
|
+
if (item) items.push(item);
|
|
117
|
+
const children = this.children(normalized);
|
|
118
|
+
for (const child of children) {
|
|
119
|
+
const childItem = this.get(child.tag);
|
|
120
|
+
if (childItem && !items.some(i => i.tag === childItem.tag)) {
|
|
121
|
+
items.push(childItem);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (items.length === 0) return null;
|
|
126
|
+
return items.map(i => `## #${i.tag}\n\n${i.content}`).join('\n\n---\n\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
update(tag, content) {
|
|
130
|
+
const normalized = this._normalize(tag);
|
|
131
|
+
if (!normalized) throw new Error('Tag is required');
|
|
132
|
+
if (content === undefined || content === null) throw new Error('Content is required');
|
|
133
|
+
const filePath = this._tagToPath(normalized);
|
|
134
|
+
if (!existsSync(filePath)) throw new Error(`Memory #${normalized} does not exist`);
|
|
135
|
+
writeFileSync(filePath, String(content));
|
|
136
|
+
this._index[normalized] = {
|
|
137
|
+
...this._index[normalized],
|
|
138
|
+
updatedAt: new Date().toISOString(),
|
|
139
|
+
size: String(content).length,
|
|
140
|
+
};
|
|
141
|
+
this._saveIndex();
|
|
142
|
+
return { tag: normalized, ...this._index[normalized] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
delete(tag) {
|
|
146
|
+
const normalized = this._normalize(tag);
|
|
147
|
+
if (!normalized) throw new Error('Tag is required');
|
|
148
|
+
const filePath = this._tagToPath(normalized);
|
|
149
|
+
if (!existsSync(filePath)) return false;
|
|
150
|
+
unlinkSync(filePath);
|
|
151
|
+
delete this._index[normalized];
|
|
152
|
+
this._saveIndex();
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Doc (AI-generated) ───────────────────────────────────
|
|
157
|
+
|
|
158
|
+
saveDoc(tag, content) {
|
|
159
|
+
const normalized = this._normalize(tag);
|
|
160
|
+
if (!normalized) throw new Error('Tag is required');
|
|
161
|
+
const result = this.save(normalized, content);
|
|
162
|
+
this._index[normalized].type = 'doc';
|
|
163
|
+
this._saveIndex();
|
|
164
|
+
return { tag: normalized, ...this._index[normalized] };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Links ─────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
link(tag, docPath) {
|
|
170
|
+
const normalized = this._normalize(tag);
|
|
171
|
+
if (!normalized) throw new Error('Tag is required');
|
|
172
|
+
if (!docPath) throw new Error('Document path is required');
|
|
173
|
+
if (!this._index[normalized]) {
|
|
174
|
+
this.save(normalized, `Linked to: ${docPath}`);
|
|
175
|
+
}
|
|
176
|
+
const links = this._index[normalized].links || [];
|
|
177
|
+
if (!links.includes(docPath)) {
|
|
178
|
+
links.push(docPath);
|
|
179
|
+
}
|
|
180
|
+
this._index[normalized].links = links;
|
|
181
|
+
this._saveIndex();
|
|
182
|
+
return { tag: normalized, ...this._index[normalized] };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
unlink(tag, docPath) {
|
|
186
|
+
const normalized = this._normalize(tag);
|
|
187
|
+
if (!normalized) throw new Error('Tag is required');
|
|
188
|
+
if (!this._index[normalized]) return false;
|
|
189
|
+
this._index[normalized].links = (this._index[normalized].links || []).filter(l => l !== docPath);
|
|
190
|
+
this._saveIndex();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Query ─────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
list() {
|
|
197
|
+
return Object.values(this._index).sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
children(parentTag) {
|
|
201
|
+
const normalized = this._normalize(parentTag);
|
|
202
|
+
const prefix = normalized + '/';
|
|
203
|
+
return Object.values(this._index)
|
|
204
|
+
.filter(entry => entry.tag.startsWith(prefix))
|
|
205
|
+
.sort((a, b) => a.tag.localeCompare(b.tag));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
tree() {
|
|
209
|
+
const entries = this.list();
|
|
210
|
+
const roots = [];
|
|
211
|
+
const childMap = {};
|
|
212
|
+
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
const parts = entry.tag.split('/');
|
|
215
|
+
if (parts.length === 1) {
|
|
216
|
+
roots.push({ ...entry, children: [] });
|
|
217
|
+
} else {
|
|
218
|
+
const parent = parts.slice(0, -1).join('/');
|
|
219
|
+
if (!childMap[parent]) childMap[parent] = [];
|
|
220
|
+
childMap[parent].push(entry);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const root of roots) {
|
|
225
|
+
root.children = childMap[root.tag] || [];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const [parent, children] of Object.entries(childMap)) {
|
|
229
|
+
if (!roots.some(r => r.tag === parent) && !parent.includes('/')) {
|
|
230
|
+
roots.push({ tag: parent, virtual: true, children });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return roots.sort((a, b) => a.tag.localeCompare(b.tag));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
search(query) {
|
|
238
|
+
if (!query) return this.list();
|
|
239
|
+
const q = query.toLowerCase();
|
|
240
|
+
const results = [];
|
|
241
|
+
for (const entry of Object.values(this._index)) {
|
|
242
|
+
if (entry.tag.includes(q)) {
|
|
243
|
+
results.push({ ...entry, matchType: 'tag' });
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const item = this.get(entry.tag);
|
|
247
|
+
if (item && item.content.toLowerCase().includes(q)) {
|
|
248
|
+
results.push({ ...entry, matchType: 'content' });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return results.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Command parser ────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
static parseCommand(text) {
|
|
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
|
+
if (!cmdMatch) return null;
|
|
259
|
+
const command = (cmdMatch[1] || cmdMatch[2]).toLowerCase();
|
|
260
|
+
const rest = text.slice(cmdMatch.index + cmdMatch[0].length).trim();
|
|
261
|
+
|
|
262
|
+
if (command === 'instruct') {
|
|
263
|
+
return { command, tags: [], extra: null };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (command === 'link') {
|
|
267
|
+
const linkMatch = rest.match(/^((?:#[\w/.-]+\s*)+)\s+(.+)$/);
|
|
268
|
+
if (!linkMatch) return null;
|
|
269
|
+
const tags = linkMatch[1].match(/#[\w/.-]+/g).map(t => t.replace(/^#/, ''));
|
|
270
|
+
return { command, tags, extra: linkMatch[2].trim() };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const tags = (rest.match(/#[\w/.-]+/g) || []).map(t => t.replace(/^#/, ''));
|
|
274
|
+
if (tags.length === 0 && command !== 'instruct') return null;
|
|
275
|
+
return { command, tags, extra: null };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -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,7 +137,10 @@ 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,
|
|
143
|
+
keeperTags: Array.isArray(config.keeperTags) ? config.keeperTags.filter(t => typeof t === 'string').slice(0, 20) : undefined,
|
|
135
144
|
};
|
|
136
145
|
}
|
|
137
146
|
|