mcp-subagents-opencode 1.0.0

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 (219) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +602 -0
  3. package/build/config/timeouts.d.ts +9 -0
  4. package/build/config/timeouts.d.ts.map +1 -0
  5. package/build/config/timeouts.js +18 -0
  6. package/build/config/timeouts.js.map +1 -0
  7. package/build/helpers.d.ts +6 -0
  8. package/build/helpers.d.ts.map +1 -0
  9. package/build/helpers.js +47 -0
  10. package/build/helpers.js.map +1 -0
  11. package/build/index.d.ts +3 -0
  12. package/build/index.d.ts.map +1 -0
  13. package/build/index.js +245 -0
  14. package/build/index.js.map +1 -0
  15. package/build/models.d.ts +32 -0
  16. package/build/models.d.ts.map +1 -0
  17. package/build/models.js +58 -0
  18. package/build/models.js.map +1 -0
  19. package/build/server/register-notifications.d.ts +3 -0
  20. package/build/server/register-notifications.d.ts.map +1 -0
  21. package/build/server/register-notifications.js +77 -0
  22. package/build/server/register-notifications.js.map +1 -0
  23. package/build/server/register-resources.d.ts +3 -0
  24. package/build/server/register-resources.d.ts.map +1 -0
  25. package/build/server/register-resources.js +210 -0
  26. package/build/server/register-resources.js.map +1 -0
  27. package/build/server/register-retry-execution.d.ts +2 -0
  28. package/build/server/register-retry-execution.d.ts.map +1 -0
  29. package/build/server/register-retry-execution.js +28 -0
  30. package/build/server/register-retry-execution.js.map +1 -0
  31. package/build/server/register-tasks.d.ts +3 -0
  32. package/build/server/register-tasks.d.ts.map +1 -0
  33. package/build/server/register-tasks.js +52 -0
  34. package/build/server/register-tasks.js.map +1 -0
  35. package/build/server/register-tools.d.ts +3 -0
  36. package/build/server/register-tools.d.ts.map +1 -0
  37. package/build/server/register-tools.js +32 -0
  38. package/build/server/register-tools.js.map +1 -0
  39. package/build/server/resource-helpers.d.ts +21 -0
  40. package/build/server/resource-helpers.d.ts.map +1 -0
  41. package/build/server/resource-helpers.js +84 -0
  42. package/build/server/resource-helpers.js.map +1 -0
  43. package/build/services/account-manager.d.ts +88 -0
  44. package/build/services/account-manager.d.ts.map +1 -0
  45. package/build/services/account-manager.js +239 -0
  46. package/build/services/account-manager.js.map +1 -0
  47. package/build/services/claude-code-runner.d.ts +15 -0
  48. package/build/services/claude-code-runner.d.ts.map +1 -0
  49. package/build/services/claude-code-runner.js +475 -0
  50. package/build/services/claude-code-runner.js.map +1 -0
  51. package/build/services/client-context.d.ts +31 -0
  52. package/build/services/client-context.d.ts.map +1 -0
  53. package/build/services/client-context.js +44 -0
  54. package/build/services/client-context.js.map +1 -0
  55. package/build/services/exhaustion-fallback.d.ts +27 -0
  56. package/build/services/exhaustion-fallback.d.ts.map +1 -0
  57. package/build/services/exhaustion-fallback.js +30 -0
  58. package/build/services/exhaustion-fallback.js.map +1 -0
  59. package/build/services/fallback-orchestrator.d.ts +16 -0
  60. package/build/services/fallback-orchestrator.d.ts.map +1 -0
  61. package/build/services/fallback-orchestrator.js +48 -0
  62. package/build/services/fallback-orchestrator.js.map +1 -0
  63. package/build/services/opencode-client.d.ts +40 -0
  64. package/build/services/opencode-client.d.ts.map +1 -0
  65. package/build/services/opencode-client.js +147 -0
  66. package/build/services/opencode-client.js.map +1 -0
  67. package/build/services/opencode-spawner.d.ts +56 -0
  68. package/build/services/opencode-spawner.d.ts.map +1 -0
  69. package/build/services/opencode-spawner.js +426 -0
  70. package/build/services/opencode-spawner.js.map +1 -0
  71. package/build/services/output-file.d.ts +24 -0
  72. package/build/services/output-file.d.ts.map +1 -0
  73. package/build/services/output-file.js +90 -0
  74. package/build/services/output-file.js.map +1 -0
  75. package/build/services/progress-registry.d.ts +12 -0
  76. package/build/services/progress-registry.d.ts.map +1 -0
  77. package/build/services/progress-registry.js +97 -0
  78. package/build/services/progress-registry.js.map +1 -0
  79. package/build/services/question-registry.d.ts +79 -0
  80. package/build/services/question-registry.d.ts.map +1 -0
  81. package/build/services/question-registry.js +249 -0
  82. package/build/services/question-registry.js.map +1 -0
  83. package/build/services/retry-queue.d.ts +41 -0
  84. package/build/services/retry-queue.d.ts.map +1 -0
  85. package/build/services/retry-queue.js +195 -0
  86. package/build/services/retry-queue.js.map +1 -0
  87. package/build/services/sdk-client-manager.d.ts +149 -0
  88. package/build/services/sdk-client-manager.d.ts.map +1 -0
  89. package/build/services/sdk-client-manager.js +632 -0
  90. package/build/services/sdk-client-manager.js.map +1 -0
  91. package/build/services/sdk-session-adapter.d.ts +203 -0
  92. package/build/services/sdk-session-adapter.d.ts.map +1 -0
  93. package/build/services/sdk-session-adapter.js +1088 -0
  94. package/build/services/sdk-session-adapter.js.map +1 -0
  95. package/build/services/sdk-spawner.d.ts +42 -0
  96. package/build/services/sdk-spawner.d.ts.map +1 -0
  97. package/build/services/sdk-spawner.js +488 -0
  98. package/build/services/sdk-spawner.js.map +1 -0
  99. package/build/services/session-hooks.d.ts +24 -0
  100. package/build/services/session-hooks.d.ts.map +1 -0
  101. package/build/services/session-hooks.js +130 -0
  102. package/build/services/session-hooks.js.map +1 -0
  103. package/build/services/session-snapshot.d.ts +19 -0
  104. package/build/services/session-snapshot.d.ts.map +1 -0
  105. package/build/services/session-snapshot.js +203 -0
  106. package/build/services/session-snapshot.js.map +1 -0
  107. package/build/services/subscription-registry.d.ts +12 -0
  108. package/build/services/subscription-registry.d.ts.map +1 -0
  109. package/build/services/subscription-registry.js +27 -0
  110. package/build/services/subscription-registry.js.map +1 -0
  111. package/build/services/task-manager.d.ts +150 -0
  112. package/build/services/task-manager.d.ts.map +1 -0
  113. package/build/services/task-manager.js +765 -0
  114. package/build/services/task-manager.js.map +1 -0
  115. package/build/services/task-persistence.d.ts +29 -0
  116. package/build/services/task-persistence.d.ts.map +1 -0
  117. package/build/services/task-persistence.js +159 -0
  118. package/build/services/task-persistence.js.map +1 -0
  119. package/build/services/task-status-mapper.d.ts +21 -0
  120. package/build/services/task-status-mapper.d.ts.map +1 -0
  121. package/build/services/task-status-mapper.js +171 -0
  122. package/build/services/task-status-mapper.js.map +1 -0
  123. package/build/templates/index.d.ts +22 -0
  124. package/build/templates/index.d.ts.map +1 -0
  125. package/build/templates/index.js +147 -0
  126. package/build/templates/index.js.map +1 -0
  127. package/build/templates/overlays/coder-csharp.mdx +58 -0
  128. package/build/templates/overlays/coder-go.mdx +53 -0
  129. package/build/templates/overlays/coder-java.mdx +54 -0
  130. package/build/templates/overlays/coder-kotlin.mdx +56 -0
  131. package/build/templates/overlays/coder-nextjs.mdx +65 -0
  132. package/build/templates/overlays/coder-python.mdx +53 -0
  133. package/build/templates/overlays/coder-react.mdx +55 -0
  134. package/build/templates/overlays/coder-ruby.mdx +59 -0
  135. package/build/templates/overlays/coder-rust.mdx +48 -0
  136. package/build/templates/overlays/coder-supabase.mdx +268 -0
  137. package/build/templates/overlays/coder-supastarter.mdx +313 -0
  138. package/build/templates/overlays/coder-swift.mdx +56 -0
  139. package/build/templates/overlays/coder-tauri.mdx +566 -0
  140. package/build/templates/overlays/coder-triggerdev.mdx +296 -0
  141. package/build/templates/overlays/coder-typescript.mdx +45 -0
  142. package/build/templates/overlays/coder-vue.mdx +62 -0
  143. package/build/templates/overlays/planner-architecture.mdx +78 -0
  144. package/build/templates/overlays/planner-bugfix.mdx +36 -0
  145. package/build/templates/overlays/planner-feature.mdx +38 -0
  146. package/build/templates/overlays/planner-migration.mdx +50 -0
  147. package/build/templates/overlays/planner-refactor.mdx +57 -0
  148. package/build/templates/overlays/researcher-library.mdx +59 -0
  149. package/build/templates/overlays/researcher-performance.mdx +68 -0
  150. package/build/templates/overlays/researcher-security.mdx +86 -0
  151. package/build/templates/overlays/tester-graphql.mdx +191 -0
  152. package/build/templates/overlays/tester-playwright.mdx +621 -0
  153. package/build/templates/overlays/tester-rest.mdx +101 -0
  154. package/build/templates/overlays/tester-suite.mdx +177 -0
  155. package/build/templates/super-coder.mdx +529 -0
  156. package/build/templates/super-planner.mdx +568 -0
  157. package/build/templates/super-researcher.mdx +406 -0
  158. package/build/templates/super-tester.mdx +243 -0
  159. package/build/tools/answer-question.d.ts +30 -0
  160. package/build/tools/answer-question.d.ts.map +1 -0
  161. package/build/tools/answer-question.js +108 -0
  162. package/build/tools/answer-question.js.map +1 -0
  163. package/build/tools/cancel-task.d.ts +44 -0
  164. package/build/tools/cancel-task.d.ts.map +1 -0
  165. package/build/tools/cancel-task.js +144 -0
  166. package/build/tools/cancel-task.js.map +1 -0
  167. package/build/tools/send-message.d.ts +39 -0
  168. package/build/tools/send-message.d.ts.map +1 -0
  169. package/build/tools/send-message.js +124 -0
  170. package/build/tools/send-message.js.map +1 -0
  171. package/build/tools/shared-spawn.d.ts +56 -0
  172. package/build/tools/shared-spawn.d.ts.map +1 -0
  173. package/build/tools/shared-spawn.js +114 -0
  174. package/build/tools/shared-spawn.js.map +1 -0
  175. package/build/tools/spawn-agent.d.ts +85 -0
  176. package/build/tools/spawn-agent.d.ts.map +1 -0
  177. package/build/tools/spawn-agent.js +133 -0
  178. package/build/tools/spawn-agent.js.map +1 -0
  179. package/build/tools/spawn-coder.d.ts +70 -0
  180. package/build/tools/spawn-coder.d.ts.map +1 -0
  181. package/build/tools/spawn-coder.js +71 -0
  182. package/build/tools/spawn-coder.js.map +1 -0
  183. package/build/tools/spawn-planner.d.ts +70 -0
  184. package/build/tools/spawn-planner.d.ts.map +1 -0
  185. package/build/tools/spawn-planner.js +71 -0
  186. package/build/tools/spawn-planner.js.map +1 -0
  187. package/build/tools/spawn-researcher.d.ts +70 -0
  188. package/build/tools/spawn-researcher.d.ts.map +1 -0
  189. package/build/tools/spawn-researcher.js +70 -0
  190. package/build/tools/spawn-researcher.js.map +1 -0
  191. package/build/tools/spawn-task.d.ts +74 -0
  192. package/build/tools/spawn-task.d.ts.map +1 -0
  193. package/build/tools/spawn-task.js +107 -0
  194. package/build/tools/spawn-task.js.map +1 -0
  195. package/build/tools/spawn-tester.d.ts +70 -0
  196. package/build/tools/spawn-tester.d.ts.map +1 -0
  197. package/build/tools/spawn-tester.js +69 -0
  198. package/build/tools/spawn-tester.js.map +1 -0
  199. package/build/types.d.ts +101 -0
  200. package/build/types.d.ts.map +1 -0
  201. package/build/types.js +28 -0
  202. package/build/types.js.map +1 -0
  203. package/build/utils/brief-validator.d.ts +30 -0
  204. package/build/utils/brief-validator.d.ts.map +1 -0
  205. package/build/utils/brief-validator.js +254 -0
  206. package/build/utils/brief-validator.js.map +1 -0
  207. package/build/utils/format.d.ts +34 -0
  208. package/build/utils/format.d.ts.map +1 -0
  209. package/build/utils/format.js +55 -0
  210. package/build/utils/format.js.map +1 -0
  211. package/build/utils/sanitize.d.ts +240 -0
  212. package/build/utils/sanitize.d.ts.map +1 -0
  213. package/build/utils/sanitize.js +89 -0
  214. package/build/utils/sanitize.js.map +1 -0
  215. package/build/utils/task-id-generator.d.ts +10 -0
  216. package/build/utils/task-id-generator.d.ts.map +1 -0
  217. package/build/utils/task-id-generator.js +22 -0
  218. package/build/utils/task-id-generator.js.map +1 -0
  219. package/package.json +62 -0
@@ -0,0 +1,765 @@
1
+ import { generateTaskId, normalizeTaskId } from '../utils/task-id-generator.js';
2
+ import { TaskStatus, TERMINAL_STATUSES } from '../types.js';
3
+ import { saveTasks, loadTasks } from './task-persistence.js';
4
+ import { shouldRetryNow, hasExceededMaxRetries } from './retry-queue.js';
5
+ import { TASK_STALL_WARN_MS, TASK_TTL_MS } from '../config/timeouts.js';
6
+ import { createOutputFile, appendToOutputFile, finalizeOutputFile } from './output-file.js';
7
+ const MAX_TASKS = 100;
8
+ /**
9
+ * Check if adding dependencies would create a circular dependency
10
+ * @param newTaskId - The ID of the task being created
11
+ * @param dependsOn - Array of task IDs this task depends on
12
+ * @param tasks - Map of all existing tasks
13
+ * @returns true if circular dependency would be created
14
+ */
15
+ function hasCircularDependency(newTaskId, dependsOn, tasks) {
16
+ const visited = new Set();
17
+ const toCheck = [...dependsOn];
18
+ while (toCheck.length > 0) {
19
+ const depId = toCheck.pop();
20
+ const normalizedDepId = normalizeTaskId(depId);
21
+ if (normalizedDepId === normalizeTaskId(newTaskId)) {
22
+ return true; // Circular dependency found
23
+ }
24
+ if (visited.has(normalizedDepId)) {
25
+ continue;
26
+ }
27
+ visited.add(normalizedDepId);
28
+ const depTask = tasks.get(normalizedDepId);
29
+ if (depTask?.dependsOn) {
30
+ toCheck.push(...depTask.dependsOn);
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+ /**
36
+ * Check if all dependencies for a task are satisfied (completed successfully)
37
+ */
38
+ function areDependenciesSatisfied(task, tasks) {
39
+ if (!task.dependsOn || task.dependsOn.length === 0) {
40
+ return { satisfied: true, missing: [], failed: [], pending: [] };
41
+ }
42
+ const missing = [];
43
+ const failed = [];
44
+ const pending = [];
45
+ for (const depId of task.dependsOn) {
46
+ const normalizedDepId = normalizeTaskId(depId);
47
+ const depTask = tasks.get(normalizedDepId);
48
+ if (!depTask) {
49
+ missing.push(depId);
50
+ }
51
+ else if (depTask.status === TaskStatus.COMPLETED) {
52
+ // Good - dependency completed successfully
53
+ }
54
+ else if (depTask.status === TaskStatus.FAILED || depTask.status === TaskStatus.CANCELLED) {
55
+ failed.push(depId);
56
+ }
57
+ else {
58
+ // PENDING, WAITING, RUNNING, RATE_LIMITED
59
+ pending.push(depId);
60
+ }
61
+ }
62
+ const satisfied = missing.length === 0 && failed.length === 0 && pending.length === 0;
63
+ return { satisfied, missing, failed, pending };
64
+ }
65
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
66
+ const HEALTH_CHECK_INTERVAL_MS = 10 * 1000; // Check session health every 10 seconds
67
+ const MAX_OUTPUT_LINES = 2000;
68
+ // Re-export from types.ts for backward compatibility
69
+ export { TERMINAL_STATUSES } from '../types.js';
70
+ /**
71
+ * Check if a session is still active.
72
+ */
73
+ export function isSessionActive(task) {
74
+ return task.session !== undefined && task.status === TaskStatus.RUNNING;
75
+ }
76
+ class TaskManager {
77
+ tasks = new Map();
78
+ cleanupInterval = null;
79
+ healthCheckInterval = null;
80
+ rateLimitTimer = null;
81
+ currentCwd = null;
82
+ persistTimeout = null;
83
+ persistDebounceMs = 100;
84
+ outputPersistDebounceMs = 1000;
85
+ lastPersistTrigger = 'state';
86
+ retryCallback = null;
87
+ executeCallback = null;
88
+ statusChangeCallback = null;
89
+ outputCallback = null;
90
+ taskCreatedCallback = null;
91
+ taskDeletedCallback = null;
92
+ constructor() {
93
+ this.startCleanup();
94
+ this.startHealthCheck();
95
+ }
96
+ /**
97
+ * Set the current workspace and load persisted tasks
98
+ * Also triggers auto-retry for rate-limited tasks
99
+ */
100
+ setCwd(cwd) {
101
+ this.currentCwd = cwd;
102
+ const loadedTasks = loadTasks(cwd);
103
+ // Load tasks into the map
104
+ for (const task of loadedTasks) {
105
+ const normalizedId = normalizeTaskId(task.id);
106
+ this.tasks.set(normalizedId, task);
107
+ }
108
+ // Run cleanup on loaded tasks (removes expired ones)
109
+ this.cleanup();
110
+ // Process rate-limited tasks for auto-retry
111
+ this.processRateLimitedTasks();
112
+ // Process waiting tasks whose dependencies are already satisfied
113
+ this.processWaitingTasks();
114
+ // Persist recovered state so restart recovery isn't repeated forever
115
+ if (loadedTasks.length > 0) {
116
+ this.schedulePersist('state');
117
+ }
118
+ }
119
+ /**
120
+ * Register a callback to be called when a rate-limited task should be retried
121
+ * Callback should return the new task ID
122
+ */
123
+ onRetry(callback) {
124
+ this.retryCallback = callback;
125
+ this.scheduleRateLimitCheck();
126
+ }
127
+ /**
128
+ * Register a callback to execute a waiting task when dependencies are satisfied
129
+ */
130
+ onExecute(callback) {
131
+ this.executeCallback = callback;
132
+ }
133
+ onStatusChange(callback) {
134
+ this.statusChangeCallback = callback;
135
+ }
136
+ onOutput(callback) {
137
+ this.outputCallback = callback;
138
+ }
139
+ onTaskCreated(callback) {
140
+ this.taskCreatedCallback = callback;
141
+ }
142
+ onTaskDeleted(callback) {
143
+ this.taskDeletedCallback = callback;
144
+ }
145
+ /**
146
+ * Process waiting tasks and start those with satisfied dependencies
147
+ */
148
+ processWaitingTasks() {
149
+ const waitingTasks = Array.from(this.tasks.values())
150
+ .filter(t => t.status === TaskStatus.WAITING);
151
+ if (waitingTasks.length === 0) {
152
+ return;
153
+ }
154
+ for (const task of waitingTasks) {
155
+ const { satisfied } = areDependenciesSatisfied(task, this.tasks);
156
+ if (satisfied && this.executeCallback) {
157
+ console.error(`[task-manager] Dependencies satisfied for ${task.id}, starting execution`);
158
+ const updated = this.updateTask(task.id, { status: TaskStatus.PENDING });
159
+ if (!updated) {
160
+ console.error(`[task-manager] Task ${task.id} was deleted before it could start`);
161
+ continue;
162
+ }
163
+ this.executeCallback(updated).catch(err => {
164
+ console.error(`[task-manager] Failed to execute task ${task.id}:`, err);
165
+ });
166
+ }
167
+ }
168
+ }
169
+ /**
170
+ * Validate dependencies for a new task
171
+ * Returns error message if invalid, null if valid
172
+ */
173
+ validateDependencies(dependsOn, newTaskId) {
174
+ if (!dependsOn || dependsOn.length === 0) {
175
+ return null;
176
+ }
177
+ // Check if all dependencies exist
178
+ for (const depId of dependsOn) {
179
+ const normalizedDepId = normalizeTaskId(depId);
180
+ if (!this.tasks.has(normalizedDepId)) {
181
+ return `Dependency task '${depId}' not found`;
182
+ }
183
+ }
184
+ // Check for circular dependencies (only if newTaskId provided)
185
+ if (newTaskId && hasCircularDependency(newTaskId, dependsOn, this.tasks)) {
186
+ return 'Circular dependency detected';
187
+ }
188
+ return null;
189
+ }
190
+ /**
191
+ * Get dependency status info for a task
192
+ */
193
+ getDependencyStatus(taskId) {
194
+ const task = this.getTask(taskId);
195
+ if (!task || !task.dependsOn) {
196
+ return null;
197
+ }
198
+ return areDependenciesSatisfied(task, this.tasks);
199
+ }
200
+ /**
201
+ * Force start a waiting task, bypassing failed/missing dependencies
202
+ */
203
+ async forceStartTask(taskId) {
204
+ const normalizedId = normalizeTaskId(taskId);
205
+ const task = this.tasks.get(normalizedId);
206
+ if (!task) {
207
+ return { success: false, error: 'Task not found' };
208
+ }
209
+ if (task.status !== TaskStatus.WAITING) {
210
+ return { success: false, error: `Task is not waiting (status: ${task.status})` };
211
+ }
212
+ if (!this.executeCallback) {
213
+ return { success: false, error: 'No execute callback registered' };
214
+ }
215
+ const bypassedDeps = task.dependsOn || [];
216
+ // Clear dependencies so retries won't re-block
217
+ task.dependsOn = [];
218
+ this.schedulePersist('state');
219
+ console.error(`[task-manager] Force starting ${task.id}, bypassing deps: ${bypassedDeps.join(', ')}`);
220
+ // Execute the task
221
+ await this.executeCallback(task);
222
+ return {
223
+ success: true,
224
+ taskId: task.id,
225
+ bypassedDeps,
226
+ };
227
+ }
228
+ /**
229
+ * Process rate-limited tasks and trigger retries for those ready
230
+ */
231
+ processRateLimitedTasks() {
232
+ const rateLimitedTasks = Array.from(this.tasks.values())
233
+ .filter(t => t.status === TaskStatus.RATE_LIMITED);
234
+ if (rateLimitedTasks.length === 0) {
235
+ return;
236
+ }
237
+ console.error(`[task-manager] Found ${rateLimitedTasks.length} rate-limited task(s)`);
238
+ for (const task of rateLimitedTasks) {
239
+ // Check if max retries exceeded
240
+ if (hasExceededMaxRetries(task)) {
241
+ console.error(`[task-manager] Task ${task.id} exceeded max retries, marking as failed`);
242
+ this.updateTask(task.id, {
243
+ status: TaskStatus.FAILED,
244
+ error: `Max retries (${task.retryInfo?.maxRetries}) exceeded for rate limit`,
245
+ endTime: new Date().toISOString(),
246
+ exitCode: 1,
247
+ });
248
+ continue;
249
+ }
250
+ // Check if ready for retry
251
+ if (shouldRetryNow(task)) {
252
+ console.error(`[task-manager] Auto-retrying task ${task.id} (attempt ${(task.retryInfo?.retryCount ?? 0) + 1})`);
253
+ if (this.retryCallback) {
254
+ // Mark original task as failed (retried) - new task will be created
255
+ this.updateTask(task.id, {
256
+ status: TaskStatus.FAILED,
257
+ error: `Auto-retried as new task (attempt ${(task.retryInfo?.retryCount ?? 0) + 1}/${task.retryInfo?.maxRetries ?? 6})`,
258
+ endTime: new Date().toISOString(),
259
+ exitCode: 1,
260
+ });
261
+ this.retryCallback(task).catch(err => {
262
+ console.error(`[task-manager] Auto-retry failed for task ${task.id}:`, err);
263
+ });
264
+ }
265
+ else {
266
+ console.error(`[task-manager] No retry callback registered, task ${task.id} will wait`);
267
+ }
268
+ }
269
+ else {
270
+ const nextRetry = task.retryInfo?.nextRetryTime;
271
+ const waitMs = nextRetry ? new Date(nextRetry).getTime() - Date.now() : 0;
272
+ const waitMin = Math.ceil(waitMs / 60000);
273
+ console.error(`[task-manager] Task ${task.id} not ready for retry, waiting ${waitMin} more minutes`);
274
+ }
275
+ }
276
+ // Schedule next check based on soonest retry time
277
+ this.scheduleRateLimitCheck();
278
+ }
279
+ /**
280
+ * Schedule the next rate-limit retry check based on earliest nextRetryTime.
281
+ */
282
+ scheduleRateLimitCheck() {
283
+ if (this.rateLimitTimer) {
284
+ clearTimeout(this.rateLimitTimer);
285
+ this.rateLimitTimer = null;
286
+ }
287
+ if (!this.retryCallback) {
288
+ return;
289
+ }
290
+ const nextTimes = Array.from(this.tasks.values())
291
+ .filter(t => t.status === TaskStatus.RATE_LIMITED && t.retryInfo?.nextRetryTime)
292
+ .map(t => new Date(t.retryInfo.nextRetryTime).getTime())
293
+ .filter(t => Number.isFinite(t));
294
+ if (nextTimes.length === 0) {
295
+ return;
296
+ }
297
+ const nextAt = Math.min(...nextTimes);
298
+ const delayMs = Math.max(0, nextAt - Date.now());
299
+ this.rateLimitTimer = setTimeout(() => {
300
+ this.rateLimitTimer = null;
301
+ this.processRateLimitedTasks();
302
+ }, delayMs);
303
+ }
304
+ /**
305
+ * Get all rate-limited tasks
306
+ */
307
+ getRateLimitedTasks() {
308
+ return Array.from(this.tasks.values())
309
+ .filter(t => t.status === TaskStatus.RATE_LIMITED);
310
+ }
311
+ /**
312
+ * Expedite all rate-limited tasks by moving their next retry time up.
313
+ * Called after recovery so stalled tasks benefit.
314
+ * Tasks are staggered to avoid thundering herd.
315
+ */
316
+ expediteRateLimitedTasks(baseDelayMs = 5000) {
317
+ const rateLimitedTasks = Array.from(this.tasks.values())
318
+ .filter(t => t.status === TaskStatus.RATE_LIMITED && t.retryInfo);
319
+ if (rateLimitedTasks.length === 0) {
320
+ return;
321
+ }
322
+ console.error(`[task-manager] Expediting ${rateLimitedTasks.length} rate-limited task(s) after account switch`);
323
+ let delay = baseDelayMs;
324
+ for (const task of rateLimitedTasks) {
325
+ if (task.retryInfo) {
326
+ this.updateTask(task.id, {
327
+ retryInfo: {
328
+ ...task.retryInfo,
329
+ nextRetryTime: new Date(Date.now() + delay).toISOString(),
330
+ },
331
+ });
332
+ delay += 2000; // Stagger by 2 seconds per task
333
+ }
334
+ }
335
+ // Schedule retry processing based on updated nextRetryTime
336
+ this.scheduleRateLimitCheck();
337
+ }
338
+ /**
339
+ * Clear all tasks from memory (used by clear_tasks tool)
340
+ * Clears all tasks from memory and aborts running sessions.
341
+ */
342
+ async clearAllTasks() {
343
+ const count = this.tasks.size;
344
+ const abortPromises = [];
345
+ for (const task of this.tasks.values()) {
346
+ if (task.session && task.status === TaskStatus.RUNNING) {
347
+ abortPromises.push(Promise.resolve(task.session.abort()).catch(() => {
348
+ // Ignore failures while clearing
349
+ }));
350
+ }
351
+ }
352
+ await Promise.allSettled(abortPromises);
353
+ if (this.taskDeletedCallback) {
354
+ for (const id of this.tasks.keys()) {
355
+ try {
356
+ this.taskDeletedCallback(id);
357
+ }
358
+ catch { }
359
+ }
360
+ }
361
+ this.tasks.clear();
362
+ this.scheduleRateLimitCheck();
363
+ return count;
364
+ }
365
+ /**
366
+ * Manually trigger retry of a rate-limited task
367
+ * Returns the new task ID on success
368
+ */
369
+ async triggerManualRetry(taskId) {
370
+ const normalizedId = normalizeTaskId(taskId);
371
+ const task = this.tasks.get(normalizedId);
372
+ if (!task) {
373
+ return { success: false, error: 'Task not found' };
374
+ }
375
+ if (task.status !== TaskStatus.RATE_LIMITED) {
376
+ return { success: false, error: `Task is not rate-limited (status: ${task.status})` };
377
+ }
378
+ if (!this.retryCallback) {
379
+ return { success: false, error: 'No retry callback registered' };
380
+ }
381
+ // Mark original task as failed (manually retried)
382
+ this.updateTask(task.id, {
383
+ status: TaskStatus.FAILED,
384
+ error: `Manually retried (attempt ${(task.retryInfo?.retryCount ?? 0) + 1}/${task.retryInfo?.maxRetries ?? 6})`,
385
+ endTime: new Date().toISOString(),
386
+ exitCode: 1,
387
+ });
388
+ // Trigger the retry callback - it will spawn a new task and return its ID
389
+ const newTaskId = await this.retryCallback(task);
390
+ return {
391
+ success: true,
392
+ newTaskId: newTaskId || 'unknown',
393
+ };
394
+ }
395
+ /**
396
+ * Persist tasks to disk (debounced)
397
+ */
398
+ schedulePersist(trigger = 'state') {
399
+ if (!this.currentCwd) {
400
+ return;
401
+ }
402
+ // Clear existing timeout
403
+ if (this.persistTimeout) {
404
+ clearTimeout(this.persistTimeout);
405
+ }
406
+ // Use longer debounce for output-only changes (high frequency)
407
+ const debounceMs = trigger === 'output' && this.lastPersistTrigger === 'output'
408
+ ? this.outputPersistDebounceMs
409
+ : this.persistDebounceMs;
410
+ this.lastPersistTrigger = trigger;
411
+ this.persistTimeout = setTimeout(() => {
412
+ this.persistNow();
413
+ }, debounceMs);
414
+ }
415
+ /**
416
+ * Persist tasks immediately
417
+ */
418
+ persistNow() {
419
+ if (!this.currentCwd) {
420
+ return;
421
+ }
422
+ const tasks = Array.from(this.tasks.values());
423
+ saveTasks(this.currentCwd, tasks);
424
+ }
425
+ startCleanup() {
426
+ this.cleanupInterval = setInterval(() => {
427
+ this.cleanup();
428
+ }, CLEANUP_INTERVAL_MS);
429
+ }
430
+ /**
431
+ * Start periodic health check for running sessions
432
+ * Monitors for stalled sessions and enforces hard timeouts.
433
+ */
434
+ startHealthCheck() {
435
+ this.healthCheckInterval = setInterval(() => {
436
+ this.checkSessionHealth();
437
+ }, HEALTH_CHECK_INTERVAL_MS);
438
+ }
439
+ /**
440
+ * Check all RUNNING tasks to verify their sessions are healthy
441
+ * Checks all RUNNING tasks for health: heartbeat, hard timeout, and stalls.
442
+ */
443
+ checkSessionHealth() {
444
+ const now = Date.now();
445
+ for (const task of this.tasks.values()) {
446
+ if (task.status === TaskStatus.RUNNING) {
447
+ // Update heartbeat for active sessions
448
+ const lastHeartbeat = task.lastHeartbeatAt ? new Date(task.lastHeartbeatAt).getTime() : 0;
449
+ if (now - lastHeartbeat >= HEALTH_CHECK_INTERVAL_MS) {
450
+ this.updateTask(task.id, { lastHeartbeatAt: new Date(now).toISOString() });
451
+ }
452
+ // Enforce hard timeout — session.send() doesn't have a built-in timeout,
453
+ // so the health check is the enforcement mechanism.
454
+ if (task.timeoutAt) {
455
+ const timeoutAt = new Date(task.timeoutAt).getTime();
456
+ if (now >= timeoutAt) {
457
+ const elapsedMs = task.startTime ? now - new Date(task.startTime).getTime() : 0;
458
+ console.error(`[task-manager] Health check: hard timeout reached for task ${task.id} after ${elapsedMs}ms`);
459
+ this.updateTask(task.id, {
460
+ status: TaskStatus.TIMED_OUT,
461
+ endTime: new Date(now).toISOString(),
462
+ error: `Task timed out after ${task.timeout ?? elapsedMs}ms`,
463
+ timeoutReason: 'hard_timeout',
464
+ timeoutContext: {
465
+ timeoutMs: task.timeout,
466
+ elapsedMs,
467
+ detectedBy: 'health_check',
468
+ },
469
+ session: undefined,
470
+ });
471
+ // Abort the session — import handled at module level
472
+ if (task.session) {
473
+ Promise.resolve(task.session.abort()).catch(() => { });
474
+ }
475
+ continue;
476
+ }
477
+ }
478
+ // Check for stalled sessions (no output for extended period)
479
+ if (task.lastOutputAt) {
480
+ const lastOutputAgeMs = now - new Date(task.lastOutputAt).getTime();
481
+ if (lastOutputAgeMs >= TASK_STALL_WARN_MS && task.timeoutReason !== 'stall') {
482
+ console.error(`[task-manager] Health check: session stall detected for task ${task.id}`);
483
+ this.updateTask(task.id, {
484
+ timeoutReason: 'stall',
485
+ timeoutContext: {
486
+ lastOutputAt: task.lastOutputAt,
487
+ lastOutputAgeMs,
488
+ lastHeartbeatAt: task.lastHeartbeatAt,
489
+ sessionAlive: isSessionActive(task),
490
+ detectedBy: 'health_check',
491
+ },
492
+ });
493
+ }
494
+ }
495
+ }
496
+ }
497
+ }
498
+ cleanup() {
499
+ const now = Date.now();
500
+ const toDelete = [];
501
+ let removed = false;
502
+ for (const [id, task] of this.tasks) {
503
+ if (task.status === TaskStatus.COMPLETED ||
504
+ task.status === TaskStatus.FAILED ||
505
+ task.status === TaskStatus.CANCELLED ||
506
+ task.status === TaskStatus.TIMED_OUT) {
507
+ const endTime = task.endTime ? new Date(task.endTime).getTime() : 0;
508
+ if (now - endTime > TASK_TTL_MS) {
509
+ toDelete.push(id);
510
+ }
511
+ }
512
+ }
513
+ for (const id of toDelete) {
514
+ try {
515
+ this.taskDeletedCallback?.(id);
516
+ }
517
+ catch { }
518
+ this.tasks.delete(id);
519
+ removed = true;
520
+ }
521
+ if (this.tasks.size > MAX_TASKS) {
522
+ const evictableStatuses = new Set([
523
+ TaskStatus.COMPLETED,
524
+ TaskStatus.FAILED,
525
+ TaskStatus.CANCELLED,
526
+ TaskStatus.TIMED_OUT,
527
+ ]);
528
+ const sorted = Array.from(this.tasks.entries())
529
+ .filter(([_, t]) => evictableStatuses.has(t.status))
530
+ .sort((a, b) => new Date(a[1].startTime).getTime() - new Date(b[1].startTime).getTime());
531
+ const toRemove = sorted.slice(0, this.tasks.size - MAX_TASKS);
532
+ for (const [id] of toRemove) {
533
+ try {
534
+ this.taskDeletedCallback?.(id);
535
+ }
536
+ catch { }
537
+ this.tasks.delete(id);
538
+ removed = true;
539
+ }
540
+ }
541
+ if (removed) {
542
+ this.schedulePersist('state');
543
+ this.scheduleRateLimitCheck();
544
+ }
545
+ }
546
+ createTask(prompt, cwd, model, options) {
547
+ let id = generateTaskId();
548
+ let normalizedId = normalizeTaskId(id);
549
+ let attempts = 0;
550
+ while (this.tasks.has(normalizedId) && attempts < 5) {
551
+ id = generateTaskId();
552
+ normalizedId = normalizeTaskId(id);
553
+ attempts += 1;
554
+ }
555
+ if (this.tasks.has(normalizedId)) {
556
+ const uniqueSuffix = `${Date.now()}-${Math.floor(Math.random() * 1000)}`;
557
+ id = `${id}-${uniqueSuffix}`;
558
+ normalizedId = normalizeTaskId(id);
559
+ }
560
+ // Determine initial status based on dependencies
561
+ let initialStatus = TaskStatus.PENDING;
562
+ const dependsOn = options?.dependsOn?.filter(d => d.trim()) || [];
563
+ const labels = options?.labels?.filter(l => l.trim()) || [];
564
+ if (dependsOn.length > 0) {
565
+ // Check if all dependencies are already completed
566
+ const { satisfied } = areDependenciesSatisfied({ dependsOn }, this.tasks);
567
+ initialStatus = satisfied ? TaskStatus.PENDING : TaskStatus.WAITING;
568
+ }
569
+ const startTime = new Date().toISOString();
570
+ // Create output file for live monitoring
571
+ const outputFilePath = cwd ? createOutputFile(cwd, id) : null;
572
+ const task = {
573
+ id,
574
+ status: initialStatus,
575
+ prompt,
576
+ output: [],
577
+ startTime,
578
+ lastHeartbeatAt: startTime,
579
+ cwd,
580
+ model,
581
+ autonomous: options?.autonomous,
582
+ isResume: options?.isResume,
583
+ retryInfo: options?.retryInfo,
584
+ dependsOn: dependsOn.length > 0 ? dependsOn : undefined,
585
+ labels: labels.length > 0 ? labels : undefined,
586
+ provider: options?.provider,
587
+ fallbackAttempted: options?.fallbackAttempted,
588
+ switchAttempted: options?.switchAttempted,
589
+ timeout: options?.timeout,
590
+ outputFilePath: outputFilePath || undefined,
591
+ };
592
+ this.tasks.set(normalizedId, task);
593
+ this.schedulePersist('state');
594
+ try {
595
+ this.taskCreatedCallback?.(task);
596
+ }
597
+ catch { }
598
+ return task;
599
+ }
600
+ getTask(id) {
601
+ const normalizedId = normalizeTaskId(id);
602
+ return this.tasks.get(normalizedId) || null;
603
+ }
604
+ updateTask(id, updates) {
605
+ const normalizedId = normalizeTaskId(id);
606
+ const task = this.tasks.get(normalizedId);
607
+ if (!task) {
608
+ return null;
609
+ }
610
+ const previousStatus = task.status;
611
+ const updated = { ...task, ...updates };
612
+ const statusChanged = updates.status && updates.status !== previousStatus;
613
+ if (updates.status && statusChanged && TERMINAL_STATUSES.has(updates.status)) {
614
+ // Clear session reference on terminal status
615
+ updated.session = undefined;
616
+ if (!updates.endTime) {
617
+ updated.endTime = new Date().toISOString();
618
+ }
619
+ // Finalize output file with completion status
620
+ if (task.cwd) {
621
+ finalizeOutputFile(task.cwd, task.id, updates.status, updates.error);
622
+ }
623
+ }
624
+ // Also clear session reference on RATE_LIMITED transition (session is no longer valid)
625
+ if (updates.status === TaskStatus.RATE_LIMITED && statusChanged) {
626
+ updated.session = undefined;
627
+ }
628
+ this.tasks.set(normalizedId, updated);
629
+ this.schedulePersist('state');
630
+ // Fire status change callback
631
+ if (updates.status && updates.status !== previousStatus) {
632
+ try {
633
+ this.statusChangeCallback?.(updated, previousStatus);
634
+ }
635
+ catch { }
636
+ }
637
+ if (updates.status && updates.status !== previousStatus) {
638
+ if (updates.status === TaskStatus.RATE_LIMITED || previousStatus === TaskStatus.RATE_LIMITED) {
639
+ this.scheduleRateLimitCheck();
640
+ }
641
+ }
642
+ // When a task completes, check if any waiting tasks can now run
643
+ if (updates.status === TaskStatus.COMPLETED && previousStatus !== TaskStatus.COMPLETED) {
644
+ this.processWaitingTasks();
645
+ }
646
+ return updated;
647
+ }
648
+ /**
649
+ * Write to output file only — skips in-memory array and callbacks.
650
+ * Use for verbose debug data (reasoning, internal events) that should be
651
+ * available in the file for debugging but not waste tokens in MCP resources.
652
+ */
653
+ appendOutputFileOnly(id, line) {
654
+ const normalizedId = normalizeTaskId(id);
655
+ const task = this.tasks.get(normalizedId);
656
+ if (task?.cwd) {
657
+ appendToOutputFile(task.cwd, task.id, line);
658
+ }
659
+ }
660
+ appendOutput(id, line) {
661
+ const normalizedId = normalizeTaskId(id);
662
+ const task = this.tasks.get(normalizedId);
663
+ if (task) {
664
+ const now = new Date().toISOString();
665
+ task.lastOutputAt = now;
666
+ task.lastHeartbeatAt = now;
667
+ if (task.timeoutReason === 'stall') {
668
+ task.timeoutReason = undefined;
669
+ task.timeoutContext = undefined;
670
+ }
671
+ task.output.push(line);
672
+ try {
673
+ this.outputCallback?.(id, line);
674
+ }
675
+ catch { }
676
+ // Write to output file for live monitoring
677
+ if (task.cwd) {
678
+ appendToOutputFile(task.cwd, task.id, line);
679
+ }
680
+ if (task.output.length > MAX_OUTPUT_LINES) {
681
+ task.output = task.output.slice(-MAX_OUTPUT_LINES);
682
+ }
683
+ if (!task.sessionId) {
684
+ const sessionMatch = line.match(/(?:Session ID:|session[_-]?id[=:]?)\s*([a-zA-Z0-9_-]+)/i);
685
+ if (sessionMatch) {
686
+ task.sessionId = sessionMatch[1];
687
+ this.schedulePersist('state');
688
+ return;
689
+ }
690
+ }
691
+ this.schedulePersist('output');
692
+ }
693
+ }
694
+ getAllTasks(statusFilter) {
695
+ const tasks = Array.from(this.tasks.values());
696
+ if (statusFilter) {
697
+ return tasks.filter(t => t.status === statusFilter);
698
+ }
699
+ return tasks;
700
+ }
701
+ cancelTask(id) {
702
+ const normalizedId = normalizeTaskId(id);
703
+ const task = this.tasks.get(normalizedId);
704
+ if (!task) {
705
+ return { success: false, error: 'Task not found' };
706
+ }
707
+ if (task.status !== TaskStatus.RUNNING && task.status !== TaskStatus.PENDING && task.status !== TaskStatus.WAITING) {
708
+ return { success: false, error: `Task is not cancellable (status: ${task.status})` };
709
+ }
710
+ let alreadyDead = false;
711
+ if (task.session) {
712
+ // Abort the session
713
+ try {
714
+ task.session.abort();
715
+ }
716
+ catch (err) {
717
+ // Session may already be done
718
+ alreadyDead = true;
719
+ console.error(`[task-manager] Cancel: session abort failed for task ${task.id}: ${err}`);
720
+ }
721
+ }
722
+ else if (task.status === TaskStatus.RUNNING) {
723
+ // Running but no session reference - already dead
724
+ alreadyDead = true;
725
+ }
726
+ this.updateTask(task.id, {
727
+ status: TaskStatus.CANCELLED,
728
+ endTime: new Date().toISOString(),
729
+ error: alreadyDead ? 'Session had already ended before cancellation' : undefined,
730
+ });
731
+ return { success: true, alreadyDead };
732
+ }
733
+ async shutdown() {
734
+ if (this.cleanupInterval) {
735
+ clearInterval(this.cleanupInterval);
736
+ this.cleanupInterval = null;
737
+ }
738
+ if (this.healthCheckInterval) {
739
+ clearInterval(this.healthCheckInterval);
740
+ this.healthCheckInterval = null;
741
+ }
742
+ if (this.persistTimeout) {
743
+ clearTimeout(this.persistTimeout);
744
+ this.persistTimeout = null;
745
+ }
746
+ if (this.rateLimitTimer) {
747
+ clearTimeout(this.rateLimitTimer);
748
+ this.rateLimitTimer = null;
749
+ }
750
+ // Abort all running sessions
751
+ const abortPromises = [];
752
+ for (const task of this.tasks.values()) {
753
+ if (task.session && task.status === TaskStatus.RUNNING) {
754
+ abortPromises.push(Promise.resolve(task.session.abort()).catch(() => {
755
+ // Ignore during shutdown
756
+ }));
757
+ }
758
+ }
759
+ await Promise.allSettled(abortPromises);
760
+ // Final persist before shutdown
761
+ this.persistNow();
762
+ }
763
+ }
764
+ export const taskManager = new TaskManager();
765
+ //# sourceMappingURL=task-manager.js.map