tuna-agent 0.1.70 → 0.1.72
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.
|
@@ -47,6 +47,8 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
47
47
|
getMetrics(): Record<string, unknown>;
|
|
48
48
|
/** Register an agent folder path for rules parsing (called from daemon). */
|
|
49
49
|
registerAgentFolder(agentId: string, folderPath: string): void;
|
|
50
|
+
/** Get folder path for an agent by ID (returns undefined if not registered). */
|
|
51
|
+
getAgentFolder(agentId: string): string | undefined;
|
|
50
52
|
/** Seed memoryCount from Mem0 for all registered agents (called once on daemon startup). */
|
|
51
53
|
seedMemoryCounts(): Promise<void>;
|
|
52
54
|
checkHealth(): Promise<{
|
|
@@ -77,14 +79,14 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
77
79
|
private static extractKeyPhrases;
|
|
78
80
|
/** Check if two rules are semantically similar (>40% key phrase overlap). */
|
|
79
81
|
private static isSimilarRule;
|
|
80
|
-
runSelfImprovement(cwd: string): Promise<void>;
|
|
82
|
+
runSelfImprovement(cwd: string, agentId?: string): Promise<void>;
|
|
81
83
|
/**
|
|
82
84
|
* Parse "## Learned Rules" section from CLAUDE.md and store in learnedRulesMap.
|
|
83
85
|
* Rule format: `- Rule text (confidence: 0.7)`
|
|
84
86
|
*/
|
|
85
87
|
private parseLearnedRules;
|
|
86
88
|
/** Track task completion metrics (public for daemon resume path). */
|
|
87
|
-
trackMetricsPublic(status: 'done' | 'failed', durationMs: number): void;
|
|
89
|
+
trackMetricsPublic(status: 'done' | 'failed', durationMs: number, agentId?: string): void;
|
|
88
90
|
/** Track task completion metrics. */
|
|
89
91
|
private trackMetrics;
|
|
90
92
|
dispose(): Promise<void>;
|
|
@@ -92,6 +92,10 @@ export class ClaudeCodeAdapter {
|
|
|
92
92
|
registerAgentFolder(agentId, folderPath) {
|
|
93
93
|
this.agentFolderMap.set(agentId, folderPath);
|
|
94
94
|
}
|
|
95
|
+
/** Get folder path for an agent by ID (returns undefined if not registered). */
|
|
96
|
+
getAgentFolder(agentId) {
|
|
97
|
+
return this.agentFolderMap.get(agentId);
|
|
98
|
+
}
|
|
95
99
|
/** Seed memoryCount from Mem0 for all registered agents (called once on daemon startup). */
|
|
96
100
|
async seedMemoryCounts() {
|
|
97
101
|
for (const [agentId, folder] of this.agentFolderMap) {
|
|
@@ -145,13 +149,16 @@ export class ClaudeCodeAdapter {
|
|
|
145
149
|
: undefined;
|
|
146
150
|
// Default mode: direct chat with Claude CLI (no PM layer)
|
|
147
151
|
// Only use PM planning when mode is explicitly 'tuna'
|
|
148
|
-
//
|
|
149
|
-
|
|
152
|
+
// Capture agent context as LOCAL variables for parallel-safety
|
|
153
|
+
const localAgentId = task.agentId || '';
|
|
150
154
|
const defaultWorkspaceEarly = path.join(os.homedir(), 'tuna-workspace');
|
|
151
|
-
|
|
155
|
+
const localAgentName = path.basename(task.repoPath || defaultWorkspaceEarly);
|
|
156
|
+
// Also set instance vars for backward compat (metrics getter, heartbeat, etc.)
|
|
157
|
+
this.currentAgentId = localAgentId;
|
|
158
|
+
this.currentAgentName = localAgentName;
|
|
152
159
|
// Track agent folder for rules parsing in heartbeat
|
|
153
160
|
const cwd = task.repoPath || defaultWorkspaceEarly;
|
|
154
|
-
this.agentFolderMap.set(
|
|
161
|
+
this.agentFolderMap.set(localAgentId, cwd);
|
|
155
162
|
if (task.mode !== 'tuna') {
|
|
156
163
|
console.log(`[ClaudeCode] Agent Team mode — direct chat with Claude CLI`);
|
|
157
164
|
ws.sendProgress(task.id, 'executing', { startedAt: new Date().toISOString() });
|
|
@@ -175,7 +182,7 @@ export class ClaudeCodeAdapter {
|
|
|
175
182
|
if (process.env.MEM0_SSH_HOST && task.description.length >= 20) {
|
|
176
183
|
try {
|
|
177
184
|
const { callMem0SearchMemory } = await import('../mcp/setup.js');
|
|
178
|
-
const memories = await callMem0SearchMemory(task.description,
|
|
185
|
+
const memories = await callMem0SearchMemory(task.description, localAgentName, 5);
|
|
179
186
|
if (memories.length > 0) {
|
|
180
187
|
const memoryContext = memories.map(m => `- ${m}`).join('\n');
|
|
181
188
|
userMessage = `${task.description}\n\n<past_learnings>\nRelevant lessons from previous tasks:\n${memoryContext}\n</past_learnings>`;
|
|
@@ -203,10 +210,11 @@ export class ClaudeCodeAdapter {
|
|
|
203
210
|
if (round === 0) {
|
|
204
211
|
writeAgentFolderMcpConfig(cwd, this.agentConfig);
|
|
205
212
|
// Seed memoryCount from Mem0 so it survives daemon restarts (non-blocking)
|
|
206
|
-
fetchMem0Count(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
213
|
+
fetchMem0Count(localAgentName).then(count => {
|
|
214
|
+
const m = this.getMetricsForAgent(localAgentId);
|
|
215
|
+
if (count > m.memoryCount) {
|
|
216
|
+
m.memoryCount = count;
|
|
217
|
+
console.log(`[Metrics] Seeded memoryCount=${count} from Mem0 for "${localAgentName}"`);
|
|
210
218
|
}
|
|
211
219
|
}).catch(() => { });
|
|
212
220
|
}
|
|
@@ -324,7 +332,7 @@ export class ClaudeCodeAdapter {
|
|
|
324
332
|
startedAt: firstChunkIso || undefined,
|
|
325
333
|
});
|
|
326
334
|
ws.sendTaskFailed(task.id, result.result);
|
|
327
|
-
this.trackMetrics('failed', totalDurationMs);
|
|
335
|
+
this.trackMetrics('failed', totalDurationMs, localAgentId);
|
|
328
336
|
console.log(`[ClaudeCode] Agent Team task ${task.id} failed in round ${round + 1}`);
|
|
329
337
|
this.runReflection(task, result.result, 'failed', cwd).catch(() => { });
|
|
330
338
|
return;
|
|
@@ -364,6 +372,7 @@ export class ClaudeCodeAdapter {
|
|
|
364
372
|
savedAt: new Date().toISOString(),
|
|
365
373
|
mode: 'agent_team',
|
|
366
374
|
agentTeamSessionId: sessionId,
|
|
375
|
+
agentId: task.agentId,
|
|
367
376
|
});
|
|
368
377
|
console.log(`[ClaudeCode] Agent Team round ${round + 1} done — waiting for follow-up (60s timeout)`);
|
|
369
378
|
// Wait for follow-up message with timeout — free agent if no reply
|
|
@@ -384,10 +393,10 @@ export class ClaudeCodeAdapter {
|
|
|
384
393
|
durationMs: totalDurationMs,
|
|
385
394
|
sessionId,
|
|
386
395
|
});
|
|
387
|
-
this.trackMetrics('done', totalDurationMs);
|
|
396
|
+
this.trackMetrics('done', totalDurationMs, localAgentId);
|
|
388
397
|
const timeoutOutput = lastTaskOutput || 'Task completed (no follow-up)';
|
|
389
398
|
this.runReflection(task, timeoutOutput, 'done', task.repoPath)
|
|
390
|
-
.then(() => this.runSelfImprovement(task.repoPath))
|
|
399
|
+
.then(() => this.runSelfImprovement(task.repoPath, task.agentId))
|
|
391
400
|
.catch(() => { });
|
|
392
401
|
return;
|
|
393
402
|
}
|
|
@@ -418,11 +427,11 @@ export class ClaudeCodeAdapter {
|
|
|
418
427
|
});
|
|
419
428
|
await new Promise(resolve => setTimeout(resolve, 150));
|
|
420
429
|
ws.sendPMMessage(task.id, { sender: 'pm', content: 'Task completed.' });
|
|
421
|
-
this.trackMetrics('done', totalDurationMs);
|
|
430
|
+
this.trackMetrics('done', totalDurationMs, localAgentId);
|
|
422
431
|
console.log(`[ClaudeCode] Agent Team task ${task.id} completed (${(totalDurationMs / 1000).toFixed(1)}s)`);
|
|
423
432
|
// Post-task reflection with actual output (non-blocking)
|
|
424
433
|
this.runReflection(task, lastTaskOutput || 'Task completed without text output', 'done', task.repoPath)
|
|
425
|
-
.then(() => this.runSelfImprovement(task.repoPath))
|
|
434
|
+
.then(() => this.runSelfImprovement(task.repoPath, task.agentId))
|
|
426
435
|
.catch(() => { });
|
|
427
436
|
}
|
|
428
437
|
finally {
|
|
@@ -486,7 +495,7 @@ export class ClaudeCodeAdapter {
|
|
|
486
495
|
const MAX_CHAT_ROUNDS = 50;
|
|
487
496
|
let pmSessionId = planResult.pmSessionId || '';
|
|
488
497
|
// Persist PM state for recovery after agent restart
|
|
489
|
-
savePMState({ taskId: task.id, pmSessionId, repoPath: task.repoPath, savedAt: new Date().toISOString() });
|
|
498
|
+
savePMState({ taskId: task.id, pmSessionId, repoPath: task.repoPath, savedAt: new Date().toISOString(), agentId: task.agentId });
|
|
490
499
|
// Track the latest PM message to use as context for needs_input
|
|
491
500
|
let latestPMMessage = firstMsg;
|
|
492
501
|
for (let round = 0; round < MAX_CHAT_ROUNDS; round++) {
|
|
@@ -529,7 +538,7 @@ export class ClaudeCodeAdapter {
|
|
|
529
538
|
console.log(`[ClaudeCode] ⏱ chat total: ${chatDoneMs - inputReceivedMs}ms | first chunk: ${firstChunkSentMs ? firstChunkSentMs - inputReceivedMs : 'none'}ms | chunks: ${chunkCount}`);
|
|
530
539
|
pmSessionId = chatResult.sessionId || pmSessionId;
|
|
531
540
|
// Update persisted PM session ID
|
|
532
|
-
savePMState({ taskId: task.id, pmSessionId, repoPath: task.repoPath, savedAt: new Date().toISOString() });
|
|
541
|
+
savePMState({ taskId: task.id, pmSessionId, repoPath: task.repoPath, savedAt: new Date().toISOString(), agentId: task.agentId });
|
|
533
542
|
// PM produced a plan with subtasks → break out to execution
|
|
534
543
|
if (chatResult.plan && chatResult.plan.subtasks.length > 0) {
|
|
535
544
|
planResult = { plan: chatResult.plan, pmSessionId };
|
|
@@ -779,6 +788,7 @@ export class ClaudeCodeAdapter {
|
|
|
779
788
|
pmSessionId: planResult.pmSessionId,
|
|
780
789
|
repoPath: task.repoPath,
|
|
781
790
|
savedAt: new Date().toISOString(),
|
|
791
|
+
agentId: task.agentId,
|
|
782
792
|
});
|
|
783
793
|
}
|
|
784
794
|
ws.sendTaskDone(task.id, {
|
|
@@ -897,7 +907,7 @@ export class ClaudeCodeAdapter {
|
|
|
897
907
|
const overlap = phrasesA.filter(p => phrasesB.has(p)).length;
|
|
898
908
|
return overlap / phrasesA.length > 0.4;
|
|
899
909
|
}
|
|
900
|
-
async runSelfImprovement(cwd) {
|
|
910
|
+
async runSelfImprovement(cwd, agentId) {
|
|
901
911
|
if (!process.env.MEM0_SSH_HOST)
|
|
902
912
|
return;
|
|
903
913
|
this.taskCount++;
|
|
@@ -983,7 +993,7 @@ export class ClaudeCodeAdapter {
|
|
|
983
993
|
console.log(`[Self-Improve] Added ${toAdd.length} new rules to CLAUDE.md:`);
|
|
984
994
|
toAdd.forEach(p => console.log(`[Self-Improve] - ${p.rule}`));
|
|
985
995
|
// Sync learned rules to heartbeat metrics
|
|
986
|
-
this.parseLearnedRules(claudeMdPath);
|
|
996
|
+
this.parseLearnedRules(claudeMdPath, agentId);
|
|
987
997
|
}
|
|
988
998
|
catch (err) {
|
|
989
999
|
console.warn(`[Self-Improve] Failed:`, err instanceof Error ? err.message : err);
|
|
@@ -993,7 +1003,7 @@ export class ClaudeCodeAdapter {
|
|
|
993
1003
|
* Parse "## Learned Rules" section from CLAUDE.md and store in learnedRulesMap.
|
|
994
1004
|
* Rule format: `- Rule text (confidence: 0.7)`
|
|
995
1005
|
*/
|
|
996
|
-
parseLearnedRules(claudeMdPath) {
|
|
1006
|
+
parseLearnedRules(claudeMdPath, agentId) {
|
|
997
1007
|
try {
|
|
998
1008
|
if (!fs.existsSync(claudeMdPath))
|
|
999
1009
|
return;
|
|
@@ -1038,8 +1048,9 @@ export class ClaudeCodeAdapter {
|
|
|
1038
1048
|
});
|
|
1039
1049
|
}
|
|
1040
1050
|
if (rules.length > 0) {
|
|
1041
|
-
this.
|
|
1042
|
-
this.
|
|
1051
|
+
const resolvedAgentId = agentId || this.currentAgentId;
|
|
1052
|
+
this.learnedRulesMap.set(resolvedAgentId, rules);
|
|
1053
|
+
this.getMetricsForAgent(resolvedAgentId).rulesCount = rules.length;
|
|
1043
1054
|
console.log(`[Self-Improve] Parsed ${rules.length} rules from CLAUDE.md for heartbeat sync`);
|
|
1044
1055
|
}
|
|
1045
1056
|
}
|
|
@@ -1048,20 +1059,21 @@ export class ClaudeCodeAdapter {
|
|
|
1048
1059
|
}
|
|
1049
1060
|
}
|
|
1050
1061
|
/** Track task completion metrics (public for daemon resume path). */
|
|
1051
|
-
trackMetricsPublic(status, durationMs) {
|
|
1052
|
-
this.trackMetrics(status, durationMs);
|
|
1062
|
+
trackMetricsPublic(status, durationMs, agentId) {
|
|
1063
|
+
this.trackMetrics(status, durationMs, agentId);
|
|
1053
1064
|
}
|
|
1054
1065
|
/** Track task completion metrics. */
|
|
1055
|
-
trackMetrics(status, durationMs) {
|
|
1056
|
-
this.metrics
|
|
1066
|
+
trackMetrics(status, durationMs, agentId) {
|
|
1067
|
+
const m = agentId ? this.getMetricsForAgent(agentId) : this.metrics;
|
|
1068
|
+
m.taskCount++;
|
|
1057
1069
|
if (status === 'done')
|
|
1058
|
-
|
|
1070
|
+
m.successCount++;
|
|
1059
1071
|
else
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
console.log(`[Metrics] Tasks: ${
|
|
1072
|
+
m.failCount++;
|
|
1073
|
+
m.totalDurationMs += durationMs;
|
|
1074
|
+
m.avgDurationMs = Math.round(m.totalDurationMs / m.taskCount);
|
|
1075
|
+
m.lastTaskAt = new Date().toISOString();
|
|
1076
|
+
console.log(`[Metrics] Tasks: ${m.successCount}✓ ${m.failCount}✗ | Avg: ${(m.avgDurationMs / 1000).toFixed(0)}s | Reflections: ${m.reflectionCount} | Patterns: ${m.patternsLearnedCount}`);
|
|
1065
1077
|
}
|
|
1066
1078
|
async dispose() {
|
|
1067
1079
|
// No persistent resources to clean up
|
package/dist/daemon/index.js
CHANGED
|
@@ -134,9 +134,7 @@ export async function startDaemon(config) {
|
|
|
134
134
|
const activeAgentTasks = new Map();
|
|
135
135
|
// Track abort controllers per task
|
|
136
136
|
const taskAbortControllers = new Map();
|
|
137
|
-
//
|
|
138
|
-
let currentTaskId = null;
|
|
139
|
-
let currentTaskAbort = null;
|
|
137
|
+
// Note: currentTaskId/currentTaskAbort removed — use taskAbortControllers + activeAgentTasks instead
|
|
140
138
|
const onAuthFailed = (code, reason) => {
|
|
141
139
|
console.error(`\n[Daemon] Authentication failed (code: ${code}, reason: ${reason}).`);
|
|
142
140
|
console.error('[Daemon] Your machine token is invalid or expired.');
|
|
@@ -203,7 +201,7 @@ export async function startDaemon(config) {
|
|
|
203
201
|
}
|
|
204
202
|
// Recover orphaned tasks — pass active task IDs so API won't fail tasks we're still running
|
|
205
203
|
const activeTaskIds = Array.from(taskAbortControllers.keys());
|
|
206
|
-
ws.send({ action: 'recover_orphaned_tasks',
|
|
204
|
+
ws.send({ action: 'recover_orphaned_tasks', activeTaskIds });
|
|
207
205
|
break;
|
|
208
206
|
case 'task_assigned': {
|
|
209
207
|
const task = msg.task;
|
|
@@ -226,11 +224,7 @@ export async function startDaemon(config) {
|
|
|
226
224
|
activeAgentTasks.set(agentId, task.id);
|
|
227
225
|
const abort = new AbortController();
|
|
228
226
|
taskAbortControllers.set(task.id, abort);
|
|
229
|
-
currentTaskId = task.id;
|
|
230
|
-
currentTaskAbort = abort;
|
|
231
227
|
console.log(`[Daemon] Received task: ${task.id} agent=${agentId} — ${task.description.slice(0, 80)} (attachments: ${task.attachments?.length ?? 0}) [active: ${activeTasks}]`);
|
|
232
|
-
// MCP config per-agent is handled by writeAgentFolderMcpConfig in adapter
|
|
233
|
-
// (writes per-agent .mcp.json in agent folder — safe for parallel execution)
|
|
234
228
|
// Run task in background (non-blocking) to allow parallel agent execution
|
|
235
229
|
(async () => {
|
|
236
230
|
try {
|
|
@@ -250,10 +244,6 @@ export async function startDaemon(config) {
|
|
|
250
244
|
activeTasks--;
|
|
251
245
|
activeAgentTasks.delete(agentId);
|
|
252
246
|
taskAbortControllers.delete(task.id);
|
|
253
|
-
if (currentTaskId === task.id) {
|
|
254
|
-
currentTaskId = null;
|
|
255
|
-
currentTaskAbort = null;
|
|
256
|
-
}
|
|
257
247
|
pendingInputResolvers.delete(task.id);
|
|
258
248
|
ws.send({ action: 'agent_ready', agentId });
|
|
259
249
|
}
|
|
@@ -435,13 +425,14 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
435
425
|
// Store rating in Mem0 via adapter (fire-and-forget)
|
|
436
426
|
if (adapter.type === 'claude-code') {
|
|
437
427
|
const ccAdapter = adapter;
|
|
438
|
-
const
|
|
428
|
+
const agentFolder = agentId ? ccAdapter.getAgentFolder(agentId) : undefined;
|
|
429
|
+
const cwd = agentFolder || path.join(os.homedir(), 'tuna-workspace');
|
|
439
430
|
ccAdapter.storeRatingMemory({
|
|
440
431
|
taskTitle,
|
|
441
432
|
taskDescription,
|
|
442
433
|
score,
|
|
443
434
|
comment: comment || undefined,
|
|
444
|
-
cwd
|
|
435
|
+
cwd,
|
|
445
436
|
}).catch((err) => {
|
|
446
437
|
console.warn(`[Daemon] Rating→Mem0 failed:`, err instanceof Error ? err.message : err);
|
|
447
438
|
});
|
|
@@ -467,6 +458,13 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
467
458
|
// No resolver — check if we have a persisted session to resume
|
|
468
459
|
const savedState = loadPMState(taskId);
|
|
469
460
|
if (savedState) {
|
|
461
|
+
// Check per-agent concurrency before resuming
|
|
462
|
+
const resumeAgentId = savedState.agentId || '__default__';
|
|
463
|
+
if (activeAgentTasks.has(resumeAgentId)) {
|
|
464
|
+
console.warn(`[Daemon] Cannot resume task ${taskId} — agent ${resumeAgentId} is busy with task ${activeAgentTasks.get(resumeAgentId)}`);
|
|
465
|
+
ws.send({ action: 'task_rejected', taskId, reason: 'agent_busy' });
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
470
468
|
if (savedState.mode === 'agent_team') {
|
|
471
469
|
console.log(`[Daemon] Resuming agent_team for task ${taskId} (session: ${savedState.agentTeamSessionId?.substring(0, 12) ?? 'none'})`);
|
|
472
470
|
resumeAgentTeamChat(taskId, answer, attachments, savedState, ws, pendingInputResolvers);
|
|
@@ -635,11 +633,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
635
633
|
* Loads persisted pmSessionId and runs a chat loop.
|
|
636
634
|
*/
|
|
637
635
|
async function resumePMChat(taskId, firstMessage, firstAttachments, pmState, wsClient, resolvers) {
|
|
636
|
+
const agentId = pmState.agentId || '__default__';
|
|
638
637
|
activeTasks++;
|
|
638
|
+
activeAgentTasks.set(agentId, taskId);
|
|
639
639
|
const abort = new AbortController();
|
|
640
640
|
taskAbortControllers.set(taskId, abort);
|
|
641
|
-
currentTaskId = taskId;
|
|
642
|
-
currentTaskAbort = abort;
|
|
643
641
|
let pmSessionId = pmState.pmSessionId;
|
|
644
642
|
const MAX_RESUMED_ROUNDS = 50;
|
|
645
643
|
try {
|
|
@@ -667,7 +665,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
667
665
|
}, inputFiles);
|
|
668
666
|
wsClient.sendPMStreamEnd(taskId, streamMsgId);
|
|
669
667
|
pmSessionId = chatResult.sessionId || pmSessionId;
|
|
670
|
-
savePMState({ taskId, pmSessionId, repoPath: pmState.repoPath, savedAt: new Date().toISOString() });
|
|
668
|
+
savePMState({ taskId, pmSessionId, repoPath: pmState.repoPath, savedAt: new Date().toISOString(), agentId: pmState.agentId });
|
|
671
669
|
// PM produced a plan → execute it using shared helper
|
|
672
670
|
if (chatResult.plan && chatResult.plan.subtasks.length > 0) {
|
|
673
671
|
const plan = chatResult.plan;
|
|
@@ -731,14 +729,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
731
729
|
}
|
|
732
730
|
finally {
|
|
733
731
|
activeTasks--;
|
|
732
|
+
activeAgentTasks.delete(agentId);
|
|
734
733
|
taskAbortControllers.delete(taskId);
|
|
735
|
-
if (currentTaskId === taskId) {
|
|
736
|
-
currentTaskId = null;
|
|
737
|
-
currentTaskAbort = null;
|
|
738
|
-
}
|
|
739
734
|
resolvers.delete(taskId);
|
|
740
735
|
cleanupAttachments(taskId);
|
|
741
|
-
wsClient.send({ action: 'agent_ready' });
|
|
736
|
+
wsClient.send({ action: 'agent_ready', agentId });
|
|
742
737
|
}
|
|
743
738
|
}
|
|
744
739
|
/**
|
|
@@ -746,11 +741,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
746
741
|
* Uses saved Claude CLI session ID to resume with --resume flag.
|
|
747
742
|
*/
|
|
748
743
|
async function resumeAgentTeamChat(taskId, firstMessage, firstAttachments, savedState, wsClient, resolvers) {
|
|
744
|
+
const agentId = savedState.agentId || '__default__';
|
|
749
745
|
activeTasks++;
|
|
746
|
+
activeAgentTasks.set(agentId, taskId);
|
|
750
747
|
const abort = new AbortController();
|
|
751
748
|
taskAbortControllers.set(taskId, abort);
|
|
752
|
-
currentTaskId = taskId;
|
|
753
|
-
currentTaskAbort = abort;
|
|
754
749
|
let sessionId = savedState.agentTeamSessionId;
|
|
755
750
|
let totalDurationMs = 0;
|
|
756
751
|
try {
|
|
@@ -849,6 +844,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
849
844
|
savedAt: new Date().toISOString(),
|
|
850
845
|
mode: 'agent_team',
|
|
851
846
|
agentTeamSessionId: sessionId,
|
|
847
|
+
agentId: savedState.agentId,
|
|
852
848
|
});
|
|
853
849
|
// Wait for follow-up with timeout
|
|
854
850
|
const FOLLOW_UP_TIMEOUT_MS = 60_000;
|
|
@@ -871,7 +867,7 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
871
867
|
// Track metrics + reflection on the adapter
|
|
872
868
|
if (adapter.type === 'claude-code') {
|
|
873
869
|
const ccAdapter = adapter;
|
|
874
|
-
ccAdapter.trackMetricsPublic('done', totalDurationMs);
|
|
870
|
+
ccAdapter.trackMetricsPublic('done', totalDurationMs, savedState.agentId);
|
|
875
871
|
ccAdapter.runReflection({ id: taskId, description: firstMessage, repoPath: cwd, enableReflection: true }, lastResumeOutput || 'Task completed (no follow-up)', 'done', cwd).then(() => ccAdapter.runSelfImprovement(cwd)).catch(() => { });
|
|
876
872
|
}
|
|
877
873
|
return;
|
|
@@ -909,14 +905,11 @@ ${skillContent.slice(0, 15000)}`;
|
|
|
909
905
|
}
|
|
910
906
|
finally {
|
|
911
907
|
activeTasks--;
|
|
908
|
+
activeAgentTasks.delete(agentId);
|
|
912
909
|
taskAbortControllers.delete(taskId);
|
|
913
|
-
if (currentTaskId === taskId) {
|
|
914
|
-
currentTaskId = null;
|
|
915
|
-
currentTaskAbort = null;
|
|
916
|
-
}
|
|
917
910
|
resolvers.delete(taskId);
|
|
918
911
|
cleanupAttachments(taskId);
|
|
919
|
-
wsClient.send({ action: 'agent_ready' });
|
|
912
|
+
wsClient.send({ action: 'agent_ready', agentId });
|
|
920
913
|
}
|
|
921
914
|
}
|
|
922
915
|
// Wire up agent metrics to heartbeat
|
|
@@ -7,6 +7,8 @@ export interface PMSessionState {
|
|
|
7
7
|
mode?: 'tuna' | 'agent_team';
|
|
8
8
|
/** Claude CLI session ID for agent_team resume */
|
|
9
9
|
agentTeamSessionId?: string;
|
|
10
|
+
/** Agent ID for per-agent concurrency tracking during resume */
|
|
11
|
+
agentId?: string;
|
|
10
12
|
}
|
|
11
13
|
/** Save PM session state for a task (survives agent restart). */
|
|
12
14
|
export declare function savePMState(state: PMSessionState): void;
|