tuna-agent 0.1.70 → 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.
|
@@ -47,6 +47,8 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
47
47
|
getMetrics(): Record<string, unknown>;
|
|
48
48
|
/** Register an agent folder path for rules parsing (called from daemon). */
|
|
49
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;
|
|
50
52
|
/** Seed memoryCount from Mem0 for all registered agents (called once on daemon startup). */
|
|
51
53
|
seedMemoryCounts(): Promise<void>;
|
|
52
54
|
checkHealth(): Promise<{
|
|
@@ -92,6 +92,10 @@ export class ClaudeCodeAdapter {
|
|
|
92
92
|
registerAgentFolder(agentId, folderPath) {
|
|
93
93
|
this.agentFolderMap.set(agentId, folderPath);
|
|
94
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
|
+
}
|
|
95
99
|
/** Seed memoryCount from Mem0 for all registered agents (called once on daemon startup). */
|
|
96
100
|
async seedMemoryCounts() {
|
|
97
101
|
for (const [agentId, folder] of this.agentFolderMap) {
|
|
@@ -364,6 +368,7 @@ export class ClaudeCodeAdapter {
|
|
|
364
368
|
savedAt: new Date().toISOString(),
|
|
365
369
|
mode: 'agent_team',
|
|
366
370
|
agentTeamSessionId: sessionId,
|
|
371
|
+
agentId: task.agentId,
|
|
367
372
|
});
|
|
368
373
|
console.log(`[ClaudeCode] Agent Team round ${round + 1} done — waiting for follow-up (60s timeout)`);
|
|
369
374
|
// Wait for follow-up message with timeout — free agent if no reply
|
|
@@ -486,7 +491,7 @@ export class ClaudeCodeAdapter {
|
|
|
486
491
|
const MAX_CHAT_ROUNDS = 50;
|
|
487
492
|
let pmSessionId = planResult.pmSessionId || '';
|
|
488
493
|
// Persist PM state for recovery after agent restart
|
|
489
|
-
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 });
|
|
490
495
|
// Track the latest PM message to use as context for needs_input
|
|
491
496
|
let latestPMMessage = firstMsg;
|
|
492
497
|
for (let round = 0; round < MAX_CHAT_ROUNDS; round++) {
|
|
@@ -529,7 +534,7 @@ export class ClaudeCodeAdapter {
|
|
|
529
534
|
console.log(`[ClaudeCode] ⏱ chat total: ${chatDoneMs - inputReceivedMs}ms | first chunk: ${firstChunkSentMs ? firstChunkSentMs - inputReceivedMs : 'none'}ms | chunks: ${chunkCount}`);
|
|
530
535
|
pmSessionId = chatResult.sessionId || pmSessionId;
|
|
531
536
|
// Update persisted PM session ID
|
|
532
|
-
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 });
|
|
533
538
|
// PM produced a plan with subtasks → break out to execution
|
|
534
539
|
if (chatResult.plan && chatResult.plan.subtasks.length > 0) {
|
|
535
540
|
planResult = { plan: chatResult.plan, pmSessionId };
|
|
@@ -779,6 +784,7 @@ export class ClaudeCodeAdapter {
|
|
|
779
784
|
pmSessionId: planResult.pmSessionId,
|
|
780
785
|
repoPath: task.repoPath,
|
|
781
786
|
savedAt: new Date().toISOString(),
|
|
787
|
+
agentId: task.agentId,
|
|
782
788
|
});
|
|
783
789
|
}
|
|
784
790
|
ws.sendTaskDone(task.id, {
|
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,11 +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
|
-
// MCP config per-agent is handled by writeAgentFolderMcpConfig in adapter
|
|
233
|
-
// (writes per-agent .mcp.json in agent folder — safe for parallel execution)
|
|
234
228
|
// Run task in background (non-blocking) to allow parallel agent execution
|
|
235
229
|
(async () => {
|
|
236
230
|
try {
|
|
@@ -250,10 +244,6 @@ export async function startDaemon(config) {
|
|
|
250
244
|
activeTasks--;
|
|
251
245
|
activeAgentTasks.delete(agentId);
|
|
252
246
|
taskAbortControllers.delete(task.id);
|
|
253
|
-
if (currentTaskId === task.id) {
|
|
254
|
-
currentTaskId = null;
|
|
255
|
-
currentTaskAbort = null;
|
|
256
|
-
}
|
|
257
247
|
pendingInputResolvers.delete(task.id);
|
|
258
248
|
ws.send({ action: 'agent_ready', agentId });
|
|
259
249
|
}
|
|
@@ -435,13 +425,14 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
435
425
|
// Store rating in Mem0 via adapter (fire-and-forget)
|
|
436
426
|
if (adapter.type === 'claude-code') {
|
|
437
427
|
const ccAdapter = adapter;
|
|
438
|
-
const
|
|
428
|
+
const agentFolder = agentId ? ccAdapter.getAgentFolder(agentId) : undefined;
|
|
429
|
+
const cwd = agentFolder || path.join(os.homedir(), 'tuna-workspace');
|
|
439
430
|
ccAdapter.storeRatingMemory({
|
|
440
431
|
taskTitle,
|
|
441
432
|
taskDescription,
|
|
442
433
|
score,
|
|
443
434
|
comment: comment || undefined,
|
|
444
|
-
cwd
|
|
435
|
+
cwd,
|
|
445
436
|
}).catch((err) => {
|
|
446
437
|
console.warn(`[Daemon] Rating→Mem0 failed:`, err instanceof Error ? err.message : err);
|
|
447
438
|
});
|
|
@@ -467,6 +458,13 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
467
458
|
// No resolver — check if we have a persisted session to resume
|
|
468
459
|
const savedState = loadPMState(taskId);
|
|
469
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
|
+
}
|
|
470
468
|
if (savedState.mode === 'agent_team') {
|
|
471
469
|
console.log(`[Daemon] Resuming agent_team for task ${taskId} (session: ${savedState.agentTeamSessionId?.substring(0, 12) ?? 'none'})`);
|
|
472
470
|
resumeAgentTeamChat(taskId, answer, attachments, savedState, ws, pendingInputResolvers);
|
|
@@ -635,11 +633,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
635
633
|
* Loads persisted pmSessionId and runs a chat loop.
|
|
636
634
|
*/
|
|
637
635
|
async function resumePMChat(taskId, firstMessage, firstAttachments, pmState, wsClient, resolvers) {
|
|
636
|
+
const agentId = pmState.agentId || '__default__';
|
|
638
637
|
activeTasks++;
|
|
638
|
+
activeAgentTasks.set(agentId, taskId);
|
|
639
639
|
const abort = new AbortController();
|
|
640
640
|
taskAbortControllers.set(taskId, abort);
|
|
641
|
-
currentTaskId = taskId;
|
|
642
|
-
currentTaskAbort = abort;
|
|
643
641
|
let pmSessionId = pmState.pmSessionId;
|
|
644
642
|
const MAX_RESUMED_ROUNDS = 50;
|
|
645
643
|
try {
|
|
@@ -667,7 +665,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
667
665
|
}, inputFiles);
|
|
668
666
|
wsClient.sendPMStreamEnd(taskId, streamMsgId);
|
|
669
667
|
pmSessionId = chatResult.sessionId || pmSessionId;
|
|
670
|
-
savePMState({ taskId, pmSessionId, repoPath: pmState.repoPath, savedAt: new Date().toISOString() });
|
|
668
|
+
savePMState({ taskId, pmSessionId, repoPath: pmState.repoPath, savedAt: new Date().toISOString(), agentId: pmState.agentId });
|
|
671
669
|
// PM produced a plan → execute it using shared helper
|
|
672
670
|
if (chatResult.plan && chatResult.plan.subtasks.length > 0) {
|
|
673
671
|
const plan = chatResult.plan;
|
|
@@ -731,14 +729,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
731
729
|
}
|
|
732
730
|
finally {
|
|
733
731
|
activeTasks--;
|
|
732
|
+
activeAgentTasks.delete(agentId);
|
|
734
733
|
taskAbortControllers.delete(taskId);
|
|
735
|
-
if (currentTaskId === taskId) {
|
|
736
|
-
currentTaskId = null;
|
|
737
|
-
currentTaskAbort = null;
|
|
738
|
-
}
|
|
739
734
|
resolvers.delete(taskId);
|
|
740
735
|
cleanupAttachments(taskId);
|
|
741
|
-
wsClient.send({ action: 'agent_ready' });
|
|
736
|
+
wsClient.send({ action: 'agent_ready', agentId });
|
|
742
737
|
}
|
|
743
738
|
}
|
|
744
739
|
/**
|
|
@@ -746,11 +741,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
746
741
|
* Uses saved Claude CLI session ID to resume with --resume flag.
|
|
747
742
|
*/
|
|
748
743
|
async function resumeAgentTeamChat(taskId, firstMessage, firstAttachments, savedState, wsClient, resolvers) {
|
|
744
|
+
const agentId = savedState.agentId || '__default__';
|
|
749
745
|
activeTasks++;
|
|
746
|
+
activeAgentTasks.set(agentId, taskId);
|
|
750
747
|
const abort = new AbortController();
|
|
751
748
|
taskAbortControllers.set(taskId, abort);
|
|
752
|
-
currentTaskId = taskId;
|
|
753
|
-
currentTaskAbort = abort;
|
|
754
749
|
let sessionId = savedState.agentTeamSessionId;
|
|
755
750
|
let totalDurationMs = 0;
|
|
756
751
|
try {
|
|
@@ -849,6 +844,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
849
844
|
savedAt: new Date().toISOString(),
|
|
850
845
|
mode: 'agent_team',
|
|
851
846
|
agentTeamSessionId: sessionId,
|
|
847
|
+
agentId: savedState.agentId,
|
|
852
848
|
});
|
|
853
849
|
// Wait for follow-up with timeout
|
|
854
850
|
const FOLLOW_UP_TIMEOUT_MS = 60_000;
|
|
@@ -909,14 +905,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
909
905
|
}
|
|
910
906
|
finally {
|
|
911
907
|
activeTasks--;
|
|
908
|
+
activeAgentTasks.delete(agentId);
|
|
912
909
|
taskAbortControllers.delete(taskId);
|
|
913
|
-
if (currentTaskId === taskId) {
|
|
914
|
-
currentTaskId = null;
|
|
915
|
-
currentTaskAbort = null;
|
|
916
|
-
}
|
|
917
910
|
resolvers.delete(taskId);
|
|
918
911
|
cleanupAttachments(taskId);
|
|
919
|
-
wsClient.send({ action: 'agent_ready' });
|
|
912
|
+
wsClient.send({ action: 'agent_ready', agentId });
|
|
920
913
|
}
|
|
921
914
|
}
|
|
922
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;
|