tuna-agent 0.1.175 → 0.1.177
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.
- package/dist/daemon/index.js +36 -42
- package/dist/utils/execution-helpers.js +6 -0
- package/package.json +1 -1
package/dist/daemon/index.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
225
|
-
//
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
console.log(`[Daemon]
|
|
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: '⏳
|
|
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
|
-
/**
|
|
689
|
-
function
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1086
|
+
drainQueues();
|
|
1093
1087
|
}
|
|
1094
1088
|
}
|
|
1095
1089
|
// Wire up agent metrics to heartbeat
|
|
@@ -156,6 +156,12 @@ export async function executePlanAndReport(task, plan, ws, resolvers, signal, co
|
|
|
156
156
|
if (status === 'waiting_input') {
|
|
157
157
|
return { sessions, status };
|
|
158
158
|
}
|
|
159
|
+
// Single 'self' task: the result was already delivered by onSubtaskDone (in the
|
|
160
|
+
// agent's own voice). Complete directly — no robotic "All sessions completed /
|
|
161
|
+
// continue?" wrapper. Follow-up happens naturally via a thread reply.
|
|
162
|
+
if (plan.subtasks.length <= 1) {
|
|
163
|
+
return { sessions, status };
|
|
164
|
+
}
|
|
159
165
|
const totalDuration = sessions.reduce((sum, s) => sum + (s.durationMs ?? 0), 0);
|
|
160
166
|
const durationStr = (totalDuration / 1000).toFixed(1);
|
|
161
167
|
// Announce result
|