groove-dev 0.27.134 → 0.27.136

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.
Files changed (85) hide show
  1. package/moe-training/client/domain-tagger.js +1 -1
  2. package/moe-training/scripts/retag-delegate-yield.js +303 -0
  3. package/moe-training/test/shared/envelope-schema.test.js +3 -3
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/package.json +1 -1
  6. package/node_modules/@groove-dev/daemon/src/adaptive.js +77 -0
  7. package/node_modules/@groove-dev/daemon/src/api.js +35 -5
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +28 -12
  9. package/node_modules/@groove-dev/daemon/src/model-lab.js +53 -76
  10. package/node_modules/@groove-dev/daemon/src/process.js +91 -2
  11. package/node_modules/@groove-dev/daemon/src/rotator.js +45 -3
  12. package/node_modules/@groove-dev/gui/dist/assets/{index-Dozp69tK.js → index-BrZHF7pK.js} +1770 -1766
  13. package/node_modules/@groove-dev/gui/dist/assets/index-DIfiwdKl.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  15. package/node_modules/@groove-dev/gui/package.json +1 -1
  16. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +60 -18
  17. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +42 -20
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +1 -1
  19. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +1 -1
  20. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +2 -22
  21. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +9 -9
  22. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +1 -1
  23. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +7 -0
  24. package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +59 -51
  25. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +48 -48
  26. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +39 -38
  27. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +4 -5
  28. package/node_modules/@groove-dev/gui/src/components/lab/preset-manager.jsx +11 -11
  29. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +66 -62
  30. package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +13 -13
  31. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  32. package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +62 -22
  33. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +16 -17
  34. package/node_modules/@groove-dev/gui/src/components/ui/table-tree.jsx +38 -0
  35. package/node_modules/@groove-dev/gui/src/stores/groove.js +23 -9
  36. package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -1
  37. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +101 -87
  38. package/node_modules/moe-training/client/domain-tagger.js +1 -1
  39. package/node_modules/moe-training/scripts/retag-delegate-yield.js +303 -0
  40. package/node_modules/moe-training/test/shared/envelope-schema.test.js +3 -3
  41. package/package.json +1 -1
  42. package/packages/cli/package.json +1 -1
  43. package/packages/daemon/package.json +1 -1
  44. package/packages/daemon/src/adaptive.js +77 -0
  45. package/packages/daemon/src/api.js +35 -5
  46. package/packages/daemon/src/journalist.js +28 -12
  47. package/packages/daemon/src/model-lab.js +53 -76
  48. package/packages/daemon/src/process.js +91 -2
  49. package/packages/daemon/src/rotator.js +45 -3
  50. package/packages/gui/dist/assets/{index-Dozp69tK.js → index-BrZHF7pK.js} +1770 -1766
  51. package/packages/gui/dist/assets/index-DIfiwdKl.css +1 -0
  52. package/packages/gui/dist/index.html +2 -2
  53. package/packages/gui/package.json +1 -1
  54. package/packages/gui/src/components/agents/agent-chat.jsx +60 -18
  55. package/packages/gui/src/components/agents/agent-feed.jsx +42 -20
  56. package/packages/gui/src/components/agents/agent-file-tree.jsx +1 -1
  57. package/packages/gui/src/components/agents/workspace-mode.jsx +1 -1
  58. package/packages/gui/src/components/chat/chat-messages.jsx +2 -22
  59. package/packages/gui/src/components/editor/code-editor.jsx +9 -9
  60. package/packages/gui/src/components/editor/file-tree.jsx +1 -1
  61. package/packages/gui/src/components/editor/terminal.jsx +7 -0
  62. package/packages/gui/src/components/lab/chat-playground.jsx +59 -51
  63. package/packages/gui/src/components/lab/lab-assistant.jsx +48 -48
  64. package/packages/gui/src/components/lab/metrics-panel.jsx +39 -38
  65. package/packages/gui/src/components/lab/parameter-panel.jsx +4 -5
  66. package/packages/gui/src/components/lab/preset-manager.jsx +11 -11
  67. package/packages/gui/src/components/lab/runtime-config.jsx +66 -62
  68. package/packages/gui/src/components/lab/system-prompt-editor.jsx +13 -13
  69. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  70. package/packages/gui/src/components/preview/preview-workspace.jsx +62 -22
  71. package/packages/gui/src/components/ui/slider.jsx +16 -17
  72. package/packages/gui/src/components/ui/table-tree.jsx +38 -0
  73. package/packages/gui/src/stores/groove.js +23 -9
  74. package/packages/gui/src/views/editor.jsx +1 -1
  75. package/packages/gui/src/views/model-lab.jsx +101 -87
  76. package/plan_files/DELEGATE_YIELD_TRAINING_TAGS.md +135 -0
  77. package/plan_files/session-quality-rotation-fixes.md +218 -0
  78. package/test.py +571 -0
  79. package/node_modules/@groove-dev/gui/dist/assets/index-BgQL4bNl.css +0 -1
  80. package/packages/gui/dist/assets/index-BgQL4bNl.css +0 -1
  81. /package/{AGENT_ORCHESTRATION.md → plan_files/AGENT_ORCHESTRATION.md} +0 -0
  82. /package/{DYNAMIC_LEAF_ARCH.md → plan_files/DYNAMIC_LEAF_ARCH.md} +0 -0
  83. /package/{EMBEDDING_DIAGNOSTIC.md → plan_files/EMBEDDING_DIAGNOSTIC.md} +0 -0
  84. /package/{EMBEDDING_SERVICE_BUILD_PLAN.md → plan_files/EMBEDDING_SERVICE_BUILD_PLAN.md} +0 -0
  85. /package/{MOE_TRAINING_PIPELINE.md → plan_files/MOE_TRAINING_PIPELINE.md} +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { EMBEDDING_SERVICE_URL } from '../shared/constants.js';
4
4
 
5
- const DEFAULT_MODEL = 'BAAI/bge-small-en-v1.5';
5
+ const DEFAULT_MODEL = 'BAAI/bge-base-en-v1.5';
6
6
  const DEFAULT_TOP_K = 3;
7
7
 
8
8
  // ~40 domains covering broad technical territory.
@@ -0,0 +1,303 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ //
3
+ // Retroactive tagger: scans planner/fullstack session envelopes and re-tags
4
+ // delegation dispatches as "delegate" steps and artifact handoffs as "yield" steps.
5
+ //
6
+ // Usage:
7
+ // node retag-delegate-yield.js <input.jsonl> [output.jsonl]
8
+ //
9
+ // If output is omitted, writes to stdout. Input can also be piped via stdin.
10
+ // Only modifies planner/fullstack trajectory envelopes — SESSION_CLOSE and
11
+ // USER_FEEDBACK envelopes pass through unchanged.
12
+
13
+ import { createReadStream, createWriteStream, existsSync } from 'fs';
14
+ import { createInterface } from 'readline';
15
+
16
+ // --- Pattern A: Delegation detection ---
17
+ // A thought step reasoning about needing a specialist, followed by an action
18
+ // that dispatches to another agent. The dispatch action+observation get replaced
19
+ // with a single delegate step.
20
+
21
+ const DELEGATION_THOUGHT_RE = /\b(specialist|dispatch|delegate|hand off|route to|needs? a .*(backend|frontend|fullstack|database|devops|planner)|re-route|different agent|another agent|pass this to)\b/i;
22
+
23
+ const DELEGATION_ACTION_RE = /\b(dispatch|spawn|agent|delegate|hand off|route|assign)\b/i;
24
+
25
+ const AGENT_ID_RE = /\b(backend|frontend|fullstack|planner|devops|database|chat|advisor|qc)[-_]?\d+\b/gi;
26
+
27
+ function isDelegationThought(step) {
28
+ if (step.type !== 'thought') return false;
29
+ return DELEGATION_THOUGHT_RE.test(step.content || '');
30
+ }
31
+
32
+ function isDelegationAction(step) {
33
+ if (step.type !== 'action') return false;
34
+ const tool = (step.tool || '').toLowerCase();
35
+ const content = (step.content || '').toLowerCase();
36
+ if (tool === 'agent' || tool === 'dispatch') return true;
37
+ if (DELEGATION_ACTION_RE.test(content)) return true;
38
+ return false;
39
+ }
40
+
41
+ function extractDelegateTask(actionStep, observationStep) {
42
+ let task = '';
43
+ const content = actionStep.content || '';
44
+ const args = actionStep.arguments || {};
45
+
46
+ // Try to extract the task from arguments (e.g. Agent tool input)
47
+ if (args.prompt) {
48
+ task = args.prompt;
49
+ } else if (args.message) {
50
+ task = args.message;
51
+ } else if (args.task) {
52
+ task = args.task;
53
+ }
54
+
55
+ // Fall back to parsing the content after "Dispatch to X:" or similar
56
+ if (!task) {
57
+ const match = content.match(/(?:dispatch(?:ed)? to \S+:\s*|:\s*)(.*)/i);
58
+ task = match ? match[1] : content;
59
+ }
60
+
61
+ // Strip agent ID references — the router picks the target, not the delegator
62
+ task = task.replace(AGENT_ID_RE, '').replace(/\s{2,}/g, ' ').trim();
63
+
64
+ return task;
65
+ }
66
+
67
+ // --- Pattern B: Yield detection ---
68
+ // An action(Write/Edit) producing a file, followed by observation(success),
69
+ // followed by a resolution whose content suggests artifact handoff to another agent.
70
+
71
+ const WRITE_TOOLS = new Set(['write', 'edit', 'create', 'save']);
72
+
73
+ const YIELD_RESOLUTION_RE = /\b(next agent|can now|build on this|picks? up|hand(?:ed|s|ing)? off|artifact|ready for|phase complete|my part is done|produced|output for)\b/i;
74
+
75
+ function isWriteAction(step) {
76
+ if (step.type !== 'action') return false;
77
+ const tool = (step.tool || '').toLowerCase();
78
+ return WRITE_TOOLS.has(tool);
79
+ }
80
+
81
+ function isSuccessObservation(step) {
82
+ if (step.type !== 'observation') return false;
83
+ const c = (step.content || '').toLowerCase();
84
+ if (step.is_error) return false;
85
+ return !c.includes('error') || c.includes('0 error');
86
+ }
87
+
88
+ function isYieldResolution(step) {
89
+ if (step.type !== 'resolution') return false;
90
+ return YIELD_RESOLUTION_RE.test(step.content || '');
91
+ }
92
+
93
+ function extractFilePath(writeStep) {
94
+ const args = writeStep.arguments || {};
95
+ if (args.file_path) return args.file_path;
96
+ if (args.path) return args.path;
97
+
98
+ // Try to parse path from content
99
+ const content = writeStep.content || '';
100
+ const match = content.match(/(?:Writing|Wrote|Created?|Saving?|Edit(?:ing|ed)?)\s+(\S+\.\w+)/i);
101
+ return match ? match[1] : null;
102
+ }
103
+
104
+ function buildYieldSummary(resolutionStep, maxTokens = 20) {
105
+ const content = (resolutionStep.content || '').trim();
106
+ // Take first sentence, cap at ~80 chars for ~20 tokens
107
+ const firstSentence = content.split(/[.!?\n]/)[0].trim();
108
+ return firstSentence.slice(0, 80);
109
+ }
110
+
111
+ // --- Main retagging logic ---
112
+
113
+ function retagTrajectory(steps) {
114
+ if (!Array.isArray(steps) || steps.length < 2) return { steps, delegateCount: 0, yieldCount: 0 };
115
+
116
+ const result = [];
117
+ let delegateCount = 0;
118
+ let yieldCount = 0;
119
+ let i = 0;
120
+
121
+ while (i < steps.length) {
122
+ // Pattern A: thought(delegation) → action(dispatch) → observation → ...
123
+ // Replace action+observation with a single delegate step
124
+ if (i + 2 < steps.length &&
125
+ isDelegationThought(steps[i]) &&
126
+ isDelegationAction(steps[i + 1])) {
127
+
128
+ // Keep the thought
129
+ result.push({ ...steps[i] });
130
+
131
+ const actionStep = steps[i + 1];
132
+ const nextStep = steps[i + 2];
133
+ const obsStep = nextStep.type === 'observation' ? nextStep : null;
134
+
135
+ const task = extractDelegateTask(actionStep, obsStep);
136
+
137
+ if (task) {
138
+ result.push({
139
+ step: actionStep.step,
140
+ type: 'delegate',
141
+ content: task,
142
+ timestamp: actionStep.timestamp,
143
+ token_count: Math.max(1, Math.ceil(task.length / 4)),
144
+ });
145
+ delegateCount++;
146
+
147
+ // Skip the action and observation
148
+ i += obsStep ? 3 : 2;
149
+
150
+ // If the next step is a resolution that just confirms dispatch, skip it too
151
+ if (i < steps.length && steps[i].type === 'resolution') {
152
+ const rc = (steps[i].content || '').toLowerCase();
153
+ if (rc.includes('dispatch') || rc.includes('delegat') || rc.includes('handed off') || rc.length < 50) {
154
+ i++;
155
+ }
156
+ }
157
+ continue;
158
+ }
159
+ }
160
+
161
+ // Pattern B: action(Write/Edit) → observation(success) → resolution(handoff)
162
+ // Replace resolution with yield
163
+ if (i + 2 < steps.length &&
164
+ isWriteAction(steps[i]) &&
165
+ isSuccessObservation(steps[i + 1]) &&
166
+ isYieldResolution(steps[i + 2])) {
167
+
168
+ // Keep the action and observation as-is
169
+ result.push({ ...steps[i] });
170
+ result.push({ ...steps[i + 1] });
171
+
172
+ const writeStep = steps[i];
173
+ const resStep = steps[i + 2];
174
+ const path = extractFilePath(writeStep);
175
+ const summary = buildYieldSummary(resStep);
176
+
177
+ const yieldStep = {
178
+ step: resStep.step,
179
+ type: 'yield',
180
+ content: summary,
181
+ timestamp: resStep.timestamp,
182
+ token_count: Math.max(1, Math.ceil(summary.length / 4)),
183
+ };
184
+ if (path) yieldStep.path = path;
185
+
186
+ result.push(yieldStep);
187
+ yieldCount++;
188
+ i += 3;
189
+ continue;
190
+ }
191
+
192
+ // No pattern match — pass through unchanged
193
+ result.push({ ...steps[i] });
194
+ i++;
195
+ }
196
+
197
+ return { steps: result, delegateCount, yieldCount };
198
+ }
199
+
200
+ // --- Process envelopes ---
201
+
202
+ async function processStream(input, output) {
203
+ const rl = createInterface({ input, crlfDelay: Infinity });
204
+
205
+ let totalEnvelopes = 0;
206
+ let modifiedEnvelopes = 0;
207
+ let totalDelegates = 0;
208
+ let totalYields = 0;
209
+ let skippedRoles = 0;
210
+
211
+ for await (const line of rl) {
212
+ const trimmed = line.trim();
213
+ if (!trimmed) continue;
214
+
215
+ let envelope;
216
+ try {
217
+ envelope = JSON.parse(trimmed);
218
+ } catch {
219
+ output.write(trimmed + '\n');
220
+ continue;
221
+ }
222
+
223
+ totalEnvelopes++;
224
+
225
+ // Pass through non-trajectory envelopes unchanged
226
+ if (envelope.type === 'SESSION_CLOSE' || envelope.type === 'USER_FEEDBACK') {
227
+ output.write(JSON.stringify(envelope) + '\n');
228
+ continue;
229
+ }
230
+
231
+ // Only retag planner and fullstack sessions (where delegation/yield patterns occur)
232
+ const role = envelope.metadata?.agent_role;
233
+ if (!role || !['planner', 'fullstack', 'advisor'].includes(role)) {
234
+ skippedRoles++;
235
+ output.write(JSON.stringify(envelope) + '\n');
236
+ continue;
237
+ }
238
+
239
+ const steps = envelope.trajectory_log;
240
+ if (!Array.isArray(steps) || steps.length < 2) {
241
+ output.write(JSON.stringify(envelope) + '\n');
242
+ continue;
243
+ }
244
+
245
+ const { steps: retagged, delegateCount, yieldCount } = retagTrajectory(steps);
246
+
247
+ if (delegateCount > 0 || yieldCount > 0) {
248
+ modifiedEnvelopes++;
249
+ totalDelegates += delegateCount;
250
+ totalYields += yieldCount;
251
+ envelope.trajectory_log = retagged;
252
+ }
253
+
254
+ output.write(JSON.stringify(envelope) + '\n');
255
+ }
256
+
257
+ return { totalEnvelopes, modifiedEnvelopes, totalDelegates, totalYields, skippedRoles };
258
+ }
259
+
260
+ // --- Entry point ---
261
+
262
+ async function main() {
263
+ const args = process.argv.slice(2);
264
+ const inputPath = args[0];
265
+ const outputPath = args[1];
266
+
267
+ let input;
268
+ if (inputPath && inputPath !== '-') {
269
+ if (!existsSync(inputPath)) {
270
+ console.error(`Error: input file not found: ${inputPath}`);
271
+ process.exit(1);
272
+ }
273
+ input = createReadStream(inputPath, 'utf8');
274
+ } else {
275
+ input = process.stdin;
276
+ }
277
+
278
+ let output;
279
+ if (outputPath) {
280
+ output = createWriteStream(outputPath, 'utf8');
281
+ } else {
282
+ output = process.stdout;
283
+ }
284
+
285
+ const stats = await processStream(input, output);
286
+
287
+ if (output !== process.stdout) {
288
+ output.end();
289
+ }
290
+
291
+ // Print stats to stderr so they don't mix with JSONL output
292
+ console.error(`\n--- Retag Summary ---`);
293
+ console.error(`Envelopes processed: ${stats.totalEnvelopes}`);
294
+ console.error(`Envelopes modified: ${stats.modifiedEnvelopes}`);
295
+ console.error(`Delegate steps added: ${stats.totalDelegates}`);
296
+ console.error(`Yield steps added: ${stats.totalYields}`);
297
+ console.error(`Skipped (wrong role): ${stats.skippedRoles}`);
298
+ }
299
+
300
+ main().catch((err) => {
301
+ console.error(`Fatal: ${err.message}`);
302
+ process.exit(1);
303
+ });
@@ -475,7 +475,7 @@ describe('envelope-schema', () => {
475
475
  it('accepts valid session_embedding object', () => {
476
476
  const env = validEnvelope();
477
477
  env.metadata.session_embedding = {
478
- model: 'BAAI/bge-small-en-v1.5',
478
+ model: 'BAAI/bge-base-en-v1.5',
479
479
  vector: [0.0234, -0.0891, 0.1247, 0.0562],
480
480
  source_text: 'Write a Python decorator that caches function results',
481
481
  };
@@ -486,7 +486,7 @@ describe('envelope-schema', () => {
486
486
  it('rejects session_embedding with empty vector', () => {
487
487
  const env = validEnvelope();
488
488
  env.metadata.session_embedding = {
489
- model: 'BAAI/bge-small-en-v1.5',
489
+ model: 'BAAI/bge-base-en-v1.5',
490
490
  vector: [],
491
491
  source_text: 'test',
492
492
  };
@@ -498,7 +498,7 @@ describe('envelope-schema', () => {
498
498
  it('rejects session_embedding with non-numeric vector values', () => {
499
499
  const env = validEnvelope();
500
500
  env.metadata.session_embedding = {
501
- model: 'BAAI/bge-small-en-v1.5',
501
+ model: 'BAAI/bge-base-en-v1.5',
502
502
  vector: [0.1, 'bad', 0.3],
503
503
  source_text: 'test',
504
504
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.134",
3
+ "version": "0.27.136",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.134",
3
+ "version": "0.27.136",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -137,6 +137,20 @@ export class AdaptiveThresholds {
137
137
  const filesWritten = signals.filesWritten || 0;
138
138
  score += Math.min(filesWritten * 2, 10); // Cap at +10
139
139
 
140
+ // Output length decay: assistant responses shrinking dramatically
141
+ if (signals.outputLengthDecay) score -= 10;
142
+
143
+ // Tool output volume: bloated context from large tool results
144
+ const toolVol = signals.toolOutputVolume || 0;
145
+ if (toolVol === 2) score -= 10;
146
+ else if (toolVol === 1) score -= 5;
147
+
148
+ // Turn latency trend: agent slowing down significantly
149
+ if (signals.turnLatencyTrend) score -= 5;
150
+
151
+ // Bash repetition: agent stuck running identical commands
152
+ if (signals.bashRepetition) score -= 8;
153
+
140
154
  // Clamp to 0-100
141
155
  return Math.max(0, Math.min(100, score));
142
156
  }
@@ -165,20 +179,43 @@ export class AdaptiveThresholds {
165
179
  filesWritten: 0,
166
180
  fileChurn: 0, // same file written 3+ times → possible circular refactoring
167
181
  errorTrend: 0, // errors increasing in recent window → degradation signal
182
+ outputLengthDecay: 0, // last 5 assistant turns avg <50% of first 5 → declining output
183
+ toolOutputVolume: 0, // cumulative tool result chars (>300KB = bloated context)
184
+ turnLatencyTrend: 0, // avg gap in last 10 entries >2x first 10 → slowing down
185
+ bashRepetition: 0, // 3+ identical consecutive Bash commands → stuck in loop
168
186
  };
169
187
 
170
188
  const writtenFiles = new Set();
171
189
  const fileWriteCounts = {};
172
190
  const writeEditOps = [];
191
+ const assistantOutputLengths = [];
192
+ let toolOutputBytes = 0;
193
+ const entryTimestamps = [];
194
+ const bashCommands = [];
173
195
 
174
196
  for (const entry of entries) {
197
+ if (entry.timestamp) entryTimestamps.push(new Date(entry.timestamp).getTime());
198
+
175
199
  if (entry.type === 'error') {
176
200
  signals.errorCount++;
177
201
  }
178
202
 
203
+ // Track assistant output lengths for decay detection
204
+ if (entry.type === 'thinking' && entry.text) {
205
+ assistantOutputLengths.push(entry.text.length);
206
+ }
207
+
179
208
  if (entry.type === 'tool') {
180
209
  signals.toolCalls++;
181
210
 
211
+ // Track tool result output volume
212
+ if (entry.output) toolOutputBytes += entry.output.length;
213
+
214
+ // Track Bash commands for repetition detection
215
+ if (entry.tool === 'Bash' && entry.input) {
216
+ bashCommands.push(entry.input);
217
+ }
218
+
182
219
  if (entry.tool === 'Write' || entry.tool === 'Edit') {
183
220
  if (entry.input) {
184
221
  writtenFiles.add(entry.input);
@@ -245,6 +282,46 @@ export class AdaptiveThresholds {
245
282
  signals.errorTrend = secondHalfErrors - firstHalfErrors;
246
283
  }
247
284
 
285
+ // Output length decay: if last 5 assistant outputs avg <50% of first 5
286
+ if (assistantOutputLengths.length >= 10) {
287
+ const first5 = assistantOutputLengths.slice(0, 5);
288
+ const last5 = assistantOutputLengths.slice(-5);
289
+ const firstAvg = first5.reduce((a, b) => a + b, 0) / 5;
290
+ const lastAvg = last5.reduce((a, b) => a + b, 0) / 5;
291
+ if (firstAvg > 0 && lastAvg < firstAvg * 0.5) signals.outputLengthDecay = 1;
292
+ }
293
+
294
+ // Tool output volume: cumulative tool result size
295
+ if (toolOutputBytes > 600_000) signals.toolOutputVolume = 2;
296
+ else if (toolOutputBytes > 300_000) signals.toolOutputVolume = 1;
297
+
298
+ // Turn latency trend: avg gap in last 10 entries >2x first 10
299
+ if (entryTimestamps.length >= 20) {
300
+ const gaps = (ts) => {
301
+ const g = [];
302
+ for (let i = 1; i < ts.length; i++) g.push(ts[i] - ts[i - 1]);
303
+ return g;
304
+ };
305
+ const firstGaps = gaps(entryTimestamps.slice(0, 11));
306
+ const lastGaps = gaps(entryTimestamps.slice(-11));
307
+ const avgFirst = firstGaps.reduce((a, b) => a + b, 0) / firstGaps.length;
308
+ const avgLast = lastGaps.reduce((a, b) => a + b, 0) / lastGaps.length;
309
+ if (avgFirst > 0 && avgLast > avgFirst * 2) signals.turnLatencyTrend = 1;
310
+ }
311
+
312
+ // Bash repetition: 3+ identical consecutive Bash commands
313
+ let maxConsecutive = 0;
314
+ let streak = 1;
315
+ for (let i = 1; i < bashCommands.length; i++) {
316
+ if (bashCommands[i] === bashCommands[i - 1]) {
317
+ streak++;
318
+ if (streak > maxConsecutive) maxConsecutive = streak;
319
+ } else {
320
+ streak = 1;
321
+ }
322
+ }
323
+ if (maxConsecutive >= 3) signals.bashRepetition = 1;
324
+
248
325
  return signals;
249
326
  }
250
327
 
@@ -124,6 +124,38 @@ export function createApi(app, daemon) {
124
124
  res.json({ status: 'ok', uptime: process.uptime() });
125
125
  });
126
126
 
127
+ // Debug: test fetch to llama-server from daemon runtime
128
+ app.get('/api/lab/debug-fetch', async (req, res) => {
129
+ const target = req.query.url || 'http://localhost:8081/v1/chat/completions';
130
+ const log = [];
131
+ try {
132
+ log.push(`fetch → ${target}`);
133
+ log.push(`node ${process.version}, electron ${process.versions.electron || 'N/A'}`);
134
+ const start = Date.now();
135
+ const r = await fetch(target, {
136
+ method: 'POST',
137
+ headers: { 'Content-Type': 'application/json' },
138
+ body: JSON.stringify({ model: 'Qwen3-0.6B-Q8_0.gguf', messages: [{ role: 'user', content: 'Say ok' }], stream: true, max_tokens: 10 }),
139
+ signal: AbortSignal.timeout(10000),
140
+ });
141
+ log.push(`status=${r.status} in ${Date.now() - start}ms`);
142
+ const reader = r.body.getReader();
143
+ let chunks = 0;
144
+ while (chunks < 5) {
145
+ const { done, value } = await reader.read();
146
+ if (done) break;
147
+ chunks++;
148
+ log.push(`chunk ${chunks}: ${new TextDecoder().decode(value).slice(0, 120)}`);
149
+ }
150
+ reader.cancel();
151
+ log.push(`total chunks read: ${chunks}`);
152
+ res.json({ ok: true, log });
153
+ } catch (err) {
154
+ log.push(`ERROR: ${err.message}`);
155
+ res.json({ ok: false, log, error: err.message });
156
+ }
157
+ });
158
+
127
159
  // List all agents
128
160
  app.get('/api/agents', (req, res) => {
129
161
  res.json(daemon.registry.getAll());
@@ -6703,11 +6735,9 @@ Keep responses concise. Help them think, don't lecture them about the system the
6703
6735
  let closed = false;
6704
6736
  req.on('close', () => { closed = true; });
6705
6737
 
6706
- const stream = daemon.modelLab.streamInference(params);
6707
- for await (const event of stream) {
6708
- if (closed) break;
6709
- res.write(`data: ${JSON.stringify(event)}\n\n`);
6710
- }
6738
+ await daemon.modelLab.streamInference(params, (event) => {
6739
+ if (!closed) res.write(`data: ${JSON.stringify(event)}\n\n`);
6740
+ });
6711
6741
 
6712
6742
  if (!closed) {
6713
6743
  res.write('data: [DONE]\n\n');
@@ -460,7 +460,7 @@ export class Journalist {
460
460
  '(What was completed. Name files, functions, and line numbers.)',
461
461
  '',
462
462
  'Be specific. Name files, functions, and line numbers. Do not summarize vaguely.',
463
- 'Keep your response under 2000 characters.',
463
+ 'Keep your response under 1500 characters.',
464
464
  '',
465
465
  '---',
466
466
  '',
@@ -469,7 +469,7 @@ export class Journalist {
469
469
  ];
470
470
 
471
471
  let totalChars = 0;
472
- const cap = 30_000;
472
+ const cap = 15_000;
473
473
  for (const entry of entries.slice(-200)) {
474
474
  const line = this.formatEntry(entry);
475
475
  if (totalChars + line.length > cap) break;
@@ -853,15 +853,15 @@ export class Journalist {
853
853
  const agentLog = filteredLogs[agent.id];
854
854
  const entries = agentLog?.entries || [];
855
855
 
856
- // Layer 7 memory: discoveries, constraints, specializations
857
- const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 10, 2000) || '';
856
+ // Layer 7 memory: discoveries (inline, not pointer — agents lose context with pointers), constraints, specializations
857
+ const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 10, 1500) || '';
858
858
  const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
859
859
  const specialization = this.daemon.memory?.getSpecialization(agent.id);
860
860
  const specLine = specialization?.avgQualityScore != null
861
861
  ? `- Quality profile: ${specialization.avgQualityScore}/100 across ${specialization.sessionCount} sessions`
862
862
  : '';
863
863
 
864
- const recentChain = this.daemon.memory?.getRecentHandoffMarkdown(agent.role, 3, 3000, agent.workingDir, agent.teamId) || '';
864
+ const recentChain = this.daemon.memory?.getRecentHandoffMarkdown(agent.role, 1, 1500, agent.workingDir, agent.teamId) || '';
865
865
 
866
866
  const agentFeedback = this.getUserFeedback(agent.id).slice(-5);
867
867
  const conversationSummary = agentFeedback.length > 0
@@ -871,7 +871,7 @@ export class Journalist {
871
871
  const recentTools = entries
872
872
  .filter((e) => e.type === 'tool' || e.type === 'error')
873
873
  .slice(-5)
874
- .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || e.text || '').slice(0, 80)}`)
874
+ .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || e.text || '').slice(0, 200)}`)
875
875
  .join('\n');
876
876
 
877
877
  // Try AI-synthesized session summary
@@ -908,7 +908,7 @@ export class Journalist {
908
908
  const fallbackRecentTools = entries
909
909
  .filter((e) => e.type === 'tool' || e.type === 'error')
910
910
  .slice(-5)
911
- .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || '').slice(0, 80)}`)
911
+ .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || '').slice(0, 200)}`)
912
912
  .join('\n');
913
913
 
914
914
  const fallbackParts = [];
@@ -919,7 +919,13 @@ export class Journalist {
919
919
  sessionSummary = fallbackParts.join('\n\n');
920
920
  }
921
921
 
922
- return [
922
+ // For quality_degradation rotations, drop user messages (already in session summary)
923
+ const includeUserMessages = options.reason !== 'quality_degradation';
924
+
925
+ // Cap Original Task to 1000 chars — task descriptions for debugging can be long
926
+ const originalTask = agent.prompt ? agent.prompt.slice(0, 1000) + (agent.prompt.length > 1000 ? '…' : '') : '';
927
+
928
+ let brief = [
923
929
  `# Handoff Brief — ${agent.name} (${agent.role})`,
924
930
  ``,
925
931
  `Role: ${agent.role} | Scope: ${agent.scope?.join(', ') || 'unrestricted'} | Provider: ${agent.provider}`,
@@ -927,17 +933,27 @@ export class Journalist {
927
933
  `Rotation: ${options.reason || 'manual'}${options.qualityScore ? ` (quality: ${options.qualityScore}/100)` : ''} | Tokens: ${agent.tokensUsed}`,
928
934
  specLine,
929
935
  ``,
930
- discoveries ? `## Known Issues & Fixes\n\n${discoveries}\n` : '',
936
+ // Priority order: session summary (contains unresolved errors) first,
937
+ // then constraints, then discoveries, then tools — so the most critical
938
+ // debugging context survives even if the brief hits the hard cap.
939
+ sessionSummary ? `## Session Summary\n\n${sessionSummary}\n` : '',
931
940
  constraints ? `## Project Constraints (must follow)\n\n${constraints}\n` : '',
941
+ discoveries ? `## Known Issues & Fixes\n\n${discoveries}\n` : '',
932
942
  recentTools ? `## Last 5 Tool Calls\n\n${recentTools}\n` : '',
933
- sessionSummary ? `## Session Summary\n\n${sessionSummary}\n` : '',
934
- conversationSummary ? `## Recent User Messages\n\n${conversationSummary}\n` : '',
943
+ includeUserMessages && conversationSummary ? `## Recent User Messages\n\n${conversationSummary}\n` : '',
935
944
  recentChain ? `## Rotation History\n\n${recentChain}\n` : '',
936
- agent.prompt ? `## Original Task\n\n${agent.prompt}\n` : '',
945
+ originalTask ? `## Original Task\n\n${originalTask}\n` : '',
937
946
  ``,
938
947
  agent.role === 'planner' ? 'CRITICAL: You are a PLANNING ONLY agent. Do NOT implement code. Route all work to your team via .groove/recommended-team.json.\n' : '',
939
948
  `Continue seamlessly — finish the work and deliver the output.`,
940
949
  ].filter(Boolean).join('\n');
950
+
951
+ // Hard cap: 8000 chars — enough for debugging context without overwhelming the new agent
952
+ if (brief.length > 8000) {
953
+ brief = brief.slice(0, 7950) + '\n\n[Brief truncated — see session logs for full context]';
954
+ }
955
+
956
+ return brief;
941
957
  }
942
958
 
943
959
  // --- Workspace Grouping ---