tuna-agent 0.1.68 → 0.1.69

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.
@@ -127,9 +127,14 @@ export async function startDaemon(config) {
127
127
  console.log(`[Daemon] Agent ready: ${adapter.displayName} (${health.message})`);
128
128
  }
129
129
  let activeTasks = 0;
130
- const MAX_CONCURRENT = 1;
130
+ const MAX_CONCURRENT_PER_AGENT = 1; // Each agent can run 1 task at a time
131
131
  const pendingInputResolvers = new Map();
132
132
  const pendingPermissionResolvers = new Map();
133
+ // Track active tasks per agent (agentId → taskId)
134
+ const activeAgentTasks = new Map();
135
+ // Track abort controllers per task
136
+ const taskAbortControllers = new Map();
137
+ // Legacy single-task tracking (for functions that still use it)
133
138
  let currentTaskId = null;
134
139
  let currentTaskAbort = null;
135
140
  const onAuthFailed = (code, reason) => {
@@ -196,8 +201,9 @@ export async function startDaemon(config) {
196
201
  // Seed memory counts from Mem0 so they appear on agent detail immediately
197
202
  ccAdapter.seedMemoryCounts().catch(() => { });
198
203
  }
199
- // Recover orphaned tasks — pass activeTaskId so API won't fail a task we're still running
200
- ws.send({ action: 'recover_orphaned_tasks', activeTaskId: currentTaskId ?? undefined });
204
+ // Recover orphaned tasks — pass active task IDs so API won't fail tasks we're still running
205
+ const activeTaskIds = Array.from(taskAbortControllers.keys());
206
+ ws.send({ action: 'recover_orphaned_tasks', activeTaskId: currentTaskId ?? undefined, activeTaskIds });
201
207
  break;
202
208
  case 'task_assigned': {
203
209
  const task = msg.task;
@@ -209,15 +215,20 @@ export async function startDaemon(config) {
209
215
  if (task.repoPath?.startsWith('~/')) {
210
216
  task.repoPath = path.join(os.homedir(), task.repoPath.slice(2));
211
217
  }
212
- if (activeTasks >= MAX_CONCURRENT) {
213
- console.log(`[Daemon] Busy rejecting task ${task.id}`);
218
+ // Check per-agent concurrency (each agent can run 1 task at a time)
219
+ const agentId = task.agentId || '__default__';
220
+ if (activeAgentTasks.has(agentId)) {
221
+ console.log(`[Daemon] Agent ${agentId} busy — rejecting task ${task.id}`);
214
222
  ws.send({ action: 'task_rejected', taskId: task.id, reason: 'agent_busy' });
215
223
  break;
216
224
  }
217
225
  activeTasks++;
226
+ activeAgentTasks.set(agentId, task.id);
227
+ const abort = new AbortController();
228
+ taskAbortControllers.set(task.id, abort);
218
229
  currentTaskId = task.id;
219
- currentTaskAbort = new AbortController();
220
- console.log(`[Daemon] Received task: ${task.id} agentId=${task.agentId || '(none)'} — ${task.description.slice(0, 80)} (attachments: ${task.attachments?.length ?? 0})`);
230
+ currentTaskAbort = abort;
231
+ console.log(`[Daemon] Received task: ${task.id} agent=${agentId} — ${task.description.slice(0, 80)} (attachments: ${task.attachments?.length ?? 0}) [active: ${activeTasks}]`);
221
232
  // Update MCP config with the task's agent ID so knowledge server uses correct attribution
222
233
  if (task.agentId) {
223
234
  try {
@@ -227,35 +238,42 @@ export async function startDaemon(config) {
227
238
  console.warn(`[Daemon] MCP config update for task failed (non-fatal):`, err);
228
239
  }
229
240
  }
230
- try {
231
- await adapter.handleTask(task, ws, pendingInputResolvers, currentTaskAbort.signal, pendingPermissionResolvers);
232
- }
233
- catch (err) {
234
- const errMsg = err instanceof Error ? err.message : String(err);
235
- if (currentTaskAbort?.signal.aborted) {
236
- console.log(`[Daemon] Task ${task.id} cancelled`);
241
+ // Run task in background (non-blocking) to allow parallel agent execution
242
+ (async () => {
243
+ try {
244
+ await adapter.handleTask(task, ws, pendingInputResolvers, abort.signal, pendingPermissionResolvers);
237
245
  }
238
- else {
239
- ws.sendTaskFailed(task.id, errMsg);
240
- console.error(`[Daemon] Task ${task.id} error:`, errMsg);
246
+ catch (err) {
247
+ const errMsg = err instanceof Error ? err.message : String(err);
248
+ if (abort.signal.aborted) {
249
+ console.log(`[Daemon] Task ${task.id} cancelled`);
250
+ }
251
+ else {
252
+ ws.sendTaskFailed(task.id, errMsg);
253
+ console.error(`[Daemon] Task ${task.id} error:`, errMsg);
254
+ }
241
255
  }
242
- }
243
- finally {
244
- activeTasks--;
245
- currentTaskId = null;
246
- currentTaskAbort = null;
247
- pendingInputResolvers.delete(task.id);
248
- // Notify API that agent is available to pick up queued tasks
249
- ws.send({ action: 'agent_ready' });
250
- }
256
+ finally {
257
+ activeTasks--;
258
+ activeAgentTasks.delete(agentId);
259
+ taskAbortControllers.delete(task.id);
260
+ if (currentTaskId === task.id) {
261
+ currentTaskId = null;
262
+ currentTaskAbort = null;
263
+ }
264
+ pendingInputResolvers.delete(task.id);
265
+ ws.send({ action: 'agent_ready', agentId });
266
+ }
267
+ })();
251
268
  break;
252
269
  }
253
270
  case 'task_cancelled': {
254
271
  const taskId = msg.taskId;
255
272
  console.log(`[Daemon] Task cancelled: ${taskId}`);
256
273
  clearPMState(taskId);
257
- if (currentTaskId === taskId && currentTaskAbort) {
258
- currentTaskAbort.abort();
274
+ const taskAbort = taskAbortControllers.get(taskId);
275
+ if (taskAbort) {
276
+ taskAbort.abort();
259
277
  console.log(`[Daemon] Aborted running task ${taskId}`);
260
278
  }
261
279
  // Also resolve any pending input to unblock waitForInput
@@ -455,7 +473,7 @@ ${skillContent.slice(0, 15000)}`;
455
473
  else {
456
474
  // No resolver — check if we have a persisted session to resume
457
475
  const savedState = loadPMState(taskId);
458
- if (savedState && activeTasks < MAX_CONCURRENT) {
476
+ if (savedState) {
459
477
  if (savedState.mode === 'agent_team') {
460
478
  console.log(`[Daemon] Resuming agent_team for task ${taskId} (session: ${savedState.agentTeamSessionId?.substring(0, 12) ?? 'none'})`);
461
479
  resumeAgentTeamChat(taskId, answer, attachments, savedState, ws, pendingInputResolvers);
@@ -465,9 +483,6 @@ ${skillContent.slice(0, 15000)}`;
465
483
  resumePMChat(taskId, answer, attachments, savedState, ws, pendingInputResolvers);
466
484
  }
467
485
  }
468
- else if (savedState) {
469
- console.warn(`[Daemon] Cannot resume — agent busy (active: ${activeTasks})`);
470
- }
471
486
  else {
472
487
  console.warn(`[Daemon] No pending input resolver for task ${taskId}`);
473
488
  }
@@ -628,8 +643,10 @@ ${skillContent.slice(0, 15000)}`;
628
643
  */
629
644
  async function resumePMChat(taskId, firstMessage, firstAttachments, pmState, wsClient, resolvers) {
630
645
  activeTasks++;
646
+ const abort = new AbortController();
647
+ taskAbortControllers.set(taskId, abort);
631
648
  currentTaskId = taskId;
632
- currentTaskAbort = new AbortController();
649
+ currentTaskAbort = abort;
633
650
  let pmSessionId = pmState.pmSessionId;
634
651
  const MAX_RESUMED_ROUNDS = 50;
635
652
  try {
@@ -650,7 +667,7 @@ ${skillContent.slice(0, 15000)}`;
650
667
  console.log(`[Daemon] Resumed PM chat round ${round + 1}: ${userMessage.substring(0, 80)}${inputFiles?.length ? ` (+${inputFiles.length} images)` : ''}`);
651
668
  const streamMsgId = `pm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
652
669
  let firstChunkIso = '';
653
- const chatResult = await chatWithPM(pmSessionId, pmState.repoPath, userMessage, currentTaskAbort.signal, (chunk) => {
670
+ const chatResult = await chatWithPM(pmSessionId, pmState.repoPath, userMessage, abort.signal, (chunk) => {
654
671
  if (!firstChunkIso)
655
672
  firstChunkIso = new Date().toISOString();
656
673
  wsClient.sendPMStream(taskId, chunk);
@@ -677,7 +694,7 @@ ${skillContent.slice(0, 15000)}`;
677
694
  repoPath: pmState.repoPath,
678
695
  mode: 'tuna',
679
696
  };
680
- const result = await executePlanAndReport(task, plan, wsClient, resolvers, currentTaskAbort.signal);
697
+ const result = await executePlanAndReport(task, plan, wsClient, resolvers, abort.signal);
681
698
  if (result.status === 'done' && !result.followUpMessage) {
682
699
  const totalDuration = result.sessions.reduce((sum, s) => sum + (s.durationMs ?? 0), 0);
683
700
  wsClient.sendTaskDone(taskId, { result: plan.summary, durationMs: totalDuration });
@@ -711,7 +728,7 @@ ${skillContent.slice(0, 15000)}`;
711
728
  }
712
729
  }
713
730
  catch (err) {
714
- if (currentTaskAbort?.signal.aborted) {
731
+ if (abort.signal.aborted) {
715
732
  console.log(`[Daemon] Resumed PM chat cancelled for task ${taskId}`);
716
733
  }
717
734
  else {
@@ -721,8 +738,11 @@ ${skillContent.slice(0, 15000)}`;
721
738
  }
722
739
  finally {
723
740
  activeTasks--;
724
- currentTaskId = null;
725
- currentTaskAbort = null;
741
+ taskAbortControllers.delete(taskId);
742
+ if (currentTaskId === taskId) {
743
+ currentTaskId = null;
744
+ currentTaskAbort = null;
745
+ }
726
746
  resolvers.delete(taskId);
727
747
  cleanupAttachments(taskId);
728
748
  wsClient.send({ action: 'agent_ready' });
@@ -734,8 +754,10 @@ ${skillContent.slice(0, 15000)}`;
734
754
  */
735
755
  async function resumeAgentTeamChat(taskId, firstMessage, firstAttachments, savedState, wsClient, resolvers) {
736
756
  activeTasks++;
757
+ const abort = new AbortController();
758
+ taskAbortControllers.set(taskId, abort);
737
759
  currentTaskId = taskId;
738
- currentTaskAbort = new AbortController();
760
+ currentTaskAbort = abort;
739
761
  let sessionId = savedState.agentTeamSessionId;
740
762
  let totalDurationMs = 0;
741
763
  try {
@@ -770,7 +792,7 @@ ${skillContent.slice(0, 15000)}`;
770
792
  agentTeam: true,
771
793
  maxTurns: 50,
772
794
  resumeSessionId: sessionId,
773
- signal: currentTaskAbort.signal,
795
+ signal: abort.signal,
774
796
  inputFiles: currentInputFiles,
775
797
  onStreamLine: (data) => {
776
798
  if (data.type === 'stream_event') {
@@ -884,7 +906,7 @@ ${skillContent.slice(0, 15000)}`;
884
906
  });
885
907
  }
886
908
  catch (err) {
887
- if (currentTaskAbort?.signal.aborted) {
909
+ if (abort.signal.aborted) {
888
910
  console.log(`[Daemon] Resumed agent_team cancelled for task ${taskId}`);
889
911
  }
890
912
  else {
@@ -894,8 +916,11 @@ ${skillContent.slice(0, 15000)}`;
894
916
  }
895
917
  finally {
896
918
  activeTasks--;
897
- currentTaskId = null;
898
- currentTaskAbort = null;
919
+ taskAbortControllers.delete(taskId);
920
+ if (currentTaskId === taskId) {
921
+ currentTaskId = null;
922
+ currentTaskAbort = null;
923
+ }
899
924
  resolvers.delete(taskId);
900
925
  cleanupAttachments(taskId);
901
926
  wsClient.send({ action: 'agent_ready' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"