tuna-agent 0.1.165 → 0.1.167

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.
@@ -137,6 +137,7 @@ export async function startDaemon(config) {
137
137
  const pendingPermissionResolvers = new Map();
138
138
  // Track active tasks per agent (agentId → taskId)
139
139
  const activeAgentTasks = new Map();
140
+ const agentQueues = new Map();
140
141
  // Track abort controllers per task
141
142
  const taskAbortControllers = new Map();
142
143
  // Note: currentTaskId/currentTaskAbort removed — use taskAbortControllers + activeAgentTasks instead
@@ -218,41 +219,13 @@ export async function startDaemon(config) {
218
219
  if (task.repoPath?.startsWith('~/')) {
219
220
  task.repoPath = path.join(os.homedir(), task.repoPath.slice(2));
220
221
  }
221
- // Check per-agent concurrency (each agent can run 1 task at a time)
222
+ // Per-agent concurrency: if busy, queue instead of rejecting.
222
223
  const agentId = task.agentId || '__default__';
223
224
  if (activeAgentTasks.has(agentId)) {
224
- console.log(`[Daemon] Agent ${agentId} busy rejecting task ${task.id}`);
225
- ws.send({ action: 'task_rejected', taskId: task.id, reason: 'agent_busy' });
225
+ enqueueForAgent(agentId, { kind: 'task', task });
226
226
  break;
227
227
  }
228
- activeTasks++;
229
- activeAgentTasks.set(agentId, task.id);
230
- const abort = new AbortController();
231
- taskAbortControllers.set(task.id, abort);
232
- console.log(`[Daemon] Received task: ${task.id} agent=${agentId} — ${task.description.slice(0, 80)} (attachments: ${task.attachments?.length ?? 0}) [active: ${activeTasks}]`);
233
- // Run task in background (non-blocking) to allow parallel agent execution
234
- (async () => {
235
- try {
236
- await adapter.handleTask(task, ws, pendingInputResolvers, abort.signal, pendingPermissionResolvers);
237
- }
238
- catch (err) {
239
- const errMsg = err instanceof Error ? err.message : String(err);
240
- if (abort.signal.aborted) {
241
- console.log(`[Daemon] Task ${task.id} cancelled`);
242
- }
243
- else {
244
- ws.sendTaskFailed(task.id, errMsg);
245
- console.error(`[Daemon] Task ${task.id} error:`, errMsg);
246
- }
247
- }
248
- finally {
249
- activeTasks--;
250
- activeAgentTasks.delete(agentId);
251
- taskAbortControllers.delete(task.id);
252
- pendingInputResolvers.delete(task.id);
253
- ws.send({ action: 'agent_ready', agentId });
254
- }
255
- })();
228
+ runTaskNow(task);
256
229
  break;
257
230
  }
258
231
  case 'task_cancelled': {
@@ -479,19 +452,15 @@ ${skillContent.slice(0, 15000)}`;
479
452
  // No resolver — check if we have a persisted session to resume
480
453
  const savedState = loadPMState(taskId);
481
454
  if (savedState) {
482
- // Check per-agent concurrency before resuming
455
+ // Per-agent concurrency: if busy, queue the reply instead of dropping it.
483
456
  const resumeAgentId = savedState.agentId || '__default__';
484
457
  if (activeAgentTasks.has(resumeAgentId)) {
485
458
  const busyTask = activeAgentTasks.get(resumeAgentId);
486
- console.warn(`[Daemon] Cannot resume task ${taskId} agent ${resumeAgentId} is busy with task ${busyTask}`);
487
- // Notify user and revert task status so it doesn't get stuck at "executing"
459
+ console.log(`[Daemon] Agent ${resumeAgentId} busy with ${busyTask} queuing reply for task ${taskId}`);
460
+ enqueueForAgent(resumeAgentId, { kind: 'input', taskId, answer, attachments, savedState });
488
461
  ws.sendPMMessage(taskId, {
489
462
  sender: 'pm',
490
- content: 'Agent is currently busy with another task. Please try again shortly.',
491
- });
492
- ws.sendTaskDone(taskId, {
493
- result: 'Agent busy — message not processed',
494
- durationMs: 0,
463
+ content: ' Đang xử việc khác, sẽ trả lời ngay khi xong.',
495
464
  });
496
465
  break;
497
466
  }
@@ -697,6 +666,67 @@ ${skillContent.slice(0, 15000)}`;
697
666
  console.log(`[Daemon] Unknown message type: ${type}`);
698
667
  }
699
668
  }, onAuthFailed);
669
+ /** Queue work for an agent that is currently busy. */
670
+ function enqueueForAgent(agentId, item) {
671
+ let q = agentQueues.get(agentId);
672
+ if (!q) {
673
+ q = [];
674
+ agentQueues.set(agentId, q);
675
+ }
676
+ q.push(item);
677
+ console.log(`[Daemon] Agent ${agentId} busy — queued ${item.kind} (queue: ${q.length})`);
678
+ }
679
+ /** Start the next queued item for an agent, if any and the agent is free. */
680
+ function processNextForAgent(agentId) {
681
+ if (activeAgentTasks.has(agentId))
682
+ return; // still busy
683
+ const q = agentQueues.get(agentId);
684
+ if (!q || q.length === 0)
685
+ return;
686
+ const item = q.shift();
687
+ console.log(`[Daemon] Dequeue ${item.kind} for agent ${agentId} (remaining: ${q.length})`);
688
+ if (item.kind === 'task') {
689
+ runTaskNow(item.task);
690
+ }
691
+ else if (item.savedState.mode === 'agent_team') {
692
+ resumeAgentTeamChat(item.taskId, item.answer, item.attachments, item.savedState, ws, pendingInputResolvers);
693
+ }
694
+ else {
695
+ resumePMChat(item.taskId, item.answer, item.attachments, item.savedState, ws, pendingInputResolvers);
696
+ }
697
+ }
698
+ /** Run a task immediately (agent assumed free). Drains the queue when done. */
699
+ function runTaskNow(task) {
700
+ const agentId = task.agentId || '__default__';
701
+ activeTasks++;
702
+ activeAgentTasks.set(agentId, task.id);
703
+ const abort = new AbortController();
704
+ taskAbortControllers.set(task.id, abort);
705
+ console.log(`[Daemon] Received task: ${task.id} agent=${agentId} — ${task.description.slice(0, 80)} (attachments: ${task.attachments?.length ?? 0}) [active: ${activeTasks}]`);
706
+ (async () => {
707
+ try {
708
+ await adapter.handleTask(task, ws, pendingInputResolvers, abort.signal, pendingPermissionResolvers);
709
+ }
710
+ catch (err) {
711
+ const errMsg = err instanceof Error ? err.message : String(err);
712
+ if (abort.signal.aborted) {
713
+ console.log(`[Daemon] Task ${task.id} cancelled`);
714
+ }
715
+ else {
716
+ ws.sendTaskFailed(task.id, errMsg);
717
+ console.error(`[Daemon] Task ${task.id} error:`, errMsg);
718
+ }
719
+ }
720
+ finally {
721
+ activeTasks--;
722
+ activeAgentTasks.delete(agentId);
723
+ taskAbortControllers.delete(task.id);
724
+ pendingInputResolvers.delete(task.id);
725
+ ws.send({ action: 'agent_ready', agentId });
726
+ processNextForAgent(agentId);
727
+ }
728
+ })();
729
+ }
700
730
  /**
701
731
  * Resume PM chat for a task after agent restart or done task reopen.
702
732
  * Loads persisted pmSessionId and runs a chat loop.
@@ -803,6 +833,7 @@ ${skillContent.slice(0, 15000)}`;
803
833
  resolvers.delete(taskId);
804
834
  cleanupAttachments(taskId);
805
835
  wsClient.send({ action: 'agent_ready', agentId });
836
+ processNextForAgent(agentId);
806
837
  }
807
838
  }
808
839
  /**
@@ -1049,6 +1080,7 @@ ${skillContent.slice(0, 15000)}`;
1049
1080
  resolvers.delete(taskId);
1050
1081
  cleanupAttachments(taskId);
1051
1082
  wsClient.send({ action: 'agent_ready', agentId });
1083
+ processNextForAgent(agentId);
1052
1084
  }
1053
1085
  }
1054
1086
  // Wire up agent metrics to heartbeat
@@ -224,6 +224,18 @@ export class AgentWebSocketClient {
224
224
  return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
225
225
  }
226
226
  _connect() {
227
+ // Tear down any previous socket first. Without this, a reconnect could leak the
228
+ // old connection: it stays half-open on the server (readyState OPEN) and becomes
229
+ // a zombie the API keeps sending to, while this daemon listens on the new one.
230
+ if (this.ws) {
231
+ try {
232
+ this.ws.removeAllListeners();
233
+ this.ws.terminate();
234
+ }
235
+ catch { /* ignore */ }
236
+ this.ws = null;
237
+ }
238
+ this._stopHeartbeat();
227
239
  try {
228
240
  this.ws = new WebSocket(this.config.wsUrl, {
229
241
  headers: { 'Authorization': `Bearer ${this.config.agentToken}` },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.165",
3
+ "version": "0.1.167",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"