groove-dev 0.26.38 → 0.27.0
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/CHANGELOG.md +59 -0
- package/CLAUDE.md +24 -19
- package/node_modules/@groove-dev/cli/bin/groove.js +2 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/nuke.js +16 -4
- package/node_modules/@groove-dev/cli/src/commands/stop.js +17 -2
- package/node_modules/@groove-dev/daemon/integrations-registry.json +681 -75
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/adaptive.js +23 -25
- package/node_modules/@groove-dev/daemon/src/api.js +346 -22
- package/node_modules/@groove-dev/daemon/src/classifier.js +53 -6
- package/node_modules/@groove-dev/daemon/src/firstrun.js +14 -1
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +2 -2
- package/node_modules/@groove-dev/daemon/src/index.js +28 -4
- package/node_modules/@groove-dev/daemon/src/integrations.js +215 -14
- package/node_modules/@groove-dev/daemon/src/introducer.js +84 -11
- package/node_modules/@groove-dev/daemon/src/journalist.js +43 -1
- package/node_modules/@groove-dev/daemon/src/lockmanager.js +60 -0
- package/node_modules/@groove-dev/daemon/src/mcp-manager.js +270 -0
- package/node_modules/@groove-dev/daemon/src/memory.js +370 -0
- package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +141 -9
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/daemon/src/rotator.js +334 -31
- package/node_modules/@groove-dev/daemon/src/router.js +43 -0
- package/node_modules/@groove-dev/daemon/src/tokentracker.js +70 -18
- package/node_modules/@groove-dev/daemon/src/validate.js +5 -13
- package/node_modules/@groove-dev/daemon/templates/groove-slides.cjs +306 -0
- package/node_modules/@groove-dev/daemon/test/classifier.test.js +3 -5
- package/node_modules/@groove-dev/daemon/test/lockmanager.test.js +64 -0
- package/node_modules/@groove-dev/daemon/test/memory.test.js +252 -0
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +108 -0
- package/node_modules/@groove-dev/daemon/test/router.test.js +64 -0
- package/node_modules/@groove-dev/daemon/test/slides-engine.test.js +230 -0
- package/node_modules/@groove-dev/daemon/test/tokentracker.test.js +78 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-eCrVowF0.js +652 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -4
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -17
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +22 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +53 -21
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +132 -90
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +212 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +495 -174
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +12 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +24 -19
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +391 -61
- package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +29 -7
- package/node_modules/@groove-dev/gui/src/lib/format.js +0 -6
- package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +23 -5
- package/node_modules/@groove-dev/gui/src/stores/groove.js +59 -9
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +84 -10
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +24 -21
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +153 -85
- package/package.json +2 -8
- package/packages/cli/bin/groove.js +2 -0
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/nuke.js +16 -4
- package/packages/cli/src/commands/stop.js +17 -2
- package/packages/daemon/integrations-registry.json +681 -75
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/adaptive.js +23 -25
- package/packages/daemon/src/api.js +346 -22
- package/packages/daemon/src/classifier.js +53 -6
- package/packages/daemon/src/firstrun.js +14 -1
- package/packages/daemon/src/gateways/manager.js +2 -2
- package/packages/daemon/src/index.js +28 -4
- package/packages/daemon/src/integrations.js +215 -14
- package/packages/daemon/src/introducer.js +84 -11
- package/packages/daemon/src/journalist.js +43 -1
- package/packages/daemon/src/lockmanager.js +60 -0
- package/packages/daemon/src/mcp-manager.js +270 -0
- package/packages/daemon/src/memory.js +370 -0
- package/packages/daemon/src/pm.js +1 -1
- package/packages/daemon/src/process.js +141 -9
- package/packages/daemon/src/registry.js +1 -1
- package/packages/daemon/src/rotator.js +334 -31
- package/packages/daemon/src/router.js +43 -0
- package/packages/daemon/src/tokentracker.js +70 -18
- package/packages/daemon/src/validate.js +5 -13
- package/packages/daemon/templates/groove-slides.cjs +306 -0
- package/packages/gui/dist/assets/index-DjORRpF0.css +1 -0
- package/packages/gui/dist/assets/index-eCrVowF0.js +652 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -4
- package/packages/gui/src/components/agents/agent-chat.jsx +26 -17
- package/packages/gui/src/components/agents/agent-config.jsx +22 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +53 -21
- package/packages/gui/src/components/agents/agent-node.jsx +132 -90
- package/packages/gui/src/components/agents/spawn-wizard.jsx +212 -1
- package/packages/gui/src/components/dashboard/cache-ring.jsx +6 -2
- package/packages/gui/src/components/dashboard/intel-panel.jsx +495 -174
- package/packages/gui/src/components/dashboard/kpi-card.jsx +12 -2
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
- package/packages/gui/src/components/layout/app-shell.jsx +24 -19
- package/packages/gui/src/components/layout/command-palette.jsx +2 -2
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +391 -61
- package/packages/gui/src/components/marketplace/marketplace-card.jsx +29 -7
- package/packages/gui/src/lib/format.js +0 -6
- package/packages/gui/src/lib/hooks/use-dashboard.js +23 -5
- package/packages/gui/src/stores/groove.js +59 -9
- package/packages/gui/src/views/agents.jsx +84 -10
- package/packages/gui/src/views/dashboard.jsx +24 -21
- package/packages/gui/src/views/marketplace.jsx +153 -85
- package/node_modules/@groove-dev/gui/dist/assets/index-CEFKgLGB.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-CaKBNWcK.js +0 -638
- package/node_modules/@groove-dev/gui/dist/groove-logo-short.png +0 -0
- package/node_modules/@groove-dev/gui/dist/groove-logo.png +0 -0
- package/node_modules/@groove-dev/gui/public/groove-logo-short.png +0 -0
- package/node_modules/@groove-dev/gui/public/groove-logo.png +0 -0
- package/node_modules/@groove-dev/gui/src/components/ui/dropdown-menu.jsx +0 -60
- package/node_modules/@groove-dev/gui/src/lib/hooks/use-media-query.js +0 -18
- package/node_modules/@radix-ui/react-dropdown-menu/LICENSE +0 -21
- package/node_modules/@radix-ui/react-dropdown-menu/README.md +0 -3
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.mts +0 -97
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.ts +0 -97
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js +0 -337
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs +0 -305
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-dropdown-menu/package.json +0 -75
- package/node_modules/@radix-ui/react-popover/LICENSE +0 -21
- package/node_modules/@radix-ui/react-popover/README.md +0 -3
- package/node_modules/@radix-ui/react-popover/dist/index.d.mts +0 -85
- package/node_modules/@radix-ui/react-popover/dist/index.d.ts +0 -85
- package/node_modules/@radix-ui/react-popover/dist/index.js +0 -352
- package/node_modules/@radix-ui/react-popover/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-popover/dist/index.mjs +0 -320
- package/node_modules/@radix-ui/react-popover/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-popover/package.json +0 -82
- package/node_modules/@radix-ui/react-separator/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/dist/index.d.mts +0 -21
- package/node_modules/@radix-ui/react-separator/dist/index.d.ts +0 -21
- package/node_modules/@radix-ui/react-separator/dist/index.js +0 -65
- package/node_modules/@radix-ui/react-separator/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/dist/index.mjs +0 -32
- package/node_modules/@radix-ui/react-separator/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.mts +0 -52
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.ts +0 -52
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js +0 -80
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs +0 -47
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/package.json +0 -69
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.mts +0 -22
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.ts +0 -22
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js +0 -152
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs +0 -119
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/package.json +0 -64
- package/node_modules/@radix-ui/react-separator/package.json +0 -69
- package/packages/gui/dist/assets/index-CEFKgLGB.css +0 -1
- package/packages/gui/dist/assets/index-CaKBNWcK.js +0 -638
- package/packages/gui/dist/groove-logo-short.png +0 -0
- package/packages/gui/dist/groove-logo.png +0 -0
- package/packages/gui/public/groove-logo-short.png +0 -0
- package/packages/gui/public/groove-logo.png +0 -0
- package/packages/gui/src/components/ui/dropdown-menu.jsx +0 -60
- package/packages/gui/src/lib/hooks/use-media-query.js +0 -18
|
@@ -103,9 +103,9 @@ export class AdaptiveThresholds {
|
|
|
103
103
|
const errorCount = signals.errorCount || 0;
|
|
104
104
|
score -= errorCount * 5;
|
|
105
105
|
|
|
106
|
-
// Repetitions:
|
|
106
|
+
// Repetitions: 3+ Write/Edit to the same file in a sliding window
|
|
107
107
|
const repetitions = signals.repetitions || 0;
|
|
108
|
-
score -= repetitions *
|
|
108
|
+
score -= Math.min(repetitions * 6, 30);
|
|
109
109
|
|
|
110
110
|
// Out-of-scope access: each violation costs 10 points
|
|
111
111
|
const scopeViolations = signals.scopeViolations || 0;
|
|
@@ -116,12 +116,12 @@ export class AdaptiveThresholds {
|
|
|
116
116
|
const toolFailures = signals.toolFailures || 0;
|
|
117
117
|
if (toolCalls > 0) {
|
|
118
118
|
const successRate = (toolCalls - toolFailures) / toolCalls;
|
|
119
|
-
score += Math.round((successRate - 0.8) * 20);
|
|
119
|
+
score += Math.round((successRate - 0.8) * 20);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// File churn:
|
|
122
|
+
// File churn: same file written 5+ times — genuine circular refactoring
|
|
123
123
|
const fileChurn = signals.fileChurn || 0;
|
|
124
|
-
score -= fileChurn *
|
|
124
|
+
score -= fileChurn * 10;
|
|
125
125
|
|
|
126
126
|
// Error trend: increasing errors in second half = degradation
|
|
127
127
|
const errorTrend = signals.errorTrend || 0;
|
|
@@ -162,46 +162,34 @@ export class AdaptiveThresholds {
|
|
|
162
162
|
errorTrend: 0, // errors increasing in recent window → degradation signal
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
-
const recentTools = [];
|
|
166
165
|
const writtenFiles = new Set();
|
|
167
|
-
const fileWriteCounts = {};
|
|
168
|
-
const
|
|
166
|
+
const fileWriteCounts = {};
|
|
167
|
+
const writeEditOps = [];
|
|
169
168
|
|
|
170
169
|
for (const entry of entries) {
|
|
171
170
|
if (entry.type === 'error') {
|
|
172
171
|
signals.errorCount++;
|
|
173
|
-
errorTimestamps.push(entry.timestamp || Date.now());
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
if (entry.type === 'tool') {
|
|
177
175
|
signals.toolCalls++;
|
|
178
176
|
|
|
179
|
-
// Track file writes and churn
|
|
180
177
|
if (entry.tool === 'Write' || entry.tool === 'Edit') {
|
|
181
178
|
if (entry.input) {
|
|
182
179
|
writtenFiles.add(entry.input);
|
|
183
180
|
fileWriteCounts[entry.input] = (fileWriteCounts[entry.input] || 0) + 1;
|
|
181
|
+
writeEditOps.push(entry.input);
|
|
184
182
|
}
|
|
185
183
|
}
|
|
186
184
|
|
|
187
|
-
|
|
188
|
-
// (stream-json emits tool_use then tool_result with is_error)
|
|
189
|
-
if (entry.type === 'tool' && entry.isError) {
|
|
185
|
+
if (entry.isError) {
|
|
190
186
|
signals.toolFailures++;
|
|
191
187
|
}
|
|
192
188
|
|
|
193
|
-
//
|
|
194
|
-
const key = `${entry.tool}:${entry.input}`;
|
|
195
|
-
if (recentTools.includes(key)) {
|
|
196
|
-
signals.repetitions++;
|
|
197
|
-
}
|
|
198
|
-
recentTools.push(key);
|
|
199
|
-
if (recentTools.length > 5) recentTools.shift();
|
|
200
|
-
|
|
201
|
-
// Detect scope violations (simplified check)
|
|
189
|
+
// Scope violations: writes outside declared scope
|
|
202
190
|
if (agentScope && agentScope.length > 0 && entry.input) {
|
|
203
|
-
const file = entry.input;
|
|
204
191
|
if (entry.tool === 'Write' || entry.tool === 'Edit') {
|
|
192
|
+
const file = entry.input;
|
|
205
193
|
const inScope = agentScope.some((pattern) =>
|
|
206
194
|
file.includes(pattern.replace('/**', '').replace('**/', ''))
|
|
207
195
|
);
|
|
@@ -213,9 +201,19 @@ export class AdaptiveThresholds {
|
|
|
213
201
|
|
|
214
202
|
signals.filesWritten = writtenFiles.size;
|
|
215
203
|
|
|
216
|
-
//
|
|
204
|
+
// Repetitions: only count Write/Edit to the same file as circular behavior.
|
|
205
|
+
// Read/Grep/Glob revisits are normal investigation — not degradation.
|
|
206
|
+
// Require 3+ writes to the same file within a 15-op sliding window.
|
|
207
|
+
for (let i = 0; i < writeEditOps.length; i++) {
|
|
208
|
+
const windowStart = Math.max(0, i - 14);
|
|
209
|
+
const window = writeEditOps.slice(windowStart, i);
|
|
210
|
+
const count = window.filter((f) => f === writeEditOps[i]).length;
|
|
211
|
+
if (count >= 2) signals.repetitions++;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// File churn: files written 5+ times (circular refactoring, not normal iteration)
|
|
217
215
|
for (const count of Object.values(fileWriteCounts)) {
|
|
218
|
-
if (count >=
|
|
216
|
+
if (count >= 5) signals.fileChurn++;
|
|
219
217
|
}
|
|
220
218
|
|
|
221
219
|
// Error trend: compare error rate in first half vs second half of session
|
|
@@ -117,6 +117,116 @@ export function createApi(app, daemon) {
|
|
|
117
117
|
res.json(daemon.locks.getAll());
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
// Coordination protocol — agents declare intent on shared resources
|
|
121
|
+
// (npm install, server restart, package.json edit) to prevent races.
|
|
122
|
+
// Returns 423 Locked if another agent holds a conflicting resource.
|
|
123
|
+
app.post('/api/coordination/declare', (req, res) => {
|
|
124
|
+
const { agentId, operation, resources, ttlMs } = req.body || {};
|
|
125
|
+
if (!agentId || !operation || !Array.isArray(resources) || resources.length === 0) {
|
|
126
|
+
return res.status(400).json({ error: 'agentId, operation, and resources[] required' });
|
|
127
|
+
}
|
|
128
|
+
const result = daemon.locks.declareOperation(agentId, operation, resources, ttlMs);
|
|
129
|
+
if (result.conflict) {
|
|
130
|
+
daemon.audit.log('coordination.conflict', { agentId, operation, resource: result.resource, owner: result.owner });
|
|
131
|
+
return res.status(423).json(result);
|
|
132
|
+
}
|
|
133
|
+
daemon.audit.log('coordination.declared', { agentId, operation, resources });
|
|
134
|
+
daemon.broadcast({ type: 'coordination:declared', agentId, operation, resources });
|
|
135
|
+
res.json({ declared: true, operation, resources });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
app.post('/api/coordination/complete', (req, res) => {
|
|
139
|
+
const { agentId } = req.body || {};
|
|
140
|
+
if (!agentId) return res.status(400).json({ error: 'agentId required' });
|
|
141
|
+
const removed = daemon.locks.completeOperation(agentId);
|
|
142
|
+
daemon.audit.log('coordination.completed', { agentId });
|
|
143
|
+
daemon.broadcast({ type: 'coordination:completed', agentId });
|
|
144
|
+
res.json({ completed: removed });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
app.get('/api/coordination', (req, res) => {
|
|
148
|
+
res.json({ operations: daemon.locks.getOperations() });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// --- Persistent Memory (Layer 7) ---
|
|
152
|
+
// Constraints: project rules discovered by agents / set by user
|
|
153
|
+
app.get('/api/memory/constraints', (req, res) => {
|
|
154
|
+
res.json({ constraints: daemon.memory.listConstraints() });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
app.post('/api/memory/constraints', (req, res) => {
|
|
158
|
+
const { text, category } = req.body || {};
|
|
159
|
+
const result = daemon.memory.addConstraint({ text, category });
|
|
160
|
+
if (!result.added && result.error) {
|
|
161
|
+
return res.status(400).json(result);
|
|
162
|
+
}
|
|
163
|
+
if (result.added) {
|
|
164
|
+
daemon.audit.log('memory.constraint.added', { hash: result.hash, category });
|
|
165
|
+
daemon.broadcast({ type: 'memory:constraint:added', hash: result.hash });
|
|
166
|
+
}
|
|
167
|
+
res.json(result);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
app.delete('/api/memory/constraints/:hash', (req, res) => {
|
|
171
|
+
const removed = daemon.memory.removeConstraint(req.params.hash);
|
|
172
|
+
if (removed) {
|
|
173
|
+
daemon.audit.log('memory.constraint.removed', { hash: req.params.hash });
|
|
174
|
+
daemon.broadcast({ type: 'memory:constraint:removed', hash: req.params.hash });
|
|
175
|
+
}
|
|
176
|
+
res.json({ removed });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Handoff chains (per role)
|
|
180
|
+
app.get('/api/memory/handoff-chain/:role', (req, res) => {
|
|
181
|
+
res.json({
|
|
182
|
+
role: req.params.role,
|
|
183
|
+
entries: daemon.memory.getHandoffChain(req.params.role),
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
app.get('/api/memory/handoff-chain/:role/recent', (req, res) => {
|
|
188
|
+
const count = Math.min(parseInt(req.query.count) || 3, 10);
|
|
189
|
+
res.json({
|
|
190
|
+
role: req.params.role,
|
|
191
|
+
markdown: daemon.memory.getRecentHandoffMarkdown(req.params.role, count, 10_000),
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
app.get('/api/memory/handoff-chain', (req, res) => {
|
|
196
|
+
res.json({ roles: daemon.memory.listHandoffRoles() });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Discoveries (error → fix pairs)
|
|
200
|
+
app.get('/api/memory/discoveries', (req, res) => {
|
|
201
|
+
const role = req.query.role;
|
|
202
|
+
const limit = Math.min(parseInt(req.query.limit) || 100, 500);
|
|
203
|
+
res.json({ discoveries: daemon.memory.listDiscoveries({ role, limit }) });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
app.post('/api/memory/discoveries', (req, res) => {
|
|
207
|
+
const { agentId, role, trigger, fix, outcome } = req.body || {};
|
|
208
|
+
const result = daemon.memory.addDiscovery({ agentId, role, trigger, fix, outcome });
|
|
209
|
+
if (!result.added && result.error) {
|
|
210
|
+
return res.status(400).json(result);
|
|
211
|
+
}
|
|
212
|
+
if (result.added) {
|
|
213
|
+
daemon.audit.log('memory.discovery.added', { agentId, role });
|
|
214
|
+
daemon.broadcast({ type: 'memory:discovery:added', agentId, role });
|
|
215
|
+
}
|
|
216
|
+
res.json(result);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Specializations (per-agent + per-role quality profiles)
|
|
220
|
+
app.get('/api/memory/specializations', (req, res) => {
|
|
221
|
+
res.json(daemon.memory.getAllSpecializations());
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
app.get('/api/memory/specializations/:agentId', (req, res) => {
|
|
225
|
+
const spec = daemon.memory.getSpecialization(req.params.agentId);
|
|
226
|
+
if (!spec) return res.status(404).json({ error: 'No specialization data for this agent' });
|
|
227
|
+
res.json(spec);
|
|
228
|
+
});
|
|
229
|
+
|
|
120
230
|
// Token usage
|
|
121
231
|
app.get('/api/tokens', (req, res) => {
|
|
122
232
|
res.json(daemon.tokens.getAll());
|
|
@@ -375,6 +485,14 @@ export function createApi(app, daemon) {
|
|
|
375
485
|
res.json(rec);
|
|
376
486
|
});
|
|
377
487
|
|
|
488
|
+
// Downshift suggestion — NEVER auto-applied. User must accept via UI.
|
|
489
|
+
// Returns null (204) when classifier has no strong suggestion.
|
|
490
|
+
app.get('/api/agents/:id/routing/suggestion', (req, res) => {
|
|
491
|
+
const suggestion = daemon.router.getSuggestion(req.params.id);
|
|
492
|
+
if (!suggestion) return res.status(204).send();
|
|
493
|
+
res.json(suggestion);
|
|
494
|
+
});
|
|
495
|
+
|
|
378
496
|
// Daemon status
|
|
379
497
|
app.get('/api/status', (req, res) => {
|
|
380
498
|
res.json({
|
|
@@ -459,6 +577,68 @@ export function createApi(app, daemon) {
|
|
|
459
577
|
res.json(daemon.tokens.getSummary());
|
|
460
578
|
});
|
|
461
579
|
|
|
580
|
+
// Per-team token burn ranked by total. Answers "which team burned the most?"
|
|
581
|
+
app.get('/api/tokens/by-team', (req, res) => {
|
|
582
|
+
const agents = daemon.registry.getAll();
|
|
583
|
+
const usage = daemon.tokens.getAll();
|
|
584
|
+
const teams = daemon.teams.list();
|
|
585
|
+
const unassignedId = '__unassigned__';
|
|
586
|
+
|
|
587
|
+
const perTeam = new Map();
|
|
588
|
+
for (const t of teams) {
|
|
589
|
+
perTeam.set(t.id, {
|
|
590
|
+
teamId: t.id,
|
|
591
|
+
teamName: t.name,
|
|
592
|
+
isDefault: !!t.isDefault,
|
|
593
|
+
agentCount: 0,
|
|
594
|
+
totalTokens: 0,
|
|
595
|
+
totalCostUsd: 0,
|
|
596
|
+
avgTokensPerAgent: 0,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
perTeam.set(unassignedId, {
|
|
600
|
+
teamId: unassignedId,
|
|
601
|
+
teamName: '(unassigned)',
|
|
602
|
+
isDefault: false,
|
|
603
|
+
agentCount: 0,
|
|
604
|
+
totalTokens: 0,
|
|
605
|
+
totalCostUsd: 0,
|
|
606
|
+
avgTokensPerAgent: 0,
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
for (const agent of agents) {
|
|
610
|
+
const bucket = perTeam.get(agent.teamId) || perTeam.get(unassignedId);
|
|
611
|
+
const u = usage[agent.id];
|
|
612
|
+
if (!u) continue;
|
|
613
|
+
bucket.agentCount += 1;
|
|
614
|
+
bucket.totalTokens += u.total || 0;
|
|
615
|
+
bucket.totalCostUsd += u.totalCostUsd || 0;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const result = [...perTeam.values()]
|
|
619
|
+
.map((t) => ({
|
|
620
|
+
...t,
|
|
621
|
+
avgTokensPerAgent: t.agentCount > 0 ? Math.round(t.totalTokens / t.agentCount) : 0,
|
|
622
|
+
}))
|
|
623
|
+
.filter((t) => t.agentCount > 0 || t.isDefault)
|
|
624
|
+
.sort((a, b) => b.totalTokens - a.totalTokens);
|
|
625
|
+
|
|
626
|
+
res.json({ teams: result });
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// Stop an agent's current work without killing the agent
|
|
630
|
+
app.post('/api/agents/:id/stop', async (req, res) => {
|
|
631
|
+
try {
|
|
632
|
+
const agent = daemon.registry.get(req.params.id);
|
|
633
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
634
|
+
await daemon.processes.stop(req.params.id);
|
|
635
|
+
daemon.audit.log('agent.stop', { id: req.params.id, name: agent.name });
|
|
636
|
+
res.json({ id: req.params.id, status: 'stopped' });
|
|
637
|
+
} catch (err) {
|
|
638
|
+
res.status(500).json({ error: err.message });
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
462
642
|
// Rotate an agent
|
|
463
643
|
app.post('/api/agents/:id/rotate', async (req, res) => {
|
|
464
644
|
try {
|
|
@@ -542,7 +722,7 @@ export function createApi(app, daemon) {
|
|
|
542
722
|
'\nAnswer concisely based on the agent context above.',
|
|
543
723
|
].filter(Boolean).join('\n');
|
|
544
724
|
|
|
545
|
-
const response = await daemon.journalist.callHeadless(prompt);
|
|
725
|
+
const response = await daemon.journalist.callHeadless(prompt, { trackAs: '__agent_qa__' });
|
|
546
726
|
res.json({ response, agentId: agent.id, agentName: agent.name });
|
|
547
727
|
} catch (err) {
|
|
548
728
|
res.status(400).json({ error: err.message });
|
|
@@ -721,7 +901,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
721
901
|
|
|
722
902
|
// Slow path: CLI fallback for subscription auth (~10s)
|
|
723
903
|
const fullPrompt = `${PLAN_SYSTEM}\n\n${prompt}`;
|
|
724
|
-
const response = await daemon.journalist.callHeadless(fullPrompt);
|
|
904
|
+
const response = await daemon.journalist.callHeadless(fullPrompt, { trackAs: '__planner__' });
|
|
725
905
|
res.json({ response, mode: 'cli' });
|
|
726
906
|
} catch (err) {
|
|
727
907
|
res.status(500).json({ error: err.message });
|
|
@@ -1025,6 +1205,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1025
1205
|
}
|
|
1026
1206
|
});
|
|
1027
1207
|
|
|
1208
|
+
app.post('/api/integrations/google-workspace/oauth/start', (req, res) => {
|
|
1209
|
+
try {
|
|
1210
|
+
const { integrationIds } = req.body || {};
|
|
1211
|
+
if (!integrationIds?.length) return res.status(400).json({ error: 'integrationIds required' });
|
|
1212
|
+
const url = daemon.integrations.getGoogleWorkspaceOAuthUrl(integrationIds);
|
|
1213
|
+
res.json({ url });
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
res.status(400).json({ error: err.message });
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1028
1219
|
// Parameterized :id routes (after specific routes above)
|
|
1029
1220
|
|
|
1030
1221
|
app.post('/api/integrations/:id/authenticate', (req, res) => {
|
|
@@ -1092,6 +1283,134 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1092
1283
|
}
|
|
1093
1284
|
});
|
|
1094
1285
|
|
|
1286
|
+
// --- Integration Execution (provider-agnostic) ---
|
|
1287
|
+
|
|
1288
|
+
const _execRates = new Map();
|
|
1289
|
+
const EXEC_RATE_LIMIT = 30;
|
|
1290
|
+
const EXEC_RATE_WINDOW = 60_000;
|
|
1291
|
+
|
|
1292
|
+
app.post('/api/integrations/:id/exec', async (req, res) => {
|
|
1293
|
+
try {
|
|
1294
|
+
const { tool, params, approvalId, agent: agentId } = req.body || {};
|
|
1295
|
+
if (!tool || typeof tool !== 'string') {
|
|
1296
|
+
return res.status(400).json({ error: 'tool (string) is required' });
|
|
1297
|
+
}
|
|
1298
|
+
if (params !== undefined && (typeof params !== 'object' || Array.isArray(params))) {
|
|
1299
|
+
return res.status(400).json({ error: 'params must be an object' });
|
|
1300
|
+
}
|
|
1301
|
+
const integrationId = req.params.id;
|
|
1302
|
+
if (!daemon.integrations._isInstalled(integrationId)) {
|
|
1303
|
+
return res.status(400).json({ error: 'Integration not installed' });
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Rate limiting — sliding window per integration
|
|
1307
|
+
const now = Date.now();
|
|
1308
|
+
let window = _execRates.get(integrationId) || [];
|
|
1309
|
+
window = window.filter((t) => now - t < EXEC_RATE_WINDOW);
|
|
1310
|
+
if (window.length >= EXEC_RATE_LIMIT) {
|
|
1311
|
+
daemon.audit.log('integration.exec.rate_limited', { integrationId, tool, agentId });
|
|
1312
|
+
return res.status(429).json({ error: `Rate limit exceeded (${EXEC_RATE_LIMIT}/min) for ${integrationId}` });
|
|
1313
|
+
}
|
|
1314
|
+
window.push(now);
|
|
1315
|
+
_execRates.set(integrationId, window);
|
|
1316
|
+
|
|
1317
|
+
// Approval gate — dangerous tools require human approval (unless agent is set to auto)
|
|
1318
|
+
const entry = daemon.integrations.registry.find((s) => s.id === integrationId);
|
|
1319
|
+
const callingAgent = agentId ? daemon.registry.get(agentId) : null;
|
|
1320
|
+
const autoApprove = callingAgent?.integrationApproval === 'auto';
|
|
1321
|
+
if (entry?.requiresApproval?.includes(tool) && !autoApprove) {
|
|
1322
|
+
if (approvalId) {
|
|
1323
|
+
const approval = daemon.supervisor.getApproval(approvalId);
|
|
1324
|
+
if (!approval) return res.status(404).json({ error: 'Approval not found' });
|
|
1325
|
+
if (approval.status === 'rejected') {
|
|
1326
|
+
return res.status(403).json({ error: 'Approval rejected', reason: approval.reason });
|
|
1327
|
+
}
|
|
1328
|
+
if (approval.status !== 'approved') {
|
|
1329
|
+
return res.status(202).json({ requiresApproval: true, approvalId, status: 'pending', message: 'Waiting for human approval' });
|
|
1330
|
+
}
|
|
1331
|
+
} else {
|
|
1332
|
+
const paramsSummary = params ? JSON.stringify(params).slice(0, 500) : '{}';
|
|
1333
|
+
const approval = daemon.supervisor.requestApproval(agentId || null, {
|
|
1334
|
+
type: 'integration_exec',
|
|
1335
|
+
integrationId,
|
|
1336
|
+
tool,
|
|
1337
|
+
params: paramsSummary,
|
|
1338
|
+
description: `${entry.name}: ${tool}`,
|
|
1339
|
+
});
|
|
1340
|
+
daemon.audit.log('integration.exec.blocked', { integrationId, tool, approvalId: approval.id, agentId });
|
|
1341
|
+
return res.status(202).json({
|
|
1342
|
+
requiresApproval: true,
|
|
1343
|
+
approvalId: approval.id,
|
|
1344
|
+
message: `Tool "${tool}" requires approval. Retry with this approvalId once approved.`,
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
const result = await daemon.mcpManager.execTool(integrationId, tool, params || {});
|
|
1350
|
+
daemon.audit.log('integration.exec', { integrationId, tool, params: params ? JSON.stringify(params).slice(0, 200) : '{}', agentId });
|
|
1351
|
+
res.json({ result });
|
|
1352
|
+
} catch (err) {
|
|
1353
|
+
res.status(400).json({ error: err.message });
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
app.get('/api/integrations/:id/tools', async (req, res) => {
|
|
1358
|
+
try {
|
|
1359
|
+
if (!daemon.integrations._isInstalled(req.params.id)) {
|
|
1360
|
+
return res.status(400).json({ error: 'Integration not installed' });
|
|
1361
|
+
}
|
|
1362
|
+
const tools = await daemon.mcpManager.listTools(req.params.id);
|
|
1363
|
+
res.json({ tools });
|
|
1364
|
+
} catch (err) {
|
|
1365
|
+
res.status(400).json({ error: err.message });
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
// --- Google Drive Upload (file → native Google Workspace format) ---
|
|
1370
|
+
|
|
1371
|
+
app.post('/api/integrations/google-drive/upload', async (req, res) => {
|
|
1372
|
+
try {
|
|
1373
|
+
const { filePath, name, folderId, convert, approvalId, agent: agentId } = req.body || {};
|
|
1374
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
1375
|
+
return res.status(400).json({ error: 'filePath (string) is required' });
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// Approval gate (unless agent is set to auto)
|
|
1379
|
+
const uploadAgent = agentId ? daemon.registry.get(agentId) : null;
|
|
1380
|
+
const autoApproveUpload = uploadAgent?.integrationApproval === 'auto';
|
|
1381
|
+
if (!autoApproveUpload) {
|
|
1382
|
+
if (approvalId) {
|
|
1383
|
+
const approval = daemon.supervisor.getApproval(approvalId);
|
|
1384
|
+
if (!approval) return res.status(404).json({ error: 'Approval not found' });
|
|
1385
|
+
if (approval.status === 'rejected') return res.status(403).json({ error: 'Approval rejected', reason: approval.reason });
|
|
1386
|
+
if (approval.status !== 'approved') return res.status(202).json({ requiresApproval: true, approvalId, status: 'pending' });
|
|
1387
|
+
} else {
|
|
1388
|
+
const approval = daemon.supervisor.requestApproval(agentId || null, {
|
|
1389
|
+
type: 'google_drive_upload',
|
|
1390
|
+
filePath,
|
|
1391
|
+
name: name || filePath.split('/').pop(),
|
|
1392
|
+
description: `Upload to Google Drive: ${name || filePath.split('/').pop()}`,
|
|
1393
|
+
});
|
|
1394
|
+
daemon.audit.log('integration.upload.blocked', { filePath, approvalId: approval.id, agentId });
|
|
1395
|
+
return res.status(202).json({
|
|
1396
|
+
requiresApproval: true,
|
|
1397
|
+
approvalId: approval.id,
|
|
1398
|
+
message: `Upload requires approval. Retry with this approvalId once approved.`,
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
const result = await daemon.integrations.uploadToGoogleDrive(filePath, {
|
|
1404
|
+
name, folderId, convert: convert !== false,
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
daemon.audit.log('integration.upload', { filePath, driveFileId: result.id, name: result.name, agentId });
|
|
1408
|
+
res.json(result);
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
res.status(400).json({ error: err.message });
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1095
1414
|
// --- Agent Integrations (attach/detach) ---
|
|
1096
1415
|
|
|
1097
1416
|
app.post('/api/agents/:agentId/integrations/:integrationId', (req, res) => {
|
|
@@ -1939,8 +2258,9 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1939
2258
|
// Recalculate savings estimates
|
|
1940
2259
|
const estimated = registryTokens + tokenSummary.savings.total;
|
|
1941
2260
|
tokenSummary.savings.estimatedWithoutGroove = estimated;
|
|
1942
|
-
tokenSummary.savings.
|
|
1943
|
-
|
|
2261
|
+
const rawPct = estimated > 0 ? (tokenSummary.savings.total / estimated) * 100 : 0;
|
|
2262
|
+
tokenSummary.savings.percentage = rawPct > 0 && rawPct < 1
|
|
2263
|
+
? Math.round(rawPct * 10) / 10 : Math.round(rawPct);
|
|
1944
2264
|
}
|
|
1945
2265
|
const rotationStats = daemon.rotator.getStats();
|
|
1946
2266
|
const rotationHistory = daemon.rotator.getHistory();
|
|
@@ -1964,27 +2284,31 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1964
2284
|
// Per-agent enriched data with quality signals
|
|
1965
2285
|
const agentBreakdown = agents.map((a) => {
|
|
1966
2286
|
const tokenData = daemon.tokens.getAgent(a.id);
|
|
1967
|
-
|
|
2287
|
+
// Cache rate denominator: reads + creation (cacheable), excludes fresh inputTokens
|
|
2288
|
+
const agentCacheable = (tokenData.cacheReadTokens || 0) + (tokenData.cacheCreationTokens || 0);
|
|
1968
2289
|
|
|
1969
|
-
// Quality signals from classifier + adaptive
|
|
1970
2290
|
let quality = null;
|
|
1971
2291
|
try {
|
|
1972
|
-
const
|
|
2292
|
+
const events = daemon.classifier.agentWindows[a.id] || [];
|
|
2293
|
+
const signals = events.length >= 6 ? daemon.adaptive.extractSignals(events, a.scope) : null;
|
|
2294
|
+
const score = signals ? daemon.adaptive.scoreSession(signals) : null;
|
|
1973
2295
|
const classification = daemon.classifier.classify(a.id);
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2296
|
+
const history = daemon.rotator.scoreHistory[a.id] || [];
|
|
2297
|
+
quality = {
|
|
2298
|
+
score,
|
|
2299
|
+
scoreHistory: history,
|
|
2300
|
+
errorCount: signals?.errorCount || 0,
|
|
2301
|
+
toolCalls: signals?.toolCalls || 0,
|
|
2302
|
+
toolFailures: signals?.toolFailures || 0,
|
|
2303
|
+
toolSuccessRate: signals?.toolCalls > 0 ? 1 - (signals.toolFailures / signals.toolCalls) : 1,
|
|
2304
|
+
filesWritten: signals?.filesWritten || 0,
|
|
2305
|
+
fileChurn: signals?.fileChurn || 0,
|
|
2306
|
+
repetitions: signals?.repetitions || 0,
|
|
2307
|
+
errorTrend: signals?.errorTrend || 0,
|
|
2308
|
+
tier: classification?.tier || classification || 'medium',
|
|
2309
|
+
eventCount: events.length,
|
|
2310
|
+
};
|
|
2311
|
+
} catch { /* classifier/adaptive may not have data yet */ }
|
|
1988
2312
|
|
|
1989
2313
|
return {
|
|
1990
2314
|
id: a.id,
|
|
@@ -2000,7 +2324,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2000
2324
|
costSource: a.provider === 'claude-code' ? 'actual' : a.provider === 'ollama' ? 'local' : 'estimated',
|
|
2001
2325
|
inputTokens: tokenData.inputTokens || 0,
|
|
2002
2326
|
outputTokens: tokenData.outputTokens || 0,
|
|
2003
|
-
cacheHitRate:
|
|
2327
|
+
cacheHitRate: agentCacheable > 0 ? Math.round(((tokenData.cacheReadTokens || 0) / agentCacheable) * 1000) / 1000 : 0,
|
|
2004
2328
|
contextUsage: a.contextUsage || 0,
|
|
2005
2329
|
rotationThreshold: daemon.adaptive.getThreshold(a.provider, a.role),
|
|
2006
2330
|
durationMs: a.durationMs || tokenData.totalDurationMs || 0,
|
|
@@ -35,17 +35,64 @@ const LIGHT_SIGNALS = [
|
|
|
35
35
|
];
|
|
36
36
|
|
|
37
37
|
export class TaskClassifier {
|
|
38
|
-
constructor() {
|
|
39
|
-
this.windowSize =
|
|
38
|
+
constructor(daemon = null) {
|
|
39
|
+
this.windowSize = 200; // Large enough for quality signal extraction across tool calls
|
|
40
40
|
this.agentWindows = {}; // for degradation detection and adaptive scoring
|
|
41
|
+
this.daemon = daemon; // optional — enables broadcast of classification updates
|
|
42
|
+
this._lastBroadcastCount = {}; // per-agent throttle
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
// Add an event to the classification window
|
|
44
45
|
addEvent(agentId, event) {
|
|
45
46
|
if (!this.agentWindows[agentId]) this.agentWindows[agentId] = [];
|
|
46
|
-
this.agentWindows[agentId]
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const window = this.agentWindows[agentId];
|
|
48
|
+
|
|
49
|
+
// Extract structured tool/error events from Claude Code content blocks.
|
|
50
|
+
// Claude's stream-json emits assistant messages with content arrays containing
|
|
51
|
+
// tool_use and tool_result blocks that drive quality signal extraction.
|
|
52
|
+
let extracted = false;
|
|
53
|
+
if (event.type === 'activity' && Array.isArray(event.data)) {
|
|
54
|
+
for (const block of event.data) {
|
|
55
|
+
if (block.type === 'tool_use') {
|
|
56
|
+
window.push({
|
|
57
|
+
type: 'tool',
|
|
58
|
+
tool: block.name,
|
|
59
|
+
input: block.input?.file_path || block.input?.path || block.input?.command || '',
|
|
60
|
+
isError: false,
|
|
61
|
+
timestamp: Date.now(),
|
|
62
|
+
});
|
|
63
|
+
extracted = true;
|
|
64
|
+
}
|
|
65
|
+
if (block.type === 'tool_result') {
|
|
66
|
+
if (block.is_error) {
|
|
67
|
+
window.push({ type: 'error', timestamp: Date.now() });
|
|
68
|
+
extracted = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Only push the raw event if we didn't extract structured events from it —
|
|
75
|
+
// avoids double-counting and window bloat from activity wrappers.
|
|
76
|
+
if (!extracted) {
|
|
77
|
+
window.push({ ...event, timestamp: event.timestamp || Date.now() });
|
|
78
|
+
}
|
|
79
|
+
while (window.length > this.windowSize) window.shift();
|
|
80
|
+
|
|
81
|
+
// Broadcast classification updates periodically. Enables GUI to surface
|
|
82
|
+
// downshift suggestions ("agent's been doing light work — switch to Haiku?").
|
|
83
|
+
// Requires 40+ events before any broadcast; throttles to every 20 events.
|
|
84
|
+
if (this.daemon?.broadcast && window.length >= 40) {
|
|
85
|
+
const lastBroadcast = this._lastBroadcastCount[agentId] || 0;
|
|
86
|
+
if (window.length - lastBroadcast >= 20) {
|
|
87
|
+
this._lastBroadcastCount[agentId] = window.length;
|
|
88
|
+
const tier = this.classify(agentId);
|
|
89
|
+
this.daemon.broadcast({
|
|
90
|
+
type: 'classifier:update',
|
|
91
|
+
agentId,
|
|
92
|
+
tier,
|
|
93
|
+
eventCount: window.length,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
49
96
|
}
|
|
50
97
|
}
|
|
51
98
|
|
|
@@ -15,6 +15,16 @@ const DEFAULT_CONFIG = {
|
|
|
15
15
|
qcThreshold: 4,
|
|
16
16
|
maxAgents: 10,
|
|
17
17
|
defaultProvider: 'claude-code',
|
|
18
|
+
// Self-healing rotation triggers. Catch pathological agent behavior
|
|
19
|
+
// (stuck loops, runaway tool-call cycles) and auto-rotate with fresh
|
|
20
|
+
// context. Tokens carry forward; journalist generates handoff brief.
|
|
21
|
+
// Set autoRotate=false to disable and get broadcast-only notifications.
|
|
22
|
+
safety: {
|
|
23
|
+
autoRotate: true,
|
|
24
|
+
tokenCeilingPerAgent: 5_000_000,
|
|
25
|
+
velocityWindowSeconds: 300,
|
|
26
|
+
velocityTokenThreshold: 1_500_000,
|
|
27
|
+
},
|
|
18
28
|
};
|
|
19
29
|
|
|
20
30
|
export function isFirstRun(grooveDir) {
|
|
@@ -123,7 +133,10 @@ export function loadConfig(grooveDir) {
|
|
|
123
133
|
|
|
124
134
|
try {
|
|
125
135
|
const saved = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
126
|
-
|
|
136
|
+
const merged = { ...DEFAULT_CONFIG, ...saved };
|
|
137
|
+
// Deep-merge safety subtree so partial user config doesn't drop defaults
|
|
138
|
+
merged.safety = { ...DEFAULT_CONFIG.safety, ...(saved.safety || {}) };
|
|
139
|
+
return merged;
|
|
127
140
|
} catch {
|
|
128
141
|
return { ...DEFAULT_CONFIG };
|
|
129
142
|
}
|
|
@@ -638,7 +638,7 @@ export class GatewayManager {
|
|
|
638
638
|
'\nSynthesize a concise answer based on the team\'s collective context.',
|
|
639
639
|
].join('\n');
|
|
640
640
|
|
|
641
|
-
const response = await this.daemon.journalist.callHeadless(prompt);
|
|
641
|
+
const response = await this.daemon.journalist.callHeadless(prompt, { trackAs: '__gateway__' });
|
|
642
642
|
return { text: `\ud83d\udcac Team ${result.team.name}:\n${truncate(response, 3000)}` };
|
|
643
643
|
}
|
|
644
644
|
|
|
@@ -659,7 +659,7 @@ export class GatewayManager {
|
|
|
659
659
|
'\nAnswer concisely based on the agent context above.',
|
|
660
660
|
].filter(Boolean).join('\n');
|
|
661
661
|
|
|
662
|
-
const response = await this.daemon.journalist.callHeadless(prompt);
|
|
662
|
+
const response = await this.daemon.journalist.callHeadless(prompt, { trackAs: '__gateway__' });
|
|
663
663
|
return { text: `\ud83d\udcac ${agent.name || agent.id}:\n${truncate(response, 3000)}` };
|
|
664
664
|
}
|
|
665
665
|
|