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, {
@@ -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
- // Legacy single-task tracking (for functions that still use it)
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', activeTaskId: currentTaskId ?? undefined, activeTaskIds });
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 defaultCwd = path.join(os.homedir(), 'tuna-workspace');
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: defaultCwd,
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"