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.
- package/dist/daemon/index.js +68 -43
- package/package.json +1 -1
package/dist/daemon/index.js
CHANGED
|
@@ -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
|
|
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
|
|
200
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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 =
|
|
220
|
-
console.log(`[Daemon] Received task: ${task.id}
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
725
|
-
|
|
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 =
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
898
|
-
|
|
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' });
|