tuna-agent 0.1.69 → 0.1.71
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;
|
|
@@ -45,6 +47,8 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
45
47
|
getMetrics(): Record<string, unknown>;
|
|
46
48
|
/** Register an agent folder path for rules parsing (called from daemon). */
|
|
47
49
|
registerAgentFolder(agentId: string, folderPath: string): void;
|
|
50
|
+
/** Get folder path for an agent by ID (returns undefined if not registered). */
|
|
51
|
+
getAgentFolder(agentId: string): string | undefined;
|
|
48
52
|
/** Seed memoryCount from Mem0 for all registered agents (called once on daemon startup). */
|
|
49
53
|
seedMemoryCounts(): Promise<void>;
|
|
50
54
|
checkHealth(): Promise<{
|
|
@@ -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;
|
|
@@ -88,6 +92,10 @@ export class ClaudeCodeAdapter {
|
|
|
88
92
|
registerAgentFolder(agentId, folderPath) {
|
|
89
93
|
this.agentFolderMap.set(agentId, folderPath);
|
|
90
94
|
}
|
|
95
|
+
/** Get folder path for an agent by ID (returns undefined if not registered). */
|
|
96
|
+
getAgentFolder(agentId) {
|
|
97
|
+
return this.agentFolderMap.get(agentId);
|
|
98
|
+
}
|
|
91
99
|
/** Seed memoryCount from Mem0 for all registered agents (called once on daemon startup). */
|
|
92
100
|
async seedMemoryCounts() {
|
|
93
101
|
for (const [agentId, folder] of this.agentFolderMap) {
|
|
@@ -360,6 +368,7 @@ export class ClaudeCodeAdapter {
|
|
|
360
368
|
savedAt: new Date().toISOString(),
|
|
361
369
|
mode: 'agent_team',
|
|
362
370
|
agentTeamSessionId: sessionId,
|
|
371
|
+
agentId: task.agentId,
|
|
363
372
|
});
|
|
364
373
|
console.log(`[ClaudeCode] Agent Team round ${round + 1} done — waiting for follow-up (60s timeout)`);
|
|
365
374
|
// Wait for follow-up message with timeout — free agent if no reply
|
|
@@ -482,7 +491,7 @@ export class ClaudeCodeAdapter {
|
|
|
482
491
|
const MAX_CHAT_ROUNDS = 50;
|
|
483
492
|
let pmSessionId = planResult.pmSessionId || '';
|
|
484
493
|
// Persist PM state for recovery after agent restart
|
|
485
|
-
savePMState({ taskId: task.id, pmSessionId, repoPath: task.repoPath, savedAt: new Date().toISOString() });
|
|
494
|
+
savePMState({ taskId: task.id, pmSessionId, repoPath: task.repoPath, savedAt: new Date().toISOString(), agentId: task.agentId });
|
|
486
495
|
// Track the latest PM message to use as context for needs_input
|
|
487
496
|
let latestPMMessage = firstMsg;
|
|
488
497
|
for (let round = 0; round < MAX_CHAT_ROUNDS; round++) {
|
|
@@ -525,7 +534,7 @@ export class ClaudeCodeAdapter {
|
|
|
525
534
|
console.log(`[ClaudeCode] ⏱ chat total: ${chatDoneMs - inputReceivedMs}ms | first chunk: ${firstChunkSentMs ? firstChunkSentMs - inputReceivedMs : 'none'}ms | chunks: ${chunkCount}`);
|
|
526
535
|
pmSessionId = chatResult.sessionId || pmSessionId;
|
|
527
536
|
// Update persisted PM session ID
|
|
528
|
-
savePMState({ taskId: task.id, pmSessionId, repoPath: task.repoPath, savedAt: new Date().toISOString() });
|
|
537
|
+
savePMState({ taskId: task.id, pmSessionId, repoPath: task.repoPath, savedAt: new Date().toISOString(), agentId: task.agentId });
|
|
529
538
|
// PM produced a plan with subtasks → break out to execution
|
|
530
539
|
if (chatResult.plan && chatResult.plan.subtasks.length > 0) {
|
|
531
540
|
planResult = { plan: chatResult.plan, pmSessionId };
|
|
@@ -775,6 +784,7 @@ export class ClaudeCodeAdapter {
|
|
|
775
784
|
pmSessionId: planResult.pmSessionId,
|
|
776
785
|
repoPath: task.repoPath,
|
|
777
786
|
savedAt: new Date().toISOString(),
|
|
787
|
+
agentId: task.agentId,
|
|
778
788
|
});
|
|
779
789
|
}
|
|
780
790
|
ws.sendTaskDone(task.id, {
|
|
@@ -810,22 +820,26 @@ export class ClaudeCodeAdapter {
|
|
|
810
820
|
const MIN_DESCRIPTION_LENGTH = 20;
|
|
811
821
|
if (task.description.length < MIN_DESCRIPTION_LENGTH)
|
|
812
822
|
return;
|
|
823
|
+
// Derive agent name from cwd (safe for parallel execution — no shared state)
|
|
824
|
+
const agentName = path.basename(cwd) || this.currentAgentName;
|
|
825
|
+
const agentId = task.agentId || this.currentAgentId;
|
|
813
826
|
try {
|
|
814
827
|
// Step 1: Generate AI-powered reflection via Ollama
|
|
815
828
|
console.log(`[Reflection] Generating AI reflection for task ${task.id} (${status}), input: ${resultSummary.substring(0, 150)}...`);
|
|
816
829
|
const { callMem0Reflect, callMem0AddMemory } = await import('../mcp/setup.js');
|
|
817
830
|
const aiReflection = await callMem0Reflect(task.description, resultSummary, status);
|
|
818
831
|
if (!aiReflection) {
|
|
819
|
-
this.
|
|
832
|
+
this.getMetricsForAgent(agentId).reflectionSkipCount++;
|
|
820
833
|
console.log(`[Reflection] No meaningful lesson — skipping storage for task ${task.id}`);
|
|
821
834
|
return;
|
|
822
835
|
}
|
|
823
836
|
// Step 2: Store the AI-generated reflection in Mem0
|
|
824
837
|
console.log(`[Reflection] Storing: "${aiReflection.substring(0, 100)}..."`);
|
|
825
|
-
await callMem0AddMemory(aiReflection,
|
|
826
|
-
this.
|
|
827
|
-
|
|
828
|
-
|
|
838
|
+
await callMem0AddMemory(aiReflection, agentName);
|
|
839
|
+
const m = this.getMetricsForAgent(agentId);
|
|
840
|
+
m.reflectionCount++;
|
|
841
|
+
m.memoryCount++;
|
|
842
|
+
m.lastReflectionAt = new Date().toISOString();
|
|
829
843
|
console.log(`[Reflection] Stored for task ${task.id}`);
|
|
830
844
|
}
|
|
831
845
|
catch (err) {
|
|
@@ -836,7 +850,7 @@ export class ClaudeCodeAdapter {
|
|
|
836
850
|
? `Task failed: "${task.description.substring(0, 150)}". Error: ${resultSummary.substring(0, 200)}`
|
|
837
851
|
: `Task completed: "${task.description.substring(0, 150)}". Result: ${resultSummary.substring(0, 200)}`;
|
|
838
852
|
const { callMem0AddMemory } = await import('../mcp/setup.js');
|
|
839
|
-
await callMem0AddMemory(fallback,
|
|
853
|
+
await callMem0AddMemory(fallback, agentName);
|
|
840
854
|
}
|
|
841
855
|
catch {
|
|
842
856
|
// Both AI and fallback failed — give up silently
|
|
@@ -859,7 +873,8 @@ export class ClaudeCodeAdapter {
|
|
|
859
873
|
try {
|
|
860
874
|
console.log(`[Rating→Mem0] Storing rating for task "${data.taskTitle}" (${data.score > 0 ? '👍' : '👎'})`);
|
|
861
875
|
const { callMem0AddMemory } = await import('../mcp/setup.js');
|
|
862
|
-
|
|
876
|
+
const agentName = path.basename(data.cwd) || this.currentAgentName;
|
|
877
|
+
await callMem0AddMemory(memoryText, agentName);
|
|
863
878
|
console.log(`[Rating→Mem0] Rating stored successfully`);
|
|
864
879
|
}
|
|
865
880
|
catch (err) {
|
|
@@ -897,7 +912,8 @@ export class ClaudeCodeAdapter {
|
|
|
897
912
|
try {
|
|
898
913
|
console.log(`[Self-Improve] Running pattern detection (every ${ClaudeCodeAdapter.PATTERN_CHECK_INTERVAL} tasks, count=${this.taskCount})`);
|
|
899
914
|
const { callMem0Patterns } = await import('../mcp/setup.js');
|
|
900
|
-
const
|
|
915
|
+
const agentName = path.basename(cwd) || this.currentAgentName;
|
|
916
|
+
const patterns = await callMem0Patterns(agentName, 2);
|
|
901
917
|
if (patterns.length === 0) {
|
|
902
918
|
console.log(`[Self-Improve] No patterns detected yet`);
|
|
903
919
|
return;
|
package/dist/daemon/index.js
CHANGED
|
@@ -134,9 +134,7 @@ export async function startDaemon(config) {
|
|
|
134
134
|
const activeAgentTasks = new Map();
|
|
135
135
|
// Track abort controllers per task
|
|
136
136
|
const taskAbortControllers = new Map();
|
|
137
|
-
//
|
|
138
|
-
let currentTaskId = null;
|
|
139
|
-
let currentTaskAbort = null;
|
|
137
|
+
// Note: currentTaskId/currentTaskAbort removed — use taskAbortControllers + activeAgentTasks instead
|
|
140
138
|
const onAuthFailed = (code, reason) => {
|
|
141
139
|
console.error(`\n[Daemon] Authentication failed (code: ${code}, reason: ${reason}).`);
|
|
142
140
|
console.error('[Daemon] Your machine token is invalid or expired.');
|
|
@@ -203,7 +201,7 @@ export async function startDaemon(config) {
|
|
|
203
201
|
}
|
|
204
202
|
// Recover orphaned tasks — pass active task IDs so API won't fail tasks we're still running
|
|
205
203
|
const activeTaskIds = Array.from(taskAbortControllers.keys());
|
|
206
|
-
ws.send({ action: 'recover_orphaned_tasks',
|
|
204
|
+
ws.send({ action: 'recover_orphaned_tasks', activeTaskIds });
|
|
207
205
|
break;
|
|
208
206
|
case 'task_assigned': {
|
|
209
207
|
const task = msg.task;
|
|
@@ -226,18 +224,7 @@ export async function startDaemon(config) {
|
|
|
226
224
|
activeAgentTasks.set(agentId, task.id);
|
|
227
225
|
const abort = new AbortController();
|
|
228
226
|
taskAbortControllers.set(task.id, abort);
|
|
229
|
-
currentTaskId = task.id;
|
|
230
|
-
currentTaskAbort = abort;
|
|
231
227
|
console.log(`[Daemon] Received task: ${task.id} agent=${agentId} — ${task.description.slice(0, 80)} (attachments: ${task.attachments?.length ?? 0}) [active: ${activeTasks}]`);
|
|
232
|
-
// Update MCP config with the task's agent ID so knowledge server uses correct attribution
|
|
233
|
-
if (task.agentId) {
|
|
234
|
-
try {
|
|
235
|
-
setupMcpConfig({ ...config, agentId: task.agentId });
|
|
236
|
-
}
|
|
237
|
-
catch (err) {
|
|
238
|
-
console.warn(`[Daemon] MCP config update for task failed (non-fatal):`, err);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
228
|
// Run task in background (non-blocking) to allow parallel agent execution
|
|
242
229
|
(async () => {
|
|
243
230
|
try {
|
|
@@ -257,10 +244,6 @@ export async function startDaemon(config) {
|
|
|
257
244
|
activeTasks--;
|
|
258
245
|
activeAgentTasks.delete(agentId);
|
|
259
246
|
taskAbortControllers.delete(task.id);
|
|
260
|
-
if (currentTaskId === task.id) {
|
|
261
|
-
currentTaskId = null;
|
|
262
|
-
currentTaskAbort = null;
|
|
263
|
-
}
|
|
264
247
|
pendingInputResolvers.delete(task.id);
|
|
265
248
|
ws.send({ action: 'agent_ready', agentId });
|
|
266
249
|
}
|
|
@@ -442,13 +425,14 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
442
425
|
// Store rating in Mem0 via adapter (fire-and-forget)
|
|
443
426
|
if (adapter.type === 'claude-code') {
|
|
444
427
|
const ccAdapter = adapter;
|
|
445
|
-
const
|
|
428
|
+
const agentFolder = agentId ? ccAdapter.getAgentFolder(agentId) : undefined;
|
|
429
|
+
const cwd = agentFolder || path.join(os.homedir(), 'tuna-workspace');
|
|
446
430
|
ccAdapter.storeRatingMemory({
|
|
447
431
|
taskTitle,
|
|
448
432
|
taskDescription,
|
|
449
433
|
score,
|
|
450
434
|
comment: comment || undefined,
|
|
451
|
-
cwd
|
|
435
|
+
cwd,
|
|
452
436
|
}).catch((err) => {
|
|
453
437
|
console.warn(`[Daemon] Rating→Mem0 failed:`, err instanceof Error ? err.message : err);
|
|
454
438
|
});
|
|
@@ -474,6 +458,13 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
474
458
|
// No resolver — check if we have a persisted session to resume
|
|
475
459
|
const savedState = loadPMState(taskId);
|
|
476
460
|
if (savedState) {
|
|
461
|
+
// Check per-agent concurrency before resuming
|
|
462
|
+
const resumeAgentId = savedState.agentId || '__default__';
|
|
463
|
+
if (activeAgentTasks.has(resumeAgentId)) {
|
|
464
|
+
console.warn(`[Daemon] Cannot resume task ${taskId} — agent ${resumeAgentId} is busy with task ${activeAgentTasks.get(resumeAgentId)}`);
|
|
465
|
+
ws.send({ action: 'task_rejected', taskId, reason: 'agent_busy' });
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
477
468
|
if (savedState.mode === 'agent_team') {
|
|
478
469
|
console.log(`[Daemon] Resuming agent_team for task ${taskId} (session: ${savedState.agentTeamSessionId?.substring(0, 12) ?? 'none'})`);
|
|
479
470
|
resumeAgentTeamChat(taskId, answer, attachments, savedState, ws, pendingInputResolvers);
|
|
@@ -642,11 +633,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
642
633
|
* Loads persisted pmSessionId and runs a chat loop.
|
|
643
634
|
*/
|
|
644
635
|
async function resumePMChat(taskId, firstMessage, firstAttachments, pmState, wsClient, resolvers) {
|
|
636
|
+
const agentId = pmState.agentId || '__default__';
|
|
645
637
|
activeTasks++;
|
|
638
|
+
activeAgentTasks.set(agentId, taskId);
|
|
646
639
|
const abort = new AbortController();
|
|
647
640
|
taskAbortControllers.set(taskId, abort);
|
|
648
|
-
currentTaskId = taskId;
|
|
649
|
-
currentTaskAbort = abort;
|
|
650
641
|
let pmSessionId = pmState.pmSessionId;
|
|
651
642
|
const MAX_RESUMED_ROUNDS = 50;
|
|
652
643
|
try {
|
|
@@ -674,7 +665,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
674
665
|
}, inputFiles);
|
|
675
666
|
wsClient.sendPMStreamEnd(taskId, streamMsgId);
|
|
676
667
|
pmSessionId = chatResult.sessionId || pmSessionId;
|
|
677
|
-
savePMState({ taskId, pmSessionId, repoPath: pmState.repoPath, savedAt: new Date().toISOString() });
|
|
668
|
+
savePMState({ taskId, pmSessionId, repoPath: pmState.repoPath, savedAt: new Date().toISOString(), agentId: pmState.agentId });
|
|
678
669
|
// PM produced a plan → execute it using shared helper
|
|
679
670
|
if (chatResult.plan && chatResult.plan.subtasks.length > 0) {
|
|
680
671
|
const plan = chatResult.plan;
|
|
@@ -738,14 +729,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
738
729
|
}
|
|
739
730
|
finally {
|
|
740
731
|
activeTasks--;
|
|
732
|
+
activeAgentTasks.delete(agentId);
|
|
741
733
|
taskAbortControllers.delete(taskId);
|
|
742
|
-
if (currentTaskId === taskId) {
|
|
743
|
-
currentTaskId = null;
|
|
744
|
-
currentTaskAbort = null;
|
|
745
|
-
}
|
|
746
734
|
resolvers.delete(taskId);
|
|
747
735
|
cleanupAttachments(taskId);
|
|
748
|
-
wsClient.send({ action: 'agent_ready' });
|
|
736
|
+
wsClient.send({ action: 'agent_ready', agentId });
|
|
749
737
|
}
|
|
750
738
|
}
|
|
751
739
|
/**
|
|
@@ -753,11 +741,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
753
741
|
* Uses saved Claude CLI session ID to resume with --resume flag.
|
|
754
742
|
*/
|
|
755
743
|
async function resumeAgentTeamChat(taskId, firstMessage, firstAttachments, savedState, wsClient, resolvers) {
|
|
744
|
+
const agentId = savedState.agentId || '__default__';
|
|
756
745
|
activeTasks++;
|
|
746
|
+
activeAgentTasks.set(agentId, taskId);
|
|
757
747
|
const abort = new AbortController();
|
|
758
748
|
taskAbortControllers.set(taskId, abort);
|
|
759
|
-
currentTaskId = taskId;
|
|
760
|
-
currentTaskAbort = abort;
|
|
761
749
|
let sessionId = savedState.agentTeamSessionId;
|
|
762
750
|
let totalDurationMs = 0;
|
|
763
751
|
try {
|
|
@@ -856,6 +844,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
856
844
|
savedAt: new Date().toISOString(),
|
|
857
845
|
mode: 'agent_team',
|
|
858
846
|
agentTeamSessionId: sessionId,
|
|
847
|
+
agentId: savedState.agentId,
|
|
859
848
|
});
|
|
860
849
|
// Wait for follow-up with timeout
|
|
861
850
|
const FOLLOW_UP_TIMEOUT_MS = 60_000;
|
|
@@ -916,14 +905,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
916
905
|
}
|
|
917
906
|
finally {
|
|
918
907
|
activeTasks--;
|
|
908
|
+
activeAgentTasks.delete(agentId);
|
|
919
909
|
taskAbortControllers.delete(taskId);
|
|
920
|
-
if (currentTaskId === taskId) {
|
|
921
|
-
currentTaskId = null;
|
|
922
|
-
currentTaskAbort = null;
|
|
923
|
-
}
|
|
924
910
|
resolvers.delete(taskId);
|
|
925
911
|
cleanupAttachments(taskId);
|
|
926
|
-
wsClient.send({ action: 'agent_ready' });
|
|
912
|
+
wsClient.send({ action: 'agent_ready', agentId });
|
|
927
913
|
}
|
|
928
914
|
}
|
|
929
915
|
// Wire up agent metrics to heartbeat
|
|
@@ -7,6 +7,8 @@ export interface PMSessionState {
|
|
|
7
7
|
mode?: 'tuna' | 'agent_team';
|
|
8
8
|
/** Claude CLI session ID for agent_team resume */
|
|
9
9
|
agentTeamSessionId?: string;
|
|
10
|
+
/** Agent ID for per-agent concurrency tracking during resume */
|
|
11
|
+
agentId?: string;
|
|
10
12
|
}
|
|
11
13
|
/** Save PM session state for a task (survives agent restart). */
|
|
12
14
|
export declare function savePMState(state: PMSessionState): void;
|