tuna-agent 0.1.80 → 0.1.82
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.
|
@@ -185,6 +185,7 @@ export class ClaudeCodeAdapter {
|
|
|
185
185
|
let firstChunkIso = '';
|
|
186
186
|
let turnAccumulatedText = '';
|
|
187
187
|
let messageCount = 0; // Track message_start events to detect new turns
|
|
188
|
+
let lastSentContent = ''; // Dedup: skip sending same message twice
|
|
188
189
|
console.log(`[ClaudeCode] Agent Team round ${round + 1}: ${userMessage.substring(0, 80)}${currentInputFiles?.length ? ` (+${currentInputFiles.length} images)` : ''}`);
|
|
189
190
|
const defaultWorkspace = path.join(os.homedir(), 'tuna-workspace');
|
|
190
191
|
const cwd = task.repoPath || defaultWorkspace;
|
|
@@ -231,11 +232,18 @@ export class ClaudeCodeAdapter {
|
|
|
231
232
|
if (messageCount > 1 && turnAccumulatedText.trim()) {
|
|
232
233
|
console.log(`[ClaudeCode] ✂️ Splitting bubble — finalizing ${turnAccumulatedText.length} chars from previous turn`);
|
|
233
234
|
ws.sendPMStreamEnd(task.id, streamMsgId);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
const simplified = simplifyMarkdown(turnAccumulatedText);
|
|
236
|
+
if (simplified !== lastSentContent) {
|
|
237
|
+
ws.sendPMMessage(task.id, {
|
|
238
|
+
sender: 'pm',
|
|
239
|
+
content: simplified,
|
|
240
|
+
startedAt: firstChunkIso || undefined,
|
|
241
|
+
});
|
|
242
|
+
lastSentContent = simplified;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
console.log(`[ClaudeCode] ⏭️ Skipping duplicate message (${simplified.length} chars)`);
|
|
246
|
+
}
|
|
239
247
|
turnAccumulatedText = '';
|
|
240
248
|
firstChunkIso = '';
|
|
241
249
|
streamMsgId = `team-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -327,13 +335,20 @@ export class ClaudeCodeAdapter {
|
|
|
327
335
|
if (lastTaskOutput) {
|
|
328
336
|
console.log(`[Reflection] Captured ${lastTaskOutput.length} chars of task output for reflection`);
|
|
329
337
|
}
|
|
330
|
-
// Send finalized message for the last turn's remaining text
|
|
338
|
+
// Send finalized message for the last turn's remaining text (skip if duplicate)
|
|
331
339
|
if (turnAccumulatedText.trim()) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
340
|
+
const simplified = simplifyMarkdown(turnAccumulatedText);
|
|
341
|
+
if (simplified !== lastSentContent) {
|
|
342
|
+
ws.sendPMMessage(task.id, {
|
|
343
|
+
sender: 'pm',
|
|
344
|
+
content: simplified,
|
|
345
|
+
startedAt: firstChunkIso || undefined,
|
|
346
|
+
});
|
|
347
|
+
lastSentContent = simplified;
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
console.log(`[ClaudeCode] ⏭️ Skipping duplicate final message (${simplified.length} chars)`);
|
|
351
|
+
}
|
|
337
352
|
}
|
|
338
353
|
// Last round → close
|
|
339
354
|
if (round === MAX_ROUNDS - 1)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
|
-
import { StringDecoder } from 'string_decoder';
|
|
5
4
|
import { runClaude } from '../utils/claude-cli.js';
|
|
6
5
|
import { validatePath } from '../utils/validate-path.js';
|
|
7
6
|
const NEEDS_INPUT_MARKER = '"status":"NEEDS_INPUT"';
|
|
@@ -23,9 +22,6 @@ export async function runTask(task, onProgress, signal, confirmBeforeEdit) {
|
|
|
23
22
|
if (confirmBeforeEdit) {
|
|
24
23
|
args.push('--permission-mode', 'default');
|
|
25
24
|
}
|
|
26
|
-
else {
|
|
27
|
-
args.push('--permission-mode', 'bypassPermissions');
|
|
28
|
-
}
|
|
29
25
|
const env = {
|
|
30
26
|
...process.env,
|
|
31
27
|
HOME: process.env.HOME || '',
|
|
@@ -83,9 +79,8 @@ export async function runTask(task, onProgress, signal, confirmBeforeEdit) {
|
|
|
83
79
|
let stdout = '';
|
|
84
80
|
let stderr = '';
|
|
85
81
|
let buffer = '';
|
|
86
|
-
const stdoutDecoder = new StringDecoder('utf8');
|
|
87
82
|
proc.stdout.on('data', (chunk) => {
|
|
88
|
-
const text =
|
|
83
|
+
const text = chunk.toString();
|
|
89
84
|
stdout += text;
|
|
90
85
|
buffer += text;
|
|
91
86
|
const lines = buffer.split('\n');
|
|
@@ -102,9 +97,8 @@ export async function runTask(task, onProgress, signal, confirmBeforeEdit) {
|
|
|
102
97
|
}
|
|
103
98
|
}
|
|
104
99
|
});
|
|
105
|
-
const stderrDecoder = new StringDecoder('utf8');
|
|
106
100
|
proc.stderr.on('data', (chunk) => {
|
|
107
|
-
stderr +=
|
|
101
|
+
stderr += chunk.toString();
|
|
108
102
|
});
|
|
109
103
|
proc.on('close', (code) => {
|
|
110
104
|
clearTimeout(timeoutTimer);
|
|
@@ -431,15 +425,8 @@ export async function executeSubtask(subtask, repoPath, contracts, callbacks, si
|
|
|
431
425
|
}
|
|
432
426
|
else {
|
|
433
427
|
// Fallback: detect if AI just wrote questions as text without using NEEDS_INPUT
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const isShortOutput = result.result.length < 500;
|
|
437
|
-
const isQuickRun = result.durationMs != null && result.durationMs < 15000;
|
|
438
|
-
const fallbackQuestion = (isShortOutput && isQuickRun)
|
|
439
|
-
? parseQuestionFromOutput(result.result, subtask.id)
|
|
440
|
-
: null;
|
|
441
|
-
const seemsLikeQuestion = !!fallbackQuestion;
|
|
442
|
-
if (seemsLikeQuestion && fallbackQuestion && callbacks?.onSubtaskNeedsInput) {
|
|
428
|
+
const fallbackQuestion = parseQuestionFromOutput(result.result, subtask.id);
|
|
429
|
+
if (fallbackQuestion && callbacks?.onSubtaskNeedsInput) {
|
|
443
430
|
info.status = 'waiting_input';
|
|
444
431
|
info.pendingQuestion = fallbackQuestion;
|
|
445
432
|
log(info, 'thinking', `Fallback: detected question in output (no NEEDS_INPUT marker): ${fallbackQuestion.question}`);
|
|
@@ -490,6 +477,15 @@ export async function executeSubtask(subtask, repoPath, contracts, callbacks, si
|
|
|
490
477
|
log(info, 'thinking', 'No answer received — task paused');
|
|
491
478
|
}
|
|
492
479
|
}
|
|
480
|
+
else if (looksIncomplete(result.result)) {
|
|
481
|
+
info.status = 'failed';
|
|
482
|
+
info.result = result.result;
|
|
483
|
+
log(info, 'error', `Detected incomplete output — marking as failed`);
|
|
484
|
+
callbacks?.onSubtaskLog?.(subtask.id, {
|
|
485
|
+
type: 'error',
|
|
486
|
+
message: `❌ Task did not complete: AI could not fulfill the request`,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
493
489
|
else {
|
|
494
490
|
info.status = 'done';
|
|
495
491
|
info.result = result.result;
|
|
@@ -586,6 +582,26 @@ export async function executeTaskWithPlan(task, plan, onProgress, callbacks, sig
|
|
|
586
582
|
console.log(`[Executor] Total time: ${(totalTime / 1000).toFixed(1)}s`);
|
|
587
583
|
return { sessions: allSessions, status: 'done' };
|
|
588
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Detect if Claude's output indicates it could NOT complete the task.
|
|
587
|
+
* Checks the last ~500 chars for common failure/inability patterns.
|
|
588
|
+
*/
|
|
589
|
+
function looksIncomplete(output) {
|
|
590
|
+
// Check only the tail — that's where the conclusion lives
|
|
591
|
+
const tail = output.slice(-500).toLowerCase();
|
|
592
|
+
const patterns = [
|
|
593
|
+
/i (?:was |am )?(?:unable|not able) to/,
|
|
594
|
+
/(?:could|can)(?:n't|not) (?:find|locate|access|complete|proceed|continue)/,
|
|
595
|
+
/(?:doesn't|does not|don't|do not) (?:exist|have access)/,
|
|
596
|
+
/(?:no (?:such|matching) (?:file|directory|path|repo))/,
|
|
597
|
+
/(?:not found|file not found|directory not found)/,
|
|
598
|
+
/(?:i need (?:you to|more information|the .* path|access))/,
|
|
599
|
+
/(?:please (?:provide|specify|confirm|check))/,
|
|
600
|
+
/(?:unfortunately|i apologize).{0,50}(?:cannot|couldn't|unable|can't)/,
|
|
601
|
+
/(?:blocked|stuck).{0,30}(?:because|due to|cannot)/,
|
|
602
|
+
];
|
|
603
|
+
return patterns.some((p) => p.test(tail));
|
|
604
|
+
}
|
|
589
605
|
/**
|
|
590
606
|
* Parse question from Claude output when question.json is missing.
|
|
591
607
|
* Fallback for when Claude outputs NEEDS_INPUT but didn't create the file.
|