tuna-agent 0.1.180 → 0.1.182

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.
@@ -57,7 +57,7 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
57
57
  * Run post-task self-reflection via Mem0.
58
58
  * Agent reviews what it did and stores lessons learned.
59
59
  */
60
- runReflection(task: TaskAssignment, resultSummary: string, status: 'done' | 'failed', cwd: string): Promise<void>;
60
+ runReflection(task: TaskAssignment, resultSummary: string, status: 'done' | 'failed', cwd: string, conversation?: string[]): Promise<void>;
61
61
  /**
62
62
  * Store user rating feedback into Mem0.
63
63
  */
@@ -181,6 +181,7 @@ export class ClaudeCodeAdapter {
181
181
  }
182
182
  }
183
183
  let lastTaskOutput = ''; // Track last output for reflection
184
+ const convo = []; // User/Agent transcript → conversation memory (Mem0)
184
185
  try {
185
186
  for (let round = 0; round < MAX_ROUNDS; round++) {
186
187
  let streamMsgId = `team-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -204,6 +205,7 @@ export class ClaudeCodeAdapter {
204
205
  return false;
205
206
  };
206
207
  console.log(`[ClaudeCode] Agent Team round ${round + 1}: ${userMessage.substring(0, 80)}${currentInputFiles?.length ? ` (+${currentInputFiles.length} images)` : ''}`);
208
+ convo.push(`User: ${userMessage}`);
207
209
  const defaultWorkspace = path.join(os.homedir(), 'tuna-workspace');
208
210
  const cwd = task.repoPath || defaultWorkspace;
209
211
  if (!fs.existsSync(cwd))
@@ -346,6 +348,8 @@ export class ClaudeCodeAdapter {
346
348
  }
347
349
  // Track last output for reflection (fallback to result.result if no streaming text)
348
350
  lastTaskOutput = turnAccumulatedText.trim() || result.result;
351
+ if (lastTaskOutput)
352
+ convo.push(`Agent: ${lastTaskOutput}`);
349
353
  if (lastTaskOutput) {
350
354
  console.log(`[Reflection] Captured ${lastTaskOutput.length} chars of task output for reflection`);
351
355
  }
@@ -415,7 +419,7 @@ export class ClaudeCodeAdapter {
415
419
  });
416
420
  this.trackMetrics('done', totalDurationMs, localAgentId);
417
421
  const timeoutOutput = lastTaskOutput || 'Task completed (no follow-up)';
418
- this.runReflection(task, timeoutOutput, 'done', task.repoPath)
422
+ this.runReflection(task, timeoutOutput, 'done', task.repoPath, convo)
419
423
  .then(() => this.runSelfImprovement(task.repoPath, localAgentId))
420
424
  .catch(() => { });
421
425
  return;
@@ -448,7 +452,7 @@ export class ClaudeCodeAdapter {
448
452
  this.trackMetrics('done', totalDurationMs, localAgentId);
449
453
  console.log(`[ClaudeCode] Agent Team task ${task.id} completed (${(totalDurationMs / 1000).toFixed(1)}s)`);
450
454
  // Post-task reflection with actual output (non-blocking)
451
- this.runReflection(task, lastTaskOutput || 'Task completed without text output', 'done', task.repoPath)
455
+ this.runReflection(task, lastTaskOutput || 'Task completed without text output', 'done', task.repoPath, convo)
452
456
  .then(() => this.runSelfImprovement(task.repoPath, localAgentId))
453
457
  .catch(() => { });
454
458
  }
@@ -810,7 +814,7 @@ export class ClaudeCodeAdapter {
810
814
  * Run post-task self-reflection via Mem0.
811
815
  * Agent reviews what it did and stores lessons learned.
812
816
  */
813
- async runReflection(task, resultSummary, status, cwd) {
817
+ async runReflection(task, resultSummary, status, cwd, conversation) {
814
818
  if (task.enableReflection === false)
815
819
  return;
816
820
  if (!process.env.MEM0_SSH_HOST)
@@ -820,6 +824,16 @@ export class ClaudeCodeAdapter {
820
824
  return;
821
825
  const agentName = path.basename(cwd);
822
826
  const agentId = task.agentId || '';
827
+ // Conversation memory: distill what was DISCUSSED (topics, decisions, user
828
+ // preferences) into Mem0 — independent of the lesson reflection below, so the
829
+ // agent remembers past conversations without manually writing .md notes.
830
+ if (conversation && conversation.length >= 2) {
831
+ import('../mcp/setup.js')
832
+ .then(({ storeConversationFacts }) => storeConversationFacts(conversation, agentName))
833
+ .then((n) => { if (n)
834
+ this.getMetricsForAgent(agentId).memoryCount += n; })
835
+ .catch(() => { });
836
+ }
823
837
  // Record a quality score for this task — the signal the self-improvement loop
824
838
  // optimizes against (closes the loop: score in -> trend gates rule changes out).
825
839
  const runScore = ClaudeCodeAdapter.deriveScore(status, resultSummary);
@@ -745,6 +745,7 @@ ${skillContent.slice(0, 15000)}`;
745
745
  taskAbortControllers.set(taskId, abort);
746
746
  let pmSessionId = pmState.pmSessionId;
747
747
  const MAX_RESUMED_ROUNDS = 50;
748
+ const convo = []; // User/Agent transcript → conversation memory (Mem0)
748
749
  try {
749
750
  let userMessage = firstMessage;
750
751
  let currentAttachments = firstAttachments;
@@ -761,6 +762,7 @@ ${skillContent.slice(0, 15000)}`;
761
762
  }
762
763
  }
763
764
  console.log(`[Daemon] Resumed PM chat round ${round + 1}: ${userMessage.substring(0, 80)}${inputFiles?.length ? ` (+${inputFiles.length} images)` : ''}`);
765
+ convo.push(`User: ${userMessage}`);
764
766
  const streamMsgId = `pm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
765
767
  let firstChunkIso = '';
766
768
  const chatResult = await chatWithPM(pmSessionId, pmState.repoPath, userMessage, abort.signal, (chunk) => {
@@ -774,6 +776,7 @@ ${skillContent.slice(0, 15000)}`;
774
776
  // PM produced a plan → execute it using shared helper
775
777
  if (chatResult.plan && chatResult.plan.subtasks.length > 0) {
776
778
  const plan = chatResult.plan;
779
+ convo.push(`Agent: (bắt tay thực hiện) ${plan.summary}`);
777
780
  // (No "Got it! <summary>" — plan_ready already conveys the summary.)
778
781
  wsClient.sendPlanReady(taskId, {
779
782
  summary: plan.summary,
@@ -794,8 +797,6 @@ ${skillContent.slice(0, 15000)}`;
794
797
  if (result.status === 'done' && !result.followUpMessage) {
795
798
  const totalDuration = result.sessions.reduce((sum, s) => sum + (s.durationMs ?? 0), 0);
796
799
  wsClient.sendTaskDone(taskId, { result: plan.summary, durationMs: totalDuration });
797
- await new Promise(resolve => setTimeout(resolve, 150));
798
- wsClient.sendPMMessage(taskId, { sender: 'pm', content: `Task completed.` });
799
800
  }
800
801
  else if (result.followUpMessage) {
801
802
  userMessage = result.followUpMessage;
@@ -810,6 +811,8 @@ ${skillContent.slice(0, 15000)}`;
810
811
  return;
811
812
  }
812
813
  // Send final PM message
814
+ if (chatResult.response?.trim())
815
+ convo.push(`Agent: ${chatResult.response}`);
813
816
  wsClient.sendPMMessage(taskId, { sender: 'pm', content: chatResult.response, startedAt: firstChunkIso || undefined });
814
817
  if (round === MAX_RESUMED_ROUNDS - 1)
815
818
  break;
@@ -833,6 +836,12 @@ ${skillContent.slice(0, 15000)}`;
833
836
  }
834
837
  }
835
838
  finally {
839
+ // Conversation memory: remember what was discussed across sessions (non-blocking).
840
+ if (convo.length >= 2) {
841
+ import('../mcp/setup.js')
842
+ .then(({ storeConversationFacts }) => storeConversationFacts(convo, path.basename(pmState.repoPath)))
843
+ .catch(() => { });
844
+ }
836
845
  activeTasks--;
837
846
  taskAbortControllers.delete(taskId);
838
847
  resolvers.delete(taskId);
@@ -852,6 +861,7 @@ ${skillContent.slice(0, 15000)}`;
852
861
  taskAbortControllers.set(taskId, abort);
853
862
  let sessionId = savedState.agentTeamSessionId;
854
863
  let totalDurationMs = 0;
864
+ const convo = []; // User/Agent transcript → conversation memory (Mem0)
855
865
  try {
856
866
  let userMessage = firstMessage;
857
867
  let currentInputFiles;
@@ -875,6 +885,7 @@ ${skillContent.slice(0, 15000)}`;
875
885
  let turnAccumulatedText = '';
876
886
  let messageCount = 0;
877
887
  console.log(`[Daemon] Resumed agent_team round ${round + 1}: ${userMessage.substring(0, 80)}${currentInputFiles?.length ? ` (+${currentInputFiles.length} images)` : ''}`);
888
+ convo.push(`User: ${userMessage}`);
878
889
  let result;
879
890
  try {
880
891
  result = await runClaude({
@@ -1001,6 +1012,8 @@ ${skillContent.slice(0, 15000)}`;
1001
1012
  });
1002
1013
  }
1003
1014
  lastResumeOutput = turnAccumulatedText.trim() || resultText || lastResumeOutput;
1015
+ if (turnAccumulatedText.trim() || resultText)
1016
+ convo.push(`Agent: ${turnAccumulatedText.trim() || resultText}`);
1004
1017
  if (result.isError) {
1005
1018
  wsClient.sendTaskFailed(taskId, result.result);
1006
1019
  return;
@@ -1078,6 +1091,12 @@ ${skillContent.slice(0, 15000)}`;
1078
1091
  }
1079
1092
  }
1080
1093
  finally {
1094
+ // Conversation memory: remember what was discussed across sessions (non-blocking).
1095
+ if (convo.length >= 2) {
1096
+ import('../mcp/setup.js')
1097
+ .then(({ storeConversationFacts }) => storeConversationFacts(convo, path.basename(savedState.repoPath)))
1098
+ .catch(() => { });
1099
+ }
1081
1100
  activeTasks--;
1082
1101
  taskAbortControllers.delete(taskId);
1083
1102
  resolvers.delete(taskId);
@@ -333,7 +333,7 @@ export async function executeSubtask(subtask, repoPath, contracts, callbacks, si
333
333
  const result = await runClaude({
334
334
  prompt: subtask.description,
335
335
  cwd,
336
- allowedTools: ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep'],
336
+ allowedTools: ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep', 'mcp__mem0', 'mcp__tuna-knowledge', 'mcp__tuna-idea', 'mcp__tuna-browser', 'mcp__appeeky'],
337
337
  disallowedTools: ['AskUserQuestion'],
338
338
  systemPrompt: buildSessionPrompt(subtask, contracts),
339
339
  outputFormat: 'stream-json',
@@ -370,7 +370,7 @@ export async function executeSubtask(subtask, repoPath, contracts, callbacks, si
370
370
  const resumeResult = await runClaude({
371
371
  prompt: `The answer to your question "${question.question}" is: ${answer}\n\nContinue your work with this information.`,
372
372
  cwd,
373
- allowedTools: ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep'],
373
+ allowedTools: ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep', 'mcp__mem0', 'mcp__tuna-knowledge', 'mcp__tuna-idea', 'mcp__tuna-browser', 'mcp__appeeky'],
374
374
  disallowedTools: ['AskUserQuestion'],
375
375
  resumeSessionId: info.sessionId,
376
376
  outputFormat: 'stream-json',
@@ -449,7 +449,7 @@ export async function executeSubtask(subtask, repoPath, contracts, callbacks, si
449
449
  const resumeResult = await runClaude({
450
450
  prompt: `The answer to your question "${fallbackQuestion.question}" is: ${answer}\n\nContinue your work with this information.`,
451
451
  cwd,
452
- allowedTools: ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep'],
452
+ allowedTools: ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep', 'mcp__mem0', 'mcp__tuna-knowledge', 'mcp__tuna-idea', 'mcp__tuna-browser', 'mcp__appeeky'],
453
453
  disallowedTools: ['AskUserQuestion'],
454
454
  resumeSessionId: info.sessionId,
455
455
  outputFormat: 'stream-json',
@@ -21,12 +21,14 @@ export declare function callMem0Patterns(agentName: string, minCluster?: number)
21
21
  sources: string[];
22
22
  }>>;
23
23
  export declare function callOllamaEmbedBatch(texts: string[]): Promise<number[][] | null>;
24
+ export declare function callMem0Reflect(taskDesc: string, resultSummary: string, status: 'done' | 'failed'): Promise<string>;
24
25
  /**
25
- * Generate AI-powered reflection from task results using Claude CLI (-p mode).
26
- * Spawns `claude -p <prompt>` locally uses existing Claude subscription, no extra cost.
27
- * Returns a concise lesson learned, or empty string on failure.
26
+ * Distill a finished conversation into durable facts (topics discussed, decisions,
27
+ * user preferences/interests) and store them in Mem0 so the agent "remembers"
28
+ * past conversations in future sessions WITHOUT relying on it manually writing
29
+ * notes to a .md file. Returns the number of facts stored.
28
30
  */
29
- export declare function callMem0Reflect(taskDesc: string, resultSummary: string, status: 'done' | 'failed'): Promise<string>;
31
+ export declare function storeConversationFacts(transcript: string[], agentName: string): Promise<number>;
30
32
  /**
31
33
  * Generate MCP server config file for Claude Code.
32
34
  * This file is auto-detected by runClaude and passed via --mcp-config.
package/dist/mcp/setup.js CHANGED
@@ -328,8 +328,9 @@ export async function callOllamaEmbedBatch(texts) {
328
328
  * Spawns `claude -p <prompt>` locally — uses existing Claude subscription, no extra cost.
329
329
  * Returns a concise lesson learned, or empty string on failure.
330
330
  */
331
- export async function callMem0Reflect(taskDesc, resultSummary, status) {
332
- const { execSync } = await import('child_process');
331
+ /** Run a one-shot `claude -p` text prompt (used for reflection/distillation). */
332
+ async function claudeTextOnce(prompt, tag, timeoutMs = 60000) {
333
+ const { execSync, spawn } = await import('child_process');
333
334
  // Resolve claude binary path (same search logic as claude-cli.ts)
334
335
  let claudeBin = 'claude';
335
336
  try {
@@ -348,25 +349,7 @@ export async function callMem0Reflect(taskDesc, resultSummary, status) {
348
349
  }).trim();
349
350
  }
350
351
  catch { /* fall back to 'claude' */ }
351
- const prompt = [
352
- 'You are an AI agent reflecting on a completed task.',
353
- 'Extract 1-2 concise, actionable lessons learned that would help with similar future tasks.',
354
- '',
355
- `Task: ${taskDesc.substring(0, 300)}`,
356
- `Status: ${status}`,
357
- `Result summary: ${resultSummary.substring(0, 600)}`,
358
- '',
359
- 'Rules:',
360
- '- ALWAYS extract at least 1 lesson — there is always something to note',
361
- '- Be specific: tool names, patterns, pitfalls, or key findings worth remembering',
362
- '- If task failed: focus on what went wrong and how to avoid it next time',
363
- '- If task succeeded: note the approach or insight that was most useful',
364
- '- Keep each lesson to 1 sentence, no bullet points, no preamble',
365
- '',
366
- 'Lessons learned:',
367
- ].join('\n');
368
352
  // Use spawn with stdin='ignore' — execFile keeps stdin pipe open which causes claude to hang/SIGTERM
369
- const { spawn } = await import('child_process');
370
353
  return new Promise((resolve) => {
371
354
  const child = spawn(claudeBin, ['-p', prompt, '--output-format', 'text'], { stdio: ['ignore', 'pipe', 'pipe'] });
372
355
  let stdout = '';
@@ -375,31 +358,99 @@ export async function callMem0Reflect(taskDesc, resultSummary, status) {
375
358
  child.stderr.on('data', (d) => { stderr += d.toString(); });
376
359
  const timer = setTimeout(() => {
377
360
  child.kill();
378
- console.warn('[Mem0 Reflect] Claude CLI timed out after 60s');
361
+ console.warn(`[${tag}] Claude CLI timed out after ${timeoutMs / 1000}s`);
379
362
  resolve('');
380
- }, 60000);
363
+ }, timeoutMs);
381
364
  child.on('close', (code) => {
382
365
  clearTimeout(timer);
383
366
  if (code !== 0) {
384
- console.warn(`[Mem0 Reflect] Claude CLI exited ${code}: ${stderr.substring(0, 200)}`);
385
- resolve('');
386
- return;
387
- }
388
- const reflection = stdout.trim();
389
- if (!reflection) {
367
+ console.warn(`[${tag}] Claude CLI exited ${code}: ${stderr.substring(0, 200)}`);
390
368
  resolve('');
391
369
  return;
392
370
  }
393
- console.log(`[Mem0 Reflect] Generated: "${reflection.substring(0, 100)}..."`);
394
- resolve(reflection);
371
+ resolve(stdout.trim());
395
372
  });
396
373
  child.on('error', (err) => {
397
374
  clearTimeout(timer);
398
- console.warn(`[Mem0 Reflect] Spawn error: ${err.message}`);
375
+ console.warn(`[${tag}] Spawn error: ${err.message}`);
399
376
  resolve('');
400
377
  });
401
378
  });
402
379
  }
380
+ export async function callMem0Reflect(taskDesc, resultSummary, status) {
381
+ const prompt = [
382
+ 'You are an AI agent reflecting on a completed task.',
383
+ 'Extract 1-2 concise, actionable lessons learned that would help with similar future tasks.',
384
+ '',
385
+ `Task: ${taskDesc.substring(0, 300)}`,
386
+ `Status: ${status}`,
387
+ `Result summary: ${resultSummary.substring(0, 600)}`,
388
+ '',
389
+ 'Rules:',
390
+ '- ALWAYS extract at least 1 lesson — there is always something to note',
391
+ '- Be specific: tool names, patterns, pitfalls, or key findings worth remembering',
392
+ '- If task failed: focus on what went wrong and how to avoid it next time',
393
+ '- If task succeeded: note the approach or insight that was most useful',
394
+ '- Keep each lesson to 1 sentence, no bullet points, no preamble',
395
+ '',
396
+ 'Lessons learned:',
397
+ ].join('\n');
398
+ const reflection = await claudeTextOnce(prompt, 'Mem0 Reflect');
399
+ if (reflection)
400
+ console.log(`[Mem0 Reflect] Generated: "${reflection.substring(0, 100)}..."`);
401
+ return reflection;
402
+ }
403
+ /**
404
+ * Distill a finished conversation into durable facts (topics discussed, decisions,
405
+ * user preferences/interests) and store them in Mem0 — so the agent "remembers"
406
+ * past conversations in future sessions WITHOUT relying on it manually writing
407
+ * notes to a .md file. Returns the number of facts stored.
408
+ */
409
+ export async function storeConversationFacts(transcript, agentName) {
410
+ if (!process.env.MEM0_SSH_HOST)
411
+ return 0;
412
+ if (!transcript || transcript.length < 2)
413
+ return 0; // need at least one exchange
414
+ // Cap each message and the total so the distill prompt stays small.
415
+ const capped = transcript.map((m) => m.length > 400 ? m.slice(0, 400) + '…' : m);
416
+ let convoText = capped.join('\n');
417
+ if (convoText.length > 6000)
418
+ convoText = convoText.slice(-6000);
419
+ const prompt = [
420
+ 'Below is a conversation between a user ("User") and you, an AI agent ("Agent").',
421
+ 'Extract the facts worth remembering LONG-TERM about this conversation: topics the user cares about, decisions made, user preferences, plans/projects mentioned, key conclusions.',
422
+ '',
423
+ 'Rules:',
424
+ '- Output 0 to 5 facts, ONE PER LINE, no bullets, no numbering, no preamble.',
425
+ '- Each fact must be self-contained and understandable months later (include subject/app/project names).',
426
+ '- Write facts in Vietnamese.',
427
+ '- Skip small talk, greetings, transient status ("task done", "đang chạy...").',
428
+ '- If nothing is worth remembering, output exactly: NONE',
429
+ '',
430
+ 'Conversation:',
431
+ convoText,
432
+ '',
433
+ 'Facts:',
434
+ ].join('\n');
435
+ const out = await claudeTextOnce(prompt, 'Mem0 Convo', 90000);
436
+ if (!out || /^NONE$/im.test(out.trim()))
437
+ return 0;
438
+ const facts = out.split('\n')
439
+ .map((l) => l.replace(/^[-*\d.\s]+/, '').trim())
440
+ .filter((l) => l.length > 15 && !/^NONE$/i.test(l))
441
+ .slice(0, 5);
442
+ let stored = 0;
443
+ for (const fact of facts) {
444
+ try {
445
+ await callMem0AddMemory(`[Hội thoại] ${fact}`, agentName);
446
+ stored++;
447
+ }
448
+ catch { /* best-effort */ }
449
+ }
450
+ if (stored)
451
+ console.log(`[Mem0 Convo] Stored ${stored} conversation fact(s) for "${agentName}"`);
452
+ return stored;
453
+ }
403
454
  /**
404
455
  * Build Mem0 MCP server config for an agent.
405
456
  * - MEM0_SSH_HOST="local": run mem0-mcp directly (Mem0 infra on same machine)
@@ -144,7 +144,7 @@ export async function planTask(task, onProgress, signal, onTextChunk, inputFiles
144
144
  const result = await runClaude({
145
145
  prompt: userPrompt,
146
146
  cwd,
147
- allowedTools: ['Read', 'Glob', 'Grep'],
147
+ allowedTools: ['Read', 'Glob', 'Grep', 'mcp__mem0', 'mcp__tuna-knowledge', 'mcp__tuna-idea'],
148
148
  systemPrompt,
149
149
  maxTurns,
150
150
  outputFormat: 'stream-json',
@@ -287,17 +287,20 @@ User message: ${userMessage}`;
287
287
  let firstDeltaMs = 0;
288
288
  let deltaCount = 0;
289
289
  const cwd = repoPath || path.join(os.homedir(), 'tuna-workspace');
290
+ let streamedText = '';
290
291
  const result = await runClaude({
291
292
  prompt: chatPrompt,
292
293
  cwd,
293
294
  resumeSessionId: pmSessionId || undefined,
294
- allowedTools: [],
295
- maxTurns: 1,
296
- lightweight: true,
295
+ // Read-only tools + knowledge/memory MCP: follow-ups like "check the skill
296
+ // list" need to actually look at files. No tools + 1 turn made the model
297
+ // attempt a tool call anyway and come back with an EMPTY result (silent task).
298
+ allowedTools: ['Read', 'Glob', 'Grep', 'mcp__mem0', 'mcp__tuna-knowledge', 'mcp__tuna-idea'],
299
+ maxTurns: 8,
297
300
  outputFormat: 'stream-json',
298
301
  includePartialMessages: true,
299
302
  inputFiles,
300
- onStreamLine: onTextChunk ? (data) => {
303
+ onStreamLine: (data) => {
301
304
  // Parse stream_event → content_block_delta for real token streaming
302
305
  if (data.type === 'stream_event') {
303
306
  const event = data.event;
@@ -305,15 +308,16 @@ User message: ${userMessage}`;
305
308
  const delta = event.delta;
306
309
  if (delta?.type === 'text_delta' && delta.text) {
307
310
  deltaCount++;
311
+ streamedText += delta.text;
308
312
  if (!firstDeltaMs) {
309
313
  firstDeltaMs = Date.now();
310
314
  console.log(`[PM] ⏱ first text delta: ${firstDeltaMs - chatStartMs}ms after chat start`);
311
315
  }
312
- onTextChunk(delta.text);
316
+ onTextChunk?.(delta.text);
313
317
  }
314
318
  }
315
319
  }
316
- } : undefined,
320
+ },
317
321
  signal,
318
322
  });
319
323
  const chatDoneMs = Date.now();
@@ -321,7 +325,11 @@ User message: ${userMessage}`;
321
325
  if (result.isError) {
322
326
  return { response: `Sorry, I encountered an error: ${result.result}`, sessionId: result.sessionId };
323
327
  }
324
- const rawResult = result.result;
328
+ // A hit turn-cap (or odd stop) can yield an empty `result` even though text was
329
+ // streamed. Never return empty — a silent agent looks dead to the user.
330
+ const rawResult = result.result?.trim()
331
+ || streamedText.trim()
332
+ || 'Xin lỗi anh, phiên xử lý tin nhắn vừa rồi bị nghẽn (không có output). Anh nhắn lại giúp nhé.';
325
333
  // Try extracting JSON plan
326
334
  let jsonStr = null;
327
335
  const codeBlockMatch = rawResult.match(/```(?:json)?\s*(\{[\s\S]*?"subtasks"[\s\S]*?\})\s*```/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.180",
3
+ "version": "0.1.182",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"