@agentuity/opencode 1.0.15 → 1.0.17
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/agents/architect.d.ts +1 -1
- package/dist/agents/architect.d.ts.map +1 -1
- package/dist/agents/architect.js +30 -33
- package/dist/agents/architect.js.map +1 -1
- package/dist/agents/builder.d.ts +1 -1
- package/dist/agents/builder.d.ts.map +1 -1
- package/dist/agents/builder.js +53 -60
- package/dist/agents/builder.js.map +1 -1
- package/dist/agents/expert-backend.d.ts +1 -1
- package/dist/agents/expert-backend.d.ts.map +1 -1
- package/dist/agents/expert-backend.js +32 -40
- package/dist/agents/expert-backend.js.map +1 -1
- package/dist/agents/expert-frontend.d.ts +1 -1
- package/dist/agents/expert-frontend.d.ts.map +1 -1
- package/dist/agents/expert-frontend.js +18 -24
- package/dist/agents/expert-frontend.js.map +1 -1
- package/dist/agents/expert-ops.d.ts +1 -1
- package/dist/agents/expert-ops.d.ts.map +1 -1
- package/dist/agents/expert-ops.js +37 -51
- package/dist/agents/expert-ops.js.map +1 -1
- package/dist/agents/expert.d.ts +1 -1
- package/dist/agents/expert.d.ts.map +1 -1
- package/dist/agents/expert.js +33 -43
- package/dist/agents/expert.js.map +1 -1
- package/dist/agents/lead.d.ts +1 -1
- package/dist/agents/lead.d.ts.map +1 -1
- package/dist/agents/lead.js +179 -222
- package/dist/agents/lead.js.map +1 -1
- package/dist/agents/memory.d.ts +1 -1
- package/dist/agents/memory.d.ts.map +1 -1
- package/dist/agents/memory.js +62 -90
- package/dist/agents/memory.js.map +1 -1
- package/dist/agents/monitor.d.ts +1 -1
- package/dist/agents/monitor.d.ts.map +1 -1
- package/dist/agents/monitor.js +84 -44
- package/dist/agents/monitor.js.map +1 -1
- package/dist/agents/product.d.ts +1 -1
- package/dist/agents/product.d.ts.map +1 -1
- package/dist/agents/product.js +16 -22
- package/dist/agents/product.js.map +1 -1
- package/dist/agents/reviewer.d.ts +1 -1
- package/dist/agents/reviewer.d.ts.map +1 -1
- package/dist/agents/reviewer.js +15 -27
- package/dist/agents/reviewer.js.map +1 -1
- package/dist/agents/runner.d.ts +1 -1
- package/dist/agents/runner.d.ts.map +1 -1
- package/dist/agents/runner.js +52 -76
- package/dist/agents/runner.js.map +1 -1
- package/dist/agents/scout.d.ts +1 -1
- package/dist/agents/scout.d.ts.map +1 -1
- package/dist/agents/scout.js +42 -43
- package/dist/agents/scout.js.map +1 -1
- package/dist/agents/types.d.ts +8 -0
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/background/manager.d.ts +18 -0
- package/dist/background/manager.d.ts.map +1 -1
- package/dist/background/manager.js +201 -33
- package/dist/background/manager.js.map +1 -1
- package/dist/background/types.d.ts +3 -0
- package/dist/background/types.d.ts.map +1 -1
- package/dist/config/loader.js +2 -2
- package/dist/plugin/hooks/cadence.d.ts +3 -1
- package/dist/plugin/hooks/cadence.d.ts.map +1 -1
- package/dist/plugin/hooks/cadence.js +167 -70
- package/dist/plugin/hooks/cadence.js.map +1 -1
- package/dist/plugin/hooks/compaction-utils.d.ts +48 -0
- package/dist/plugin/hooks/compaction-utils.d.ts.map +1 -0
- package/dist/plugin/hooks/compaction-utils.js +259 -0
- package/dist/plugin/hooks/compaction-utils.js.map +1 -0
- package/dist/plugin/hooks/completion.d.ts +14 -0
- package/dist/plugin/hooks/completion.d.ts.map +1 -0
- package/dist/plugin/hooks/completion.js +45 -0
- package/dist/plugin/hooks/completion.js.map +1 -0
- package/dist/plugin/hooks/params.d.ts +47 -2
- package/dist/plugin/hooks/params.d.ts.map +1 -1
- package/dist/plugin/hooks/params.js +82 -1
- package/dist/plugin/hooks/params.js.map +1 -1
- package/dist/plugin/hooks/session-memory.d.ts +2 -1
- package/dist/plugin/hooks/session-memory.d.ts.map +1 -1
- package/dist/plugin/hooks/session-memory.js +101 -48
- package/dist/plugin/hooks/session-memory.js.map +1 -1
- package/dist/plugin/hooks/tools.d.ts.map +1 -1
- package/dist/plugin/hooks/tools.js +26 -1
- package/dist/plugin/hooks/tools.js.map +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +38 -9
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/sqlite/index.d.ts +1 -1
- package/dist/sqlite/index.d.ts.map +1 -1
- package/dist/sqlite/queries.d.ts +1 -0
- package/dist/sqlite/queries.d.ts.map +1 -1
- package/dist/sqlite/queries.js +4 -0
- package/dist/sqlite/queries.js.map +1 -1
- package/dist/sqlite/reader.d.ts +11 -1
- package/dist/sqlite/reader.d.ts.map +1 -1
- package/dist/sqlite/reader.js +62 -0
- package/dist/sqlite/reader.js.map +1 -1
- package/dist/sqlite/types.d.ts +40 -0
- package/dist/sqlite/types.d.ts.map +1 -1
- package/dist/tools/background.d.ts.map +1 -1
- package/dist/tools/background.js +15 -0
- package/dist/tools/background.js.map +1 -1
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
- package/src/agents/architect.ts +30 -33
- package/src/agents/builder.ts +53 -60
- package/src/agents/expert-backend.ts +32 -40
- package/src/agents/expert-frontend.ts +18 -24
- package/src/agents/expert-ops.ts +37 -51
- package/src/agents/expert.ts +33 -43
- package/src/agents/lead.ts +179 -222
- package/src/agents/memory.ts +62 -90
- package/src/agents/monitor.ts +84 -44
- package/src/agents/product.ts +16 -22
- package/src/agents/reviewer.ts +15 -27
- package/src/agents/runner.ts +52 -76
- package/src/agents/scout.ts +42 -43
- package/src/agents/types.ts +8 -0
- package/src/background/manager.ts +227 -38
- package/src/background/types.ts +3 -0
- package/src/config/loader.ts +2 -2
- package/src/plugin/hooks/cadence.ts +188 -74
- package/src/plugin/hooks/compaction-utils.ts +291 -0
- package/src/plugin/hooks/completion.ts +61 -0
- package/src/plugin/hooks/params.ts +107 -2
- package/src/plugin/hooks/session-memory.ts +113 -47
- package/src/plugin/hooks/tools.ts +32 -1
- package/src/plugin/plugin.ts +54 -10
- package/src/sqlite/index.ts +4 -0
- package/src/sqlite/queries.ts +5 -0
- package/src/sqlite/reader.ts +69 -0
- package/src/sqlite/types.ts +40 -0
- package/src/tools/background.ts +28 -0
- package/src/types.ts +40 -0
|
@@ -14,7 +14,7 @@ import { ConcurrencyManager } from './concurrency';
|
|
|
14
14
|
|
|
15
15
|
const DEFAULT_BACKGROUND_CONFIG: BackgroundTaskConfig = {
|
|
16
16
|
enabled: true,
|
|
17
|
-
defaultConcurrency:
|
|
17
|
+
defaultConcurrency: 5,
|
|
18
18
|
staleTimeoutMs: 30 * 60 * 1000,
|
|
19
19
|
};
|
|
20
20
|
|
|
@@ -55,7 +55,13 @@ export class BackgroundManager {
|
|
|
55
55
|
private tasksBySession = new Map<string, string>();
|
|
56
56
|
private notifications = new Map<string, Set<string>>();
|
|
57
57
|
private toolCallIds = new Map<string, Set<string>>();
|
|
58
|
+
/** Tracks tool call IDs that are currently in-flight (pending/running state) per task */
|
|
59
|
+
private activeToolCallIds = new Map<string, Set<string>>();
|
|
60
|
+
/** Maps parent session ID → monitor task ID for auto-launched monitors */
|
|
61
|
+
private monitorsPerParent = new Map<string, string>();
|
|
62
|
+
private lastNotifyTimes = new Map<string, number>();
|
|
58
63
|
private shuttingDown = false;
|
|
64
|
+
private refreshIntervalId: ReturnType<typeof setInterval> | undefined;
|
|
59
65
|
|
|
60
66
|
constructor(
|
|
61
67
|
ctx: PluginInput,
|
|
@@ -73,6 +79,17 @@ export class BackgroundManager {
|
|
|
73
79
|
this.dbReader = dbReader;
|
|
74
80
|
this.serverUrl = this.resolveServerUrl();
|
|
75
81
|
this.authHeaders = this.resolveAuthHeaders();
|
|
82
|
+
|
|
83
|
+
// Periodic safety net: refresh task statuses every 30s in case events are missed
|
|
84
|
+
this.refreshIntervalId = setInterval(() => {
|
|
85
|
+
if (this.shuttingDown) return;
|
|
86
|
+
const hasActive = Array.from(this.tasks.values()).some(
|
|
87
|
+
(t) => t.status === 'pending' || t.status === 'running'
|
|
88
|
+
);
|
|
89
|
+
if (hasActive) {
|
|
90
|
+
void this.refreshStatuses();
|
|
91
|
+
}
|
|
92
|
+
}, 30_000);
|
|
76
93
|
}
|
|
77
94
|
|
|
78
95
|
/**
|
|
@@ -135,6 +152,7 @@ export class BackgroundManager {
|
|
|
135
152
|
status: 'pending',
|
|
136
153
|
queuedAt: new Date(),
|
|
137
154
|
concurrencyGroup: this.getConcurrencyGroup(input.agent),
|
|
155
|
+
notifiedStatuses: new Set(),
|
|
138
156
|
};
|
|
139
157
|
|
|
140
158
|
this.tasks.set(task.id, task);
|
|
@@ -149,6 +167,12 @@ export class BackgroundManager {
|
|
|
149
167
|
}
|
|
150
168
|
|
|
151
169
|
void this.startTask(task);
|
|
170
|
+
|
|
171
|
+
// Auto-launch a Monitor for this parent session if not already running.
|
|
172
|
+
// Monitor uses session_dashboard scoped to the parent session ID, so it only
|
|
173
|
+
// sees sibling tasks — not unrelated sessions across the server.
|
|
174
|
+
void this.ensureMonitorForParent(input.parentSessionId);
|
|
175
|
+
|
|
152
176
|
return task;
|
|
153
177
|
}
|
|
154
178
|
|
|
@@ -176,7 +200,21 @@ export class BackgroundManager {
|
|
|
176
200
|
*/
|
|
177
201
|
async inspectTask(taskId: string): Promise<TaskInspection | undefined> {
|
|
178
202
|
const task = this.tasks.get(taskId);
|
|
179
|
-
if (!task
|
|
203
|
+
if (!task) return undefined;
|
|
204
|
+
|
|
205
|
+
// Task exists but has not yet acquired a concurrency slot — it is queued
|
|
206
|
+
// and no session has been created yet. Return a lightweight inspection so
|
|
207
|
+
// callers can distinguish "queued/pending" from "not found".
|
|
208
|
+
if (!task.sessionId) {
|
|
209
|
+
return {
|
|
210
|
+
taskId: task.id,
|
|
211
|
+
sessionId: '',
|
|
212
|
+
status: task.status,
|
|
213
|
+
session: null,
|
|
214
|
+
messages: [],
|
|
215
|
+
lastActivity: task.queuedAt?.toISOString(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
180
218
|
|
|
181
219
|
try {
|
|
182
220
|
if (this.dbReader?.isAvailable()) {
|
|
@@ -387,9 +425,19 @@ export class BackgroundManager {
|
|
|
387
425
|
progress: {
|
|
388
426
|
toolCalls: 0,
|
|
389
427
|
lastUpdate: new Date(),
|
|
428
|
+
activeToolCallsInFlight: 0,
|
|
390
429
|
},
|
|
391
430
|
};
|
|
392
431
|
|
|
432
|
+
// Mark recovered terminal tasks as already notified
|
|
433
|
+
if (
|
|
434
|
+
task.status === 'completed' ||
|
|
435
|
+
task.status === 'error' ||
|
|
436
|
+
task.status === 'cancelled'
|
|
437
|
+
) {
|
|
438
|
+
task.notifiedStatuses = new Set([task.status]);
|
|
439
|
+
}
|
|
440
|
+
|
|
393
441
|
this.tasks.set(task.id, task);
|
|
394
442
|
this.tasksBySession.set(sess.id, task.id);
|
|
395
443
|
|
|
@@ -463,9 +511,19 @@ export class BackgroundManager {
|
|
|
463
511
|
progress: {
|
|
464
512
|
toolCalls: 0,
|
|
465
513
|
lastUpdate: new Date(),
|
|
514
|
+
activeToolCallsInFlight: 0,
|
|
466
515
|
},
|
|
467
516
|
};
|
|
468
517
|
|
|
518
|
+
// Mark recovered terminal tasks as already notified
|
|
519
|
+
if (
|
|
520
|
+
task.status === 'completed' ||
|
|
521
|
+
task.status === 'error' ||
|
|
522
|
+
task.status === 'cancelled'
|
|
523
|
+
) {
|
|
524
|
+
task.notifiedStatuses = new Set([task.status]);
|
|
525
|
+
}
|
|
526
|
+
|
|
469
527
|
// Add to our tracking maps
|
|
470
528
|
this.tasks.set(task.id, task);
|
|
471
529
|
this.tasksBySession.set(sess.id, task.id);
|
|
@@ -556,12 +614,28 @@ export class BackgroundManager {
|
|
|
556
614
|
const task = sessionId ? this.findBySession(sessionId) : undefined;
|
|
557
615
|
if (!task) return;
|
|
558
616
|
const error = extractError(event.properties);
|
|
559
|
-
|
|
617
|
+
const errorMsg = error ?? 'Session error.';
|
|
618
|
+
|
|
619
|
+
// Log extra context for timeout errors — the server fires these when
|
|
620
|
+
// a model generates a long text response without tool activity.
|
|
621
|
+
if (
|
|
622
|
+
errorMsg.toLowerCase().includes('timeout') ||
|
|
623
|
+
errorMsg.toLowerCase().includes('no activity')
|
|
624
|
+
) {
|
|
625
|
+
console.debug(
|
|
626
|
+
`[BackgroundManager] Task ${task.id} timed out - may have been generating long response. Progress: ${JSON.stringify(task.progress)}`
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
this.failTask(task, errorMsg);
|
|
560
631
|
return;
|
|
561
632
|
}
|
|
562
633
|
}
|
|
563
634
|
|
|
564
635
|
markForNotification(task: BackgroundTask): void {
|
|
636
|
+
// Monitor tasks are infrastructure — never notify Lead about them.
|
|
637
|
+
// Monitor pushes its own consolidated report as its final output.
|
|
638
|
+
if (task.isMonitor) return;
|
|
565
639
|
const sessionId = task.parentSessionId;
|
|
566
640
|
if (!sessionId) return;
|
|
567
641
|
const queue = this.notifications.get(sessionId) ?? new Set<string>();
|
|
@@ -583,6 +657,10 @@ export class BackgroundManager {
|
|
|
583
657
|
|
|
584
658
|
shutdown(): void {
|
|
585
659
|
this.shuttingDown = true;
|
|
660
|
+
if (this.refreshIntervalId) {
|
|
661
|
+
clearInterval(this.refreshIntervalId);
|
|
662
|
+
this.refreshIntervalId = undefined;
|
|
663
|
+
}
|
|
586
664
|
this.concurrency.clear();
|
|
587
665
|
this.notifications.clear();
|
|
588
666
|
try {
|
|
@@ -598,6 +676,67 @@ export class BackgroundManager {
|
|
|
598
676
|
this.tasksByParent.set(task.parentSessionId, parentList);
|
|
599
677
|
}
|
|
600
678
|
|
|
679
|
+
/**
|
|
680
|
+
* Ensure a Monitor agent is watching all background tasks for the given parent session.
|
|
681
|
+
*
|
|
682
|
+
* Called automatically whenever a new background task is launched. If a Monitor is
|
|
683
|
+
* already running for this parent, this is a no-op. The Monitor uses
|
|
684
|
+
* `agentuity_session_dashboard({ session_id: parentSessionId })` which is scoped
|
|
685
|
+
* to child sessions of that parent only — it does not see unrelated sessions.
|
|
686
|
+
*
|
|
687
|
+
* The Monitor pushes a consolidated status update to Lead when all tasks complete,
|
|
688
|
+
* so Lead doesn't need to self-poll.
|
|
689
|
+
*/
|
|
690
|
+
private async ensureMonitorForParent(parentSessionId: string): Promise<void> {
|
|
691
|
+
if (this.shuttingDown) return;
|
|
692
|
+
|
|
693
|
+
// Check if we already have a live monitor for this parent
|
|
694
|
+
const existingMonitorId = this.monitorsPerParent.get(parentSessionId);
|
|
695
|
+
if (existingMonitorId) {
|
|
696
|
+
const existing = this.tasks.get(existingMonitorId);
|
|
697
|
+
if (existing && (existing.status === 'pending' || existing.status === 'running')) {
|
|
698
|
+
return; // Monitor already active
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Find the Monitor agent display name
|
|
703
|
+
const monitorAgent = Object.values(agents).find((a) => a.role === 'monitor');
|
|
704
|
+
if (!monitorAgent) return; // Monitor agent not registered
|
|
705
|
+
|
|
706
|
+
const monitorPrompt = `You are watching background tasks for parent session: ${parentSessionId}
|
|
707
|
+
|
|
708
|
+
Use \`agentuity_session_dashboard({ session_id: "${parentSessionId}" })\` to see all child task sessions and their current status.
|
|
709
|
+
|
|
710
|
+
Monitor all non-monitor background tasks until they complete. When all tasks are done (completed, error, or cancelled), send a consolidated summary back. Use \`agentuity_background_output\` to retrieve results for completed tasks.
|
|
711
|
+
|
|
712
|
+
Do not poll more than once every 30 seconds. Be patient — Scout tasks reading large codebases typically take 3–8 minutes.`;
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
const monitorTask: BackgroundTask = {
|
|
716
|
+
id: createTaskId(),
|
|
717
|
+
parentSessionId,
|
|
718
|
+
description: 'Monitor background tasks',
|
|
719
|
+
prompt: monitorPrompt,
|
|
720
|
+
agent: monitorAgent.displayName,
|
|
721
|
+
status: 'pending',
|
|
722
|
+
queuedAt: new Date(),
|
|
723
|
+
concurrencyGroup: this.getConcurrencyGroup(monitorAgent.displayName),
|
|
724
|
+
notifiedStatuses: new Set(),
|
|
725
|
+
isMonitor: true,
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
this.tasks.set(monitorTask.id, monitorTask);
|
|
729
|
+
this.monitorsPerParent.set(parentSessionId, monitorTask.id);
|
|
730
|
+
// Index monitor task so it's tracked by parent (but flagged as monitor)
|
|
731
|
+
this.indexTask(monitorTask);
|
|
732
|
+
|
|
733
|
+
void this.startTask(monitorTask);
|
|
734
|
+
} catch {
|
|
735
|
+
// Non-fatal: if monitor launch fails, the event-driven notifyParent
|
|
736
|
+
// still works as the primary completion signal
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
601
740
|
private async startTask(task: BackgroundTask): Promise<void> {
|
|
602
741
|
if (this.shuttingDown) return;
|
|
603
742
|
|
|
@@ -690,16 +829,37 @@ export class BackgroundManager {
|
|
|
690
829
|
if (part.type === 'tool') {
|
|
691
830
|
const callId = part.callID;
|
|
692
831
|
const toolName = part.tool;
|
|
832
|
+
const toolStatus = part.state?.status;
|
|
833
|
+
|
|
693
834
|
if (toolName) {
|
|
694
835
|
progress.lastTool = toolName;
|
|
695
836
|
}
|
|
837
|
+
|
|
696
838
|
if (callId) {
|
|
697
839
|
const seen = this.toolCallIds.get(task.id) ?? new Set<string>();
|
|
840
|
+
const active = this.activeToolCallIds.get(task.id) ?? new Set<string>();
|
|
841
|
+
|
|
698
842
|
if (!seen.has(callId)) {
|
|
843
|
+
// First time seeing this callId — it's a new tool call starting
|
|
699
844
|
seen.add(callId);
|
|
700
845
|
progress.toolCalls += 1;
|
|
701
846
|
this.toolCallIds.set(task.id, seen);
|
|
702
847
|
}
|
|
848
|
+
|
|
849
|
+
// Track in-flight status based on tool state
|
|
850
|
+
// Only remove for explicit terminal statuses; treat unknown/missing as in-flight
|
|
851
|
+
if (
|
|
852
|
+
toolStatus === 'completed' ||
|
|
853
|
+
toolStatus === 'error' ||
|
|
854
|
+
toolStatus === 'cancelled'
|
|
855
|
+
) {
|
|
856
|
+
active.delete(callId);
|
|
857
|
+
} else {
|
|
858
|
+
// pending, running, unknown, or missing status — treat as in-flight
|
|
859
|
+
active.add(callId);
|
|
860
|
+
}
|
|
861
|
+
this.activeToolCallIds.set(task.id, active);
|
|
862
|
+
progress.activeToolCallsInFlight = active.size;
|
|
703
863
|
}
|
|
704
864
|
}
|
|
705
865
|
|
|
@@ -715,6 +875,7 @@ export class BackgroundManager {
|
|
|
715
875
|
return {
|
|
716
876
|
toolCalls: 0,
|
|
717
877
|
lastUpdate: new Date(),
|
|
878
|
+
activeToolCallsInFlight: 0,
|
|
718
879
|
};
|
|
719
880
|
}
|
|
720
881
|
|
|
@@ -758,29 +919,34 @@ export class BackgroundManager {
|
|
|
758
919
|
|
|
759
920
|
private async notifyParent(task: BackgroundTask): Promise<void> {
|
|
760
921
|
if (!task.parentSessionId) return;
|
|
922
|
+
if (this.shuttingDown) return;
|
|
923
|
+
// Monitor tasks push their own report as their session output — no separate notification needed.
|
|
924
|
+
if (task.isMonitor) return;
|
|
761
925
|
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
// Self-healing for tasks created before deduplication was added:
|
|
767
|
-
// If a task is already in a terminal state but has no notification history,
|
|
768
|
-
// assume it was already notified and skip to prevent duplicate notifications.
|
|
769
|
-
if (
|
|
770
|
-
notifiedStatuses.size === 0 &&
|
|
771
|
-
(task.status === 'completed' || task.status === 'error' || task.status === 'cancelled')
|
|
772
|
-
) {
|
|
773
|
-
notifiedStatuses.add(task.status);
|
|
774
|
-
task.notifiedStatuses = notifiedStatuses;
|
|
926
|
+
// Recovered tasks (from recoverTasks) have no notifiedStatuses.
|
|
927
|
+
// Assume they were already notified and skip to prevent duplicate notifications.
|
|
928
|
+
if (!task.notifiedStatuses) {
|
|
929
|
+
task.notifiedStatuses = new Set([task.status]);
|
|
775
930
|
return;
|
|
776
931
|
}
|
|
777
932
|
|
|
933
|
+
const notifiedStatuses = task.notifiedStatuses;
|
|
778
934
|
if (notifiedStatuses.has(task.status)) {
|
|
779
935
|
return; // Already notified for this status, skip duplicate
|
|
780
936
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
937
|
+
|
|
938
|
+
// Belt-and-suspenders: rate limit notifications per task+status to 1 per 10s
|
|
939
|
+
const now = Date.now();
|
|
940
|
+
const lastNotifyKey = `${task.id}:${task.status}`;
|
|
941
|
+
const lastTime = this.lastNotifyTimes.get(lastNotifyKey);
|
|
942
|
+
if (lastTime && now - lastTime < 10_000) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
this.lastNotifyTimes.set(lastNotifyKey, now);
|
|
946
|
+
|
|
947
|
+
// Do NOT pre-mark as notified here — if all retries fail, the status
|
|
948
|
+
// must remain unmarked so future retry attempts (via refreshStatuses
|
|
949
|
+
// or Monitor) are not blocked. Mark only on confirmed delivery below.
|
|
784
950
|
|
|
785
951
|
const statusLine = task.status === 'completed' ? 'completed' : task.status;
|
|
786
952
|
const message = `[BACKGROUND TASK ${statusLine.toUpperCase()}]
|
|
@@ -792,21 +958,38 @@ Task ID: ${task.id}
|
|
|
792
958
|
|
|
793
959
|
Use the agentuity_background_output tool with task_id "${task.id}" to view the result.`;
|
|
794
960
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
961
|
+
const maxRetries = 3;
|
|
962
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
963
|
+
try {
|
|
964
|
+
await this.ctx.client.session.prompt({
|
|
965
|
+
path: { id: task.parentSessionId },
|
|
966
|
+
body: {
|
|
967
|
+
parts: [{ type: 'text', text: message }],
|
|
968
|
+
},
|
|
969
|
+
throwOnError: true,
|
|
970
|
+
responseStyle: 'data',
|
|
971
|
+
...this.getClientOverrides(),
|
|
972
|
+
});
|
|
973
|
+
// Mark as notified only AFTER confirmed delivery
|
|
974
|
+
notifiedStatuses.add(task.status);
|
|
975
|
+
task.notifiedStatuses = notifiedStatuses;
|
|
976
|
+
return; // Success
|
|
977
|
+
} catch (error) {
|
|
978
|
+
const errorMsg = extractErrorMessage(error, 'notification failed');
|
|
979
|
+
if (attempt < maxRetries - 1) {
|
|
980
|
+
// Exponential backoff: 1s, 2s, 4s
|
|
981
|
+
await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
982
|
+
if (this.shuttingDown) return;
|
|
983
|
+
} else {
|
|
984
|
+
console.error(
|
|
985
|
+
`[BackgroundManager] Failed to notify parent for task ${task.id} after ${maxRetries} attempts:`,
|
|
986
|
+
errorMsg
|
|
987
|
+
);
|
|
988
|
+
// Safety net: ensure status is NOT marked as notified so future
|
|
989
|
+
// retry attempts (via refreshStatuses or Monitor) are not blocked
|
|
990
|
+
notifiedStatuses.delete(task.status);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
810
993
|
}
|
|
811
994
|
}
|
|
812
995
|
|
|
@@ -895,10 +1078,16 @@ Use the agentuity_background_output tool with task_id "${task.id}" to view the r
|
|
|
895
1078
|
const now = Date.now();
|
|
896
1079
|
for (const task of this.tasks.values()) {
|
|
897
1080
|
if (task.status !== 'pending' && task.status !== 'running') continue;
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1081
|
+
// Use last activity time (last event received) rather than start time.
|
|
1082
|
+
// A task actively doing tool calls every minute should never expire —
|
|
1083
|
+
// only tasks that have gone silent for staleTimeoutMs should be killed.
|
|
1084
|
+
const lastActivity =
|
|
1085
|
+
task.progress?.lastUpdate.getTime() ??
|
|
1086
|
+
task.startedAt?.getTime() ??
|
|
1087
|
+
task.queuedAt?.getTime();
|
|
1088
|
+
if (!lastActivity) continue;
|
|
1089
|
+
if (now - lastActivity > this.config.staleTimeoutMs) {
|
|
1090
|
+
this.failTask(task, 'Background task timed out (no activity).');
|
|
902
1091
|
}
|
|
903
1092
|
}
|
|
904
1093
|
}
|
package/src/background/types.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface TaskProgress {
|
|
|
7
7
|
lastUpdate: Date;
|
|
8
8
|
lastMessage?: string;
|
|
9
9
|
lastMessageAt?: Date;
|
|
10
|
+
/** Number of tool calls currently in-flight (pending/running state) */
|
|
11
|
+
activeToolCallsInFlight: number;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export interface BackgroundTask {
|
|
@@ -27,6 +29,7 @@ export interface BackgroundTask {
|
|
|
27
29
|
concurrencyKey?: string; // Active concurrency slot key
|
|
28
30
|
concurrencyGroup?: string; // Persistent key for re-acquiring on resume
|
|
29
31
|
notifiedStatuses?: Set<BackgroundTaskStatus>; // Tracks statuses already notified to prevent duplicates
|
|
32
|
+
isMonitor?: boolean; // True if this task is an auto-launched Monitor agent
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export interface LaunchInput {
|
package/src/config/loader.ts
CHANGED
|
@@ -146,7 +146,7 @@ const DEFAULT_BLOCKED_COMMANDS = [
|
|
|
146
146
|
|
|
147
147
|
const DEFAULT_BACKGROUND_CONFIG: BackgroundTaskConfig = {
|
|
148
148
|
enabled: true,
|
|
149
|
-
defaultConcurrency:
|
|
149
|
+
defaultConcurrency: 5,
|
|
150
150
|
staleTimeoutMs: 30 * 60 * 1000,
|
|
151
151
|
providerConcurrency: {},
|
|
152
152
|
modelConcurrency: {},
|
|
@@ -199,7 +199,7 @@ function mergeBackgroundConfig(
|
|
|
199
199
|
if (!base && !override) return undefined;
|
|
200
200
|
return {
|
|
201
201
|
enabled: override?.enabled ?? base?.enabled ?? true,
|
|
202
|
-
defaultConcurrency: override?.defaultConcurrency ?? base?.defaultConcurrency ??
|
|
202
|
+
defaultConcurrency: override?.defaultConcurrency ?? base?.defaultConcurrency ?? 5,
|
|
203
203
|
staleTimeoutMs: override?.staleTimeoutMs ?? base?.staleTimeoutMs ?? 30 * 60 * 1000,
|
|
204
204
|
providerConcurrency: {
|
|
205
205
|
...(base?.providerConcurrency ?? {}),
|