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
- if (!this.metricsMap.has(this.currentAgentId)) {
27
- this.metricsMap.set(this.currentAgentId, {
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(this.currentAgentId);
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.metrics.reflectionSkipCount++;
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, this.currentAgentName);
826
- this.metrics.reflectionCount++;
827
- this.metrics.memoryCount++;
828
- this.metrics.lastReflectionAt = new Date().toISOString();
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, this.currentAgentName);
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
- await callMem0AddMemory(memoryText, this.currentAgentName);
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 patterns = await callMem0Patterns(this.currentAgentName, 2);
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;
@@ -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,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 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');
446
430
  ccAdapter.storeRatingMemory({
447
431
  taskTitle,
448
432
  taskDescription,
449
433
  score,
450
434
  comment: comment || undefined,
451
- cwd: defaultCwd,
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.69",
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"