tuna-agent 0.1.68 → 0.1.70
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.
|
@@ -38,6 +38,8 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
38
38
|
private currentAgentName;
|
|
39
39
|
/** Returns metrics for the currently active agent, initializing if needed. */
|
|
40
40
|
private get metrics();
|
|
41
|
+
/** Returns metrics for a specific agent by ID (safe for parallel execution). */
|
|
42
|
+
getMetricsForAgent(agentId: string): AgentMetrics;
|
|
41
43
|
constructor(config: AgentConfig);
|
|
42
44
|
/** Resolve CLAUDE.md path — check root first, then .claude/ subfolder. */
|
|
43
45
|
static resolveClaudeMdPath(folder: string): string;
|
|
@@ -23,8 +23,12 @@ export class ClaudeCodeAdapter {
|
|
|
23
23
|
currentAgentName = '';
|
|
24
24
|
/** Returns metrics for the currently active agent, initializing if needed. */
|
|
25
25
|
get metrics() {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
return this.getMetricsForAgent(this.currentAgentId);
|
|
27
|
+
}
|
|
28
|
+
/** Returns metrics for a specific agent by ID (safe for parallel execution). */
|
|
29
|
+
getMetricsForAgent(agentId) {
|
|
30
|
+
if (!this.metricsMap.has(agentId)) {
|
|
31
|
+
this.metricsMap.set(agentId, {
|
|
28
32
|
taskCount: 0,
|
|
29
33
|
successCount: 0,
|
|
30
34
|
failCount: 0,
|
|
@@ -42,7 +46,7 @@ export class ClaudeCodeAdapter {
|
|
|
42
46
|
upSince: new Date().toISOString(),
|
|
43
47
|
});
|
|
44
48
|
}
|
|
45
|
-
return this.metricsMap.get(
|
|
49
|
+
return this.metricsMap.get(agentId);
|
|
46
50
|
}
|
|
47
51
|
constructor(config) {
|
|
48
52
|
this.agentConfig = config;
|
|
@@ -810,22 +814,26 @@ export class ClaudeCodeAdapter {
|
|
|
810
814
|
const MIN_DESCRIPTION_LENGTH = 20;
|
|
811
815
|
if (task.description.length < MIN_DESCRIPTION_LENGTH)
|
|
812
816
|
return;
|
|
817
|
+
// Derive agent name from cwd (safe for parallel execution — no shared state)
|
|
818
|
+
const agentName = path.basename(cwd) || this.currentAgentName;
|
|
819
|
+
const agentId = task.agentId || this.currentAgentId;
|
|
813
820
|
try {
|
|
814
821
|
// Step 1: Generate AI-powered reflection via Ollama
|
|
815
822
|
console.log(`[Reflection] Generating AI reflection for task ${task.id} (${status}), input: ${resultSummary.substring(0, 150)}...`);
|
|
816
823
|
const { callMem0Reflect, callMem0AddMemory } = await import('../mcp/setup.js');
|
|
817
824
|
const aiReflection = await callMem0Reflect(task.description, resultSummary, status);
|
|
818
825
|
if (!aiReflection) {
|
|
819
|
-
this.
|
|
826
|
+
this.getMetricsForAgent(agentId).reflectionSkipCount++;
|
|
820
827
|
console.log(`[Reflection] No meaningful lesson — skipping storage for task ${task.id}`);
|
|
821
828
|
return;
|
|
822
829
|
}
|
|
823
830
|
// Step 2: Store the AI-generated reflection in Mem0
|
|
824
831
|
console.log(`[Reflection] Storing: "${aiReflection.substring(0, 100)}..."`);
|
|
825
|
-
await callMem0AddMemory(aiReflection,
|
|
826
|
-
this.
|
|
827
|
-
|
|
828
|
-
|
|
832
|
+
await callMem0AddMemory(aiReflection, agentName);
|
|
833
|
+
const m = this.getMetricsForAgent(agentId);
|
|
834
|
+
m.reflectionCount++;
|
|
835
|
+
m.memoryCount++;
|
|
836
|
+
m.lastReflectionAt = new Date().toISOString();
|
|
829
837
|
console.log(`[Reflection] Stored for task ${task.id}`);
|
|
830
838
|
}
|
|
831
839
|
catch (err) {
|
|
@@ -836,7 +844,7 @@ export class ClaudeCodeAdapter {
|
|
|
836
844
|
? `Task failed: "${task.description.substring(0, 150)}". Error: ${resultSummary.substring(0, 200)}`
|
|
837
845
|
: `Task completed: "${task.description.substring(0, 150)}". Result: ${resultSummary.substring(0, 200)}`;
|
|
838
846
|
const { callMem0AddMemory } = await import('../mcp/setup.js');
|
|
839
|
-
await callMem0AddMemory(fallback,
|
|
847
|
+
await callMem0AddMemory(fallback, agentName);
|
|
840
848
|
}
|
|
841
849
|
catch {
|
|
842
850
|
// Both AI and fallback failed — give up silently
|
|
@@ -859,7 +867,8 @@ export class ClaudeCodeAdapter {
|
|
|
859
867
|
try {
|
|
860
868
|
console.log(`[Rating→Mem0] Storing rating for task "${data.taskTitle}" (${data.score > 0 ? '👍' : '👎'})`);
|
|
861
869
|
const { callMem0AddMemory } = await import('../mcp/setup.js');
|
|
862
|
-
|
|
870
|
+
const agentName = path.basename(data.cwd) || this.currentAgentName;
|
|
871
|
+
await callMem0AddMemory(memoryText, agentName);
|
|
863
872
|
console.log(`[Rating→Mem0] Rating stored successfully`);
|
|
864
873
|
}
|
|
865
874
|
catch (err) {
|
|
@@ -897,7 +906,8 @@ export class ClaudeCodeAdapter {
|
|
|
897
906
|
try {
|
|
898
907
|
console.log(`[Self-Improve] Running pattern detection (every ${ClaudeCodeAdapter.PATTERN_CHECK_INTERVAL} tasks, count=${this.taskCount})`);
|
|
899
908
|
const { callMem0Patterns } = await import('../mcp/setup.js');
|
|
900
|
-
const
|
|
909
|
+
const agentName = path.basename(cwd) || this.currentAgentName;
|
|
910
|
+
const patterns = await callMem0Patterns(agentName, 2);
|
|
901
911
|
if (patterns.length === 0) {
|
|
902
912
|
console.log(`[Self-Improve] No patterns detected yet`);
|
|
903
913
|
return;
|
package/dist/daemon/index.js
CHANGED
|
@@ -127,9 +127,14 @@ export async function startDaemon(config) {
|
|
|
127
127
|
console.log(`[Daemon] Agent ready: ${adapter.displayName} (${health.message})`);
|
|
128
128
|
}
|
|
129
129
|
let activeTasks = 0;
|
|
130
|
-
const
|
|
130
|
+
const MAX_CONCURRENT_PER_AGENT = 1; // Each agent can run 1 task at a time
|
|
131
131
|
const pendingInputResolvers = new Map();
|
|
132
132
|
const pendingPermissionResolvers = new Map();
|
|
133
|
+
// Track active tasks per agent (agentId → taskId)
|
|
134
|
+
const activeAgentTasks = new Map();
|
|
135
|
+
// Track abort controllers per task
|
|
136
|
+
const taskAbortControllers = new Map();
|
|
137
|
+
// Legacy single-task tracking (for functions that still use it)
|
|
133
138
|
let currentTaskId = null;
|
|
134
139
|
let currentTaskAbort = null;
|
|
135
140
|
const onAuthFailed = (code, reason) => {
|
|
@@ -196,8 +201,9 @@ export async function startDaemon(config) {
|
|
|
196
201
|
// Seed memory counts from Mem0 so they appear on agent detail immediately
|
|
197
202
|
ccAdapter.seedMemoryCounts().catch(() => { });
|
|
198
203
|
}
|
|
199
|
-
// Recover orphaned tasks — pass
|
|
200
|
-
|
|
204
|
+
// Recover orphaned tasks — pass active task IDs so API won't fail tasks we're still running
|
|
205
|
+
const activeTaskIds = Array.from(taskAbortControllers.keys());
|
|
206
|
+
ws.send({ action: 'recover_orphaned_tasks', activeTaskId: currentTaskId ?? undefined, activeTaskIds });
|
|
201
207
|
break;
|
|
202
208
|
case 'task_assigned': {
|
|
203
209
|
const task = msg.task;
|
|
@@ -209,53 +215,58 @@ export async function startDaemon(config) {
|
|
|
209
215
|
if (task.repoPath?.startsWith('~/')) {
|
|
210
216
|
task.repoPath = path.join(os.homedir(), task.repoPath.slice(2));
|
|
211
217
|
}
|
|
212
|
-
|
|
213
|
-
|
|
218
|
+
// Check per-agent concurrency (each agent can run 1 task at a time)
|
|
219
|
+
const agentId = task.agentId || '__default__';
|
|
220
|
+
if (activeAgentTasks.has(agentId)) {
|
|
221
|
+
console.log(`[Daemon] Agent ${agentId} busy — rejecting task ${task.id}`);
|
|
214
222
|
ws.send({ action: 'task_rejected', taskId: task.id, reason: 'agent_busy' });
|
|
215
223
|
break;
|
|
216
224
|
}
|
|
217
225
|
activeTasks++;
|
|
226
|
+
activeAgentTasks.set(agentId, task.id);
|
|
227
|
+
const abort = new AbortController();
|
|
228
|
+
taskAbortControllers.set(task.id, abort);
|
|
218
229
|
currentTaskId = task.id;
|
|
219
|
-
currentTaskAbort =
|
|
220
|
-
console.log(`[Daemon] Received task: ${task.id}
|
|
221
|
-
//
|
|
222
|
-
|
|
230
|
+
currentTaskAbort = abort;
|
|
231
|
+
console.log(`[Daemon] Received task: ${task.id} agent=${agentId} — ${task.description.slice(0, 80)} (attachments: ${task.attachments?.length ?? 0}) [active: ${activeTasks}]`);
|
|
232
|
+
// MCP config per-agent is handled by writeAgentFolderMcpConfig in adapter
|
|
233
|
+
// (writes per-agent .mcp.json in agent folder — safe for parallel execution)
|
|
234
|
+
// Run task in background (non-blocking) to allow parallel agent execution
|
|
235
|
+
(async () => {
|
|
223
236
|
try {
|
|
224
|
-
|
|
237
|
+
await adapter.handleTask(task, ws, pendingInputResolvers, abort.signal, pendingPermissionResolvers);
|
|
225
238
|
}
|
|
226
239
|
catch (err) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (currentTaskAbort?.signal.aborted) {
|
|
236
|
-
console.log(`[Daemon] Task ${task.id} cancelled`);
|
|
240
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
241
|
+
if (abort.signal.aborted) {
|
|
242
|
+
console.log(`[Daemon] Task ${task.id} cancelled`);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
ws.sendTaskFailed(task.id, errMsg);
|
|
246
|
+
console.error(`[Daemon] Task ${task.id} error:`, errMsg);
|
|
247
|
+
}
|
|
237
248
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
249
|
+
finally {
|
|
250
|
+
activeTasks--;
|
|
251
|
+
activeAgentTasks.delete(agentId);
|
|
252
|
+
taskAbortControllers.delete(task.id);
|
|
253
|
+
if (currentTaskId === task.id) {
|
|
254
|
+
currentTaskId = null;
|
|
255
|
+
currentTaskAbort = null;
|
|
256
|
+
}
|
|
257
|
+
pendingInputResolvers.delete(task.id);
|
|
258
|
+
ws.send({ action: 'agent_ready', agentId });
|
|
241
259
|
}
|
|
242
|
-
}
|
|
243
|
-
finally {
|
|
244
|
-
activeTasks--;
|
|
245
|
-
currentTaskId = null;
|
|
246
|
-
currentTaskAbort = null;
|
|
247
|
-
pendingInputResolvers.delete(task.id);
|
|
248
|
-
// Notify API that agent is available to pick up queued tasks
|
|
249
|
-
ws.send({ action: 'agent_ready' });
|
|
250
|
-
}
|
|
260
|
+
})();
|
|
251
261
|
break;
|
|
252
262
|
}
|
|
253
263
|
case 'task_cancelled': {
|
|
254
264
|
const taskId = msg.taskId;
|
|
255
265
|
console.log(`[Daemon] Task cancelled: ${taskId}`);
|
|
256
266
|
clearPMState(taskId);
|
|
257
|
-
|
|
258
|
-
|
|
267
|
+
const taskAbort = taskAbortControllers.get(taskId);
|
|
268
|
+
if (taskAbort) {
|
|
269
|
+
taskAbort.abort();
|
|
259
270
|
console.log(`[Daemon] Aborted running task ${taskId}`);
|
|
260
271
|
}
|
|
261
272
|
// Also resolve any pending input to unblock waitForInput
|
|
@@ -455,7 +466,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
455
466
|
else {
|
|
456
467
|
// No resolver — check if we have a persisted session to resume
|
|
457
468
|
const savedState = loadPMState(taskId);
|
|
458
|
-
if (savedState
|
|
469
|
+
if (savedState) {
|
|
459
470
|
if (savedState.mode === 'agent_team') {
|
|
460
471
|
console.log(`[Daemon] Resuming agent_team for task ${taskId} (session: ${savedState.agentTeamSessionId?.substring(0, 12) ?? 'none'})`);
|
|
461
472
|
resumeAgentTeamChat(taskId, answer, attachments, savedState, ws, pendingInputResolvers);
|
|
@@ -465,9 +476,6 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
465
476
|
resumePMChat(taskId, answer, attachments, savedState, ws, pendingInputResolvers);
|
|
466
477
|
}
|
|
467
478
|
}
|
|
468
|
-
else if (savedState) {
|
|
469
|
-
console.warn(`[Daemon] Cannot resume — agent busy (active: ${activeTasks})`);
|
|
470
|
-
}
|
|
471
479
|
else {
|
|
472
480
|
console.warn(`[Daemon] No pending input resolver for task ${taskId}`);
|
|
473
481
|
}
|
|
@@ -628,8 +636,10 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
628
636
|
*/
|
|
629
637
|
async function resumePMChat(taskId, firstMessage, firstAttachments, pmState, wsClient, resolvers) {
|
|
630
638
|
activeTasks++;
|
|
639
|
+
const abort = new AbortController();
|
|
640
|
+
taskAbortControllers.set(taskId, abort);
|
|
631
641
|
currentTaskId = taskId;
|
|
632
|
-
currentTaskAbort =
|
|
642
|
+
currentTaskAbort = abort;
|
|
633
643
|
let pmSessionId = pmState.pmSessionId;
|
|
634
644
|
const MAX_RESUMED_ROUNDS = 50;
|
|
635
645
|
try {
|
|
@@ -650,7 +660,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
650
660
|
console.log(`[Daemon] Resumed PM chat round ${round + 1}: ${userMessage.substring(0, 80)}${inputFiles?.length ? ` (+${inputFiles.length} images)` : ''}`);
|
|
651
661
|
const streamMsgId = `pm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
652
662
|
let firstChunkIso = '';
|
|
653
|
-
const chatResult = await chatWithPM(pmSessionId, pmState.repoPath, userMessage,
|
|
663
|
+
const chatResult = await chatWithPM(pmSessionId, pmState.repoPath, userMessage, abort.signal, (chunk) => {
|
|
654
664
|
if (!firstChunkIso)
|
|
655
665
|
firstChunkIso = new Date().toISOString();
|
|
656
666
|
wsClient.sendPMStream(taskId, chunk);
|
|
@@ -677,7 +687,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
677
687
|
repoPath: pmState.repoPath,
|
|
678
688
|
mode: 'tuna',
|
|
679
689
|
};
|
|
680
|
-
const result = await executePlanAndReport(task, plan, wsClient, resolvers,
|
|
690
|
+
const result = await executePlanAndReport(task, plan, wsClient, resolvers, abort.signal);
|
|
681
691
|
if (result.status === 'done' && !result.followUpMessage) {
|
|
682
692
|
const totalDuration = result.sessions.reduce((sum, s) => sum + (s.durationMs ?? 0), 0);
|
|
683
693
|
wsClient.sendTaskDone(taskId, { result: plan.summary, durationMs: totalDuration });
|
|
@@ -711,7 +721,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
711
721
|
}
|
|
712
722
|
}
|
|
713
723
|
catch (err) {
|
|
714
|
-
if (
|
|
724
|
+
if (abort.signal.aborted) {
|
|
715
725
|
console.log(`[Daemon] Resumed PM chat cancelled for task ${taskId}`);
|
|
716
726
|
}
|
|
717
727
|
else {
|
|
@@ -721,8 +731,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
721
731
|
}
|
|
722
732
|
finally {
|
|
723
733
|
activeTasks--;
|
|
724
|
-
|
|
725
|
-
|
|
734
|
+
taskAbortControllers.delete(taskId);
|
|
735
|
+
if (currentTaskId === taskId) {
|
|
736
|
+
currentTaskId = null;
|
|
737
|
+
currentTaskAbort = null;
|
|
738
|
+
}
|
|
726
739
|
resolvers.delete(taskId);
|
|
727
740
|
cleanupAttachments(taskId);
|
|
728
741
|
wsClient.send({ action: 'agent_ready' });
|
|
@@ -734,8 +747,10 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
734
747
|
*/
|
|
735
748
|
async function resumeAgentTeamChat(taskId, firstMessage, firstAttachments, savedState, wsClient, resolvers) {
|
|
736
749
|
activeTasks++;
|
|
750
|
+
const abort = new AbortController();
|
|
751
|
+
taskAbortControllers.set(taskId, abort);
|
|
737
752
|
currentTaskId = taskId;
|
|
738
|
-
currentTaskAbort =
|
|
753
|
+
currentTaskAbort = abort;
|
|
739
754
|
let sessionId = savedState.agentTeamSessionId;
|
|
740
755
|
let totalDurationMs = 0;
|
|
741
756
|
try {
|
|
@@ -770,7 +785,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
770
785
|
agentTeam: true,
|
|
771
786
|
maxTurns: 50,
|
|
772
787
|
resumeSessionId: sessionId,
|
|
773
|
-
signal:
|
|
788
|
+
signal: abort.signal,
|
|
774
789
|
inputFiles: currentInputFiles,
|
|
775
790
|
onStreamLine: (data) => {
|
|
776
791
|
if (data.type === 'stream_event') {
|
|
@@ -884,7 +899,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
884
899
|
});
|
|
885
900
|
}
|
|
886
901
|
catch (err) {
|
|
887
|
-
if (
|
|
902
|
+
if (abort.signal.aborted) {
|
|
888
903
|
console.log(`[Daemon] Resumed agent_team cancelled for task ${taskId}`);
|
|
889
904
|
}
|
|
890
905
|
else {
|
|
@@ -894,8 +909,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
894
909
|
}
|
|
895
910
|
finally {
|
|
896
911
|
activeTasks--;
|
|
897
|
-
|
|
898
|
-
|
|
912
|
+
taskAbortControllers.delete(taskId);
|
|
913
|
+
if (currentTaskId === taskId) {
|
|
914
|
+
currentTaskId = null;
|
|
915
|
+
currentTaskAbort = null;
|
|
916
|
+
}
|
|
899
917
|
resolvers.delete(taskId);
|
|
900
918
|
cleanupAttachments(taskId);
|
|
901
919
|
wsClient.send({ action: 'agent_ready' });
|