@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.
Files changed (137) hide show
  1. package/dist/agents/architect.d.ts +1 -1
  2. package/dist/agents/architect.d.ts.map +1 -1
  3. package/dist/agents/architect.js +30 -33
  4. package/dist/agents/architect.js.map +1 -1
  5. package/dist/agents/builder.d.ts +1 -1
  6. package/dist/agents/builder.d.ts.map +1 -1
  7. package/dist/agents/builder.js +53 -60
  8. package/dist/agents/builder.js.map +1 -1
  9. package/dist/agents/expert-backend.d.ts +1 -1
  10. package/dist/agents/expert-backend.d.ts.map +1 -1
  11. package/dist/agents/expert-backend.js +32 -40
  12. package/dist/agents/expert-backend.js.map +1 -1
  13. package/dist/agents/expert-frontend.d.ts +1 -1
  14. package/dist/agents/expert-frontend.d.ts.map +1 -1
  15. package/dist/agents/expert-frontend.js +18 -24
  16. package/dist/agents/expert-frontend.js.map +1 -1
  17. package/dist/agents/expert-ops.d.ts +1 -1
  18. package/dist/agents/expert-ops.d.ts.map +1 -1
  19. package/dist/agents/expert-ops.js +37 -51
  20. package/dist/agents/expert-ops.js.map +1 -1
  21. package/dist/agents/expert.d.ts +1 -1
  22. package/dist/agents/expert.d.ts.map +1 -1
  23. package/dist/agents/expert.js +33 -43
  24. package/dist/agents/expert.js.map +1 -1
  25. package/dist/agents/lead.d.ts +1 -1
  26. package/dist/agents/lead.d.ts.map +1 -1
  27. package/dist/agents/lead.js +179 -222
  28. package/dist/agents/lead.js.map +1 -1
  29. package/dist/agents/memory.d.ts +1 -1
  30. package/dist/agents/memory.d.ts.map +1 -1
  31. package/dist/agents/memory.js +62 -90
  32. package/dist/agents/memory.js.map +1 -1
  33. package/dist/agents/monitor.d.ts +1 -1
  34. package/dist/agents/monitor.d.ts.map +1 -1
  35. package/dist/agents/monitor.js +84 -44
  36. package/dist/agents/monitor.js.map +1 -1
  37. package/dist/agents/product.d.ts +1 -1
  38. package/dist/agents/product.d.ts.map +1 -1
  39. package/dist/agents/product.js +16 -22
  40. package/dist/agents/product.js.map +1 -1
  41. package/dist/agents/reviewer.d.ts +1 -1
  42. package/dist/agents/reviewer.d.ts.map +1 -1
  43. package/dist/agents/reviewer.js +15 -27
  44. package/dist/agents/reviewer.js.map +1 -1
  45. package/dist/agents/runner.d.ts +1 -1
  46. package/dist/agents/runner.d.ts.map +1 -1
  47. package/dist/agents/runner.js +52 -76
  48. package/dist/agents/runner.js.map +1 -1
  49. package/dist/agents/scout.d.ts +1 -1
  50. package/dist/agents/scout.d.ts.map +1 -1
  51. package/dist/agents/scout.js +42 -43
  52. package/dist/agents/scout.js.map +1 -1
  53. package/dist/agents/types.d.ts +8 -0
  54. package/dist/agents/types.d.ts.map +1 -1
  55. package/dist/background/manager.d.ts +18 -0
  56. package/dist/background/manager.d.ts.map +1 -1
  57. package/dist/background/manager.js +201 -33
  58. package/dist/background/manager.js.map +1 -1
  59. package/dist/background/types.d.ts +3 -0
  60. package/dist/background/types.d.ts.map +1 -1
  61. package/dist/config/loader.js +2 -2
  62. package/dist/plugin/hooks/cadence.d.ts +3 -1
  63. package/dist/plugin/hooks/cadence.d.ts.map +1 -1
  64. package/dist/plugin/hooks/cadence.js +167 -70
  65. package/dist/plugin/hooks/cadence.js.map +1 -1
  66. package/dist/plugin/hooks/compaction-utils.d.ts +48 -0
  67. package/dist/plugin/hooks/compaction-utils.d.ts.map +1 -0
  68. package/dist/plugin/hooks/compaction-utils.js +259 -0
  69. package/dist/plugin/hooks/compaction-utils.js.map +1 -0
  70. package/dist/plugin/hooks/completion.d.ts +14 -0
  71. package/dist/plugin/hooks/completion.d.ts.map +1 -0
  72. package/dist/plugin/hooks/completion.js +45 -0
  73. package/dist/plugin/hooks/completion.js.map +1 -0
  74. package/dist/plugin/hooks/params.d.ts +47 -2
  75. package/dist/plugin/hooks/params.d.ts.map +1 -1
  76. package/dist/plugin/hooks/params.js +82 -1
  77. package/dist/plugin/hooks/params.js.map +1 -1
  78. package/dist/plugin/hooks/session-memory.d.ts +2 -1
  79. package/dist/plugin/hooks/session-memory.d.ts.map +1 -1
  80. package/dist/plugin/hooks/session-memory.js +101 -48
  81. package/dist/plugin/hooks/session-memory.js.map +1 -1
  82. package/dist/plugin/hooks/tools.d.ts.map +1 -1
  83. package/dist/plugin/hooks/tools.js +26 -1
  84. package/dist/plugin/hooks/tools.js.map +1 -1
  85. package/dist/plugin/plugin.d.ts.map +1 -1
  86. package/dist/plugin/plugin.js +38 -9
  87. package/dist/plugin/plugin.js.map +1 -1
  88. package/dist/sqlite/index.d.ts +1 -1
  89. package/dist/sqlite/index.d.ts.map +1 -1
  90. package/dist/sqlite/queries.d.ts +1 -0
  91. package/dist/sqlite/queries.d.ts.map +1 -1
  92. package/dist/sqlite/queries.js +4 -0
  93. package/dist/sqlite/queries.js.map +1 -1
  94. package/dist/sqlite/reader.d.ts +11 -1
  95. package/dist/sqlite/reader.d.ts.map +1 -1
  96. package/dist/sqlite/reader.js +62 -0
  97. package/dist/sqlite/reader.js.map +1 -1
  98. package/dist/sqlite/types.d.ts +40 -0
  99. package/dist/sqlite/types.d.ts.map +1 -1
  100. package/dist/tools/background.d.ts.map +1 -1
  101. package/dist/tools/background.js +15 -0
  102. package/dist/tools/background.js.map +1 -1
  103. package/dist/types.d.ts +46 -0
  104. package/dist/types.d.ts.map +1 -1
  105. package/dist/types.js +10 -0
  106. package/dist/types.js.map +1 -1
  107. package/package.json +3 -3
  108. package/src/agents/architect.ts +30 -33
  109. package/src/agents/builder.ts +53 -60
  110. package/src/agents/expert-backend.ts +32 -40
  111. package/src/agents/expert-frontend.ts +18 -24
  112. package/src/agents/expert-ops.ts +37 -51
  113. package/src/agents/expert.ts +33 -43
  114. package/src/agents/lead.ts +179 -222
  115. package/src/agents/memory.ts +62 -90
  116. package/src/agents/monitor.ts +84 -44
  117. package/src/agents/product.ts +16 -22
  118. package/src/agents/reviewer.ts +15 -27
  119. package/src/agents/runner.ts +52 -76
  120. package/src/agents/scout.ts +42 -43
  121. package/src/agents/types.ts +8 -0
  122. package/src/background/manager.ts +227 -38
  123. package/src/background/types.ts +3 -0
  124. package/src/config/loader.ts +2 -2
  125. package/src/plugin/hooks/cadence.ts +188 -74
  126. package/src/plugin/hooks/compaction-utils.ts +291 -0
  127. package/src/plugin/hooks/completion.ts +61 -0
  128. package/src/plugin/hooks/params.ts +107 -2
  129. package/src/plugin/hooks/session-memory.ts +113 -47
  130. package/src/plugin/hooks/tools.ts +32 -1
  131. package/src/plugin/plugin.ts +54 -10
  132. package/src/sqlite/index.ts +4 -0
  133. package/src/sqlite/queries.ts +5 -0
  134. package/src/sqlite/reader.ts +69 -0
  135. package/src/sqlite/types.ts +40 -0
  136. package/src/tools/background.ts +28 -0
  137. 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: 1,
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?.sessionId) return undefined;
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
- this.failTask(task, error ?? 'Session error.');
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
- // Prevent duplicate notifications for the same task+status combination
763
- // This guards against OpenCode firing multiple events for the same status transition
764
- const notifiedStatuses = task.notifiedStatuses ?? new Set();
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
- // Mark as notified BEFORE sending to prevent race conditions
782
- notifiedStatuses.add(task.status);
783
- task.notifiedStatuses = notifiedStatuses;
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
- try {
796
- await this.ctx.client.session.prompt({
797
- path: { id: task.parentSessionId },
798
- body: {
799
- parts: [{ type: 'text', text: message }],
800
- },
801
- throwOnError: true,
802
- responseStyle: 'data',
803
- ...this.getClientOverrides(),
804
- });
805
- } catch (error) {
806
- console.error(
807
- `[BackgroundManager] Failed to notify parent for task ${task.id}:`,
808
- extractErrorMessage(error, 'notification failed')
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
- const start = task.startedAt?.getTime() ?? task.queuedAt?.getTime();
899
- if (!start) continue;
900
- if (now - start > this.config.staleTimeoutMs) {
901
- this.failTask(task, 'Background task timed out.');
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
  }
@@ -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 {
@@ -146,7 +146,7 @@ const DEFAULT_BLOCKED_COMMANDS = [
146
146
 
147
147
  const DEFAULT_BACKGROUND_CONFIG: BackgroundTaskConfig = {
148
148
  enabled: true,
149
- defaultConcurrency: 1,
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 ?? 1,
202
+ defaultConcurrency: override?.defaultConcurrency ?? base?.defaultConcurrency ?? 5,
203
203
  staleTimeoutMs: override?.staleTimeoutMs ?? base?.staleTimeoutMs ?? 30 * 60 * 1000,
204
204
  providerConcurrency: {
205
205
  ...(base?.providerConcurrency ?? {}),