tuna-agent 0.1.175 → 0.1.176

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.
@@ -132,15 +132,15 @@ export async function startDaemon(config) {
132
132
  console.log(`[Daemon] Agent ready: ${adapter.displayName} (${health.message})`);
133
133
  }
134
134
  let activeTasks = 0;
135
- const MAX_CONCURRENT_PER_AGENT = 1; // Each agent can run 1 task at a time
135
+ // Concurrency is capped machine-wide, NOT per agent multiple claude sessions in
136
+ // the same agent folder are fine (subtasks already run in parallel there). A single
137
+ // waiting/slow session no longer blocks other sessions.
138
+ const MAX_CONCURRENT = 5;
136
139
  const pendingInputResolvers = new Map();
137
140
  const pendingPermissionResolvers = new Map();
138
- // Track active tasks per agent (agentId → taskId)
139
- const activeAgentTasks = new Map();
140
141
  const agentQueues = new Map();
141
142
  // Track abort controllers per task
142
143
  const taskAbortControllers = new Map();
143
- // Note: currentTaskId/currentTaskAbort removed — use taskAbortControllers + activeAgentTasks instead
144
144
  const onAuthFailed = (code, reason) => {
145
145
  console.error(`\n[Daemon] Authentication failed (code: ${code}, reason: ${reason}).`);
146
146
  console.error('[Daemon] Your machine token is invalid or expired.');
@@ -219,18 +219,18 @@ export async function startDaemon(config) {
219
219
  if (task.repoPath?.startsWith('~/')) {
220
220
  task.repoPath = path.join(os.homedir(), task.repoPath.slice(2));
221
221
  }
222
- // Per-agent concurrency: if busy, queue instead of rejecting.
223
222
  const agentId = task.agentId || '__default__';
224
- // Dedup: the API sweep re-dispatches queued tasks every 30s. Without this,
225
- // a task sent repeatedly while the agent is busy piles up as duplicates in
226
- // the queue (and would run N times). Ignore if already running or queued.
227
- const alreadyRunning = Array.from(activeAgentTasks.values()).includes(task.id);
223
+ // Dedup: the API sweep re-dispatches queued tasks every 30s. Ignore if this
224
+ // task is already running (has an abort controller) or already queued.
225
+ const alreadyRunning = taskAbortControllers.has(task.id);
228
226
  const alreadyQueued = (agentQueues.get(agentId) || []).some((it) => it.kind === 'task' && it.task.id === task.id);
229
227
  if (alreadyRunning || alreadyQueued) {
230
228
  console.log(`[Daemon] Task ${task.id} already running/queued — ignoring duplicate dispatch`);
231
229
  break;
232
230
  }
233
- if (activeAgentTasks.has(agentId)) {
231
+ // Concurrency cap is machine-wide (not per agent). Only queue when the whole
232
+ // machine is saturated — otherwise run concurrently.
233
+ if (activeTasks >= MAX_CONCURRENT) {
234
234
  enqueueForAgent(agentId, { kind: 'task', task });
235
235
  break;
236
236
  }
@@ -461,15 +461,14 @@ ${skillContent.slice(0, 15000)}`;
461
461
  // No resolver — check if we have a persisted session to resume
462
462
  const savedState = loadPMState(taskId);
463
463
  if (savedState) {
464
- // Per-agent concurrency: if busy, queue the reply instead of dropping it.
465
464
  const resumeAgentId = savedState.agentId || '__default__';
466
- if (activeAgentTasks.has(resumeAgentId)) {
467
- const busyTask = activeAgentTasks.get(resumeAgentId);
468
- console.log(`[Daemon] Agent ${resumeAgentId} busy with ${busyTask} — queuing reply for task ${taskId}`);
465
+ // Only queue when the machine is saturated — otherwise resume concurrently.
466
+ if (activeTasks >= MAX_CONCURRENT) {
467
+ console.log(`[Daemon] Machine at capacity (${activeTasks}) — queuing reply for task ${taskId}`);
469
468
  enqueueForAgent(resumeAgentId, { kind: 'input', taskId, answer, attachments, savedState });
470
469
  ws.sendPMMessage(taskId, {
471
470
  sender: 'pm',
472
- content: '⏳ Đang xử lý việc khác, sẽ trả lời ngay khi xong.',
471
+ content: '⏳ Nhiều việc quá, em xử lý xong việc đang làm rồi quay lại ngay nha anh.',
473
472
  });
474
473
  break;
475
474
  }
@@ -685,30 +684,30 @@ ${skillContent.slice(0, 15000)}`;
685
684
  q.push(item);
686
685
  console.log(`[Daemon] Agent ${agentId} busy — queued ${item.kind} (queue: ${q.length})`);
687
686
  }
688
- /** Start the next queued item for an agent, if any and the agent is free. */
689
- function processNextForAgent(agentId) {
690
- if (activeAgentTasks.has(agentId))
691
- return; // still busy
692
- const q = agentQueues.get(agentId);
693
- if (!q || q.length === 0)
694
- return;
695
- const item = q.shift();
696
- console.log(`[Daemon] Dequeue ${item.kind} for agent ${agentId} (remaining: ${q.length})`);
697
- if (item.kind === 'task') {
698
- runTaskNow(item.task);
699
- }
700
- else if (item.savedState.mode === 'agent_team') {
701
- resumeAgentTeamChat(item.taskId, item.answer, item.attachments, item.savedState, ws, pendingInputResolvers);
702
- }
703
- else {
704
- resumePMChat(item.taskId, item.answer, item.attachments, item.savedState, ws, pendingInputResolvers);
687
+ /** Drain queued work (any agent) while below the machine-wide concurrency cap. */
688
+ function drainQueues() {
689
+ for (const [, q] of agentQueues) {
690
+ while (q.length && activeTasks < MAX_CONCURRENT) {
691
+ const item = q.shift();
692
+ console.log(`[Daemon] Dequeue ${item.kind} (remaining: ${q.length})`);
693
+ if (item.kind === 'task') {
694
+ runTaskNow(item.task);
695
+ }
696
+ else if (item.savedState.mode === 'agent_team') {
697
+ resumeAgentTeamChat(item.taskId, item.answer, item.attachments, item.savedState, ws, pendingInputResolvers);
698
+ }
699
+ else {
700
+ resumePMChat(item.taskId, item.answer, item.attachments, item.savedState, ws, pendingInputResolvers);
701
+ }
702
+ }
703
+ if (activeTasks >= MAX_CONCURRENT)
704
+ break;
705
705
  }
706
706
  }
707
- /** Run a task immediately (agent assumed free). Drains the queue when done. */
707
+ /** Run a task now. Drains the queue when it finishes. */
708
708
  function runTaskNow(task) {
709
709
  const agentId = task.agentId || '__default__';
710
710
  activeTasks++;
711
- activeAgentTasks.set(agentId, task.id);
712
711
  const abort = new AbortController();
713
712
  taskAbortControllers.set(task.id, abort);
714
713
  console.log(`[Daemon] Received task: ${task.id} agent=${agentId} — ${task.description.slice(0, 80)} (attachments: ${task.attachments?.length ?? 0}) [active: ${activeTasks}]`);
@@ -728,11 +727,10 @@ ${skillContent.slice(0, 15000)}`;
728
727
  }
729
728
  finally {
730
729
  activeTasks--;
731
- activeAgentTasks.delete(agentId);
732
730
  taskAbortControllers.delete(task.id);
733
731
  pendingInputResolvers.delete(task.id);
734
732
  ws.send({ action: 'agent_ready', agentId });
735
- processNextForAgent(agentId);
733
+ drainQueues();
736
734
  }
737
735
  })();
738
736
  }
@@ -743,7 +741,6 @@ ${skillContent.slice(0, 15000)}`;
743
741
  async function resumePMChat(taskId, firstMessage, firstAttachments, pmState, wsClient, resolvers) {
744
742
  const agentId = pmState.agentId || '__default__';
745
743
  activeTasks++;
746
- activeAgentTasks.set(agentId, taskId);
747
744
  const abort = new AbortController();
748
745
  taskAbortControllers.set(taskId, abort);
749
746
  let pmSessionId = pmState.pmSessionId;
@@ -837,12 +834,11 @@ ${skillContent.slice(0, 15000)}`;
837
834
  }
838
835
  finally {
839
836
  activeTasks--;
840
- activeAgentTasks.delete(agentId);
841
837
  taskAbortControllers.delete(taskId);
842
838
  resolvers.delete(taskId);
843
839
  cleanupAttachments(taskId);
844
840
  wsClient.send({ action: 'agent_ready', agentId });
845
- processNextForAgent(agentId);
841
+ drainQueues();
846
842
  }
847
843
  }
848
844
  /**
@@ -852,7 +848,6 @@ ${skillContent.slice(0, 15000)}`;
852
848
  async function resumeAgentTeamChat(taskId, firstMessage, firstAttachments, savedState, wsClient, resolvers) {
853
849
  const agentId = savedState.agentId || '__default__';
854
850
  activeTasks++;
855
- activeAgentTasks.set(agentId, taskId);
856
851
  const abort = new AbortController();
857
852
  taskAbortControllers.set(taskId, abort);
858
853
  let sessionId = savedState.agentTeamSessionId;
@@ -1084,12 +1079,11 @@ ${skillContent.slice(0, 15000)}`;
1084
1079
  }
1085
1080
  finally {
1086
1081
  activeTasks--;
1087
- activeAgentTasks.delete(agentId);
1088
1082
  taskAbortControllers.delete(taskId);
1089
1083
  resolvers.delete(taskId);
1090
1084
  cleanupAttachments(taskId);
1091
1085
  wsClient.send({ action: 'agent_ready', agentId });
1092
- processNextForAgent(agentId);
1086
+ drainQueues();
1093
1087
  }
1094
1088
  }
1095
1089
  // Wire up agent metrics to heartbeat
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.175",
3
+ "version": "0.1.176",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"