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.
- package/LICENSE +21 -0
- package/README.md +602 -0
- package/build/config/timeouts.d.ts +9 -0
- package/build/config/timeouts.d.ts.map +1 -0
- package/build/config/timeouts.js +18 -0
- package/build/config/timeouts.js.map +1 -0
- package/build/helpers.d.ts +6 -0
- package/build/helpers.d.ts.map +1 -0
- package/build/helpers.js +47 -0
- package/build/helpers.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +245 -0
- package/build/index.js.map +1 -0
- package/build/models.d.ts +32 -0
- package/build/models.d.ts.map +1 -0
- package/build/models.js +58 -0
- package/build/models.js.map +1 -0
- package/build/server/register-notifications.d.ts +3 -0
- package/build/server/register-notifications.d.ts.map +1 -0
- package/build/server/register-notifications.js +77 -0
- package/build/server/register-notifications.js.map +1 -0
- package/build/server/register-resources.d.ts +3 -0
- package/build/server/register-resources.d.ts.map +1 -0
- package/build/server/register-resources.js +210 -0
- package/build/server/register-resources.js.map +1 -0
- package/build/server/register-retry-execution.d.ts +2 -0
- package/build/server/register-retry-execution.d.ts.map +1 -0
- package/build/server/register-retry-execution.js +28 -0
- package/build/server/register-retry-execution.js.map +1 -0
- package/build/server/register-tasks.d.ts +3 -0
- package/build/server/register-tasks.d.ts.map +1 -0
- package/build/server/register-tasks.js +52 -0
- package/build/server/register-tasks.js.map +1 -0
- package/build/server/register-tools.d.ts +3 -0
- package/build/server/register-tools.d.ts.map +1 -0
- package/build/server/register-tools.js +32 -0
- package/build/server/register-tools.js.map +1 -0
- package/build/server/resource-helpers.d.ts +21 -0
- package/build/server/resource-helpers.d.ts.map +1 -0
- package/build/server/resource-helpers.js +84 -0
- package/build/server/resource-helpers.js.map +1 -0
- package/build/services/account-manager.d.ts +88 -0
- package/build/services/account-manager.d.ts.map +1 -0
- package/build/services/account-manager.js +239 -0
- package/build/services/account-manager.js.map +1 -0
- package/build/services/claude-code-runner.d.ts +15 -0
- package/build/services/claude-code-runner.d.ts.map +1 -0
- package/build/services/claude-code-runner.js +475 -0
- package/build/services/claude-code-runner.js.map +1 -0
- package/build/services/client-context.d.ts +31 -0
- package/build/services/client-context.d.ts.map +1 -0
- package/build/services/client-context.js +44 -0
- package/build/services/client-context.js.map +1 -0
- package/build/services/exhaustion-fallback.d.ts +27 -0
- package/build/services/exhaustion-fallback.d.ts.map +1 -0
- package/build/services/exhaustion-fallback.js +30 -0
- package/build/services/exhaustion-fallback.js.map +1 -0
- package/build/services/fallback-orchestrator.d.ts +16 -0
- package/build/services/fallback-orchestrator.d.ts.map +1 -0
- package/build/services/fallback-orchestrator.js +48 -0
- package/build/services/fallback-orchestrator.js.map +1 -0
- package/build/services/opencode-client.d.ts +40 -0
- package/build/services/opencode-client.d.ts.map +1 -0
- package/build/services/opencode-client.js +147 -0
- package/build/services/opencode-client.js.map +1 -0
- package/build/services/opencode-spawner.d.ts +56 -0
- package/build/services/opencode-spawner.d.ts.map +1 -0
- package/build/services/opencode-spawner.js +426 -0
- package/build/services/opencode-spawner.js.map +1 -0
- package/build/services/output-file.d.ts +24 -0
- package/build/services/output-file.d.ts.map +1 -0
- package/build/services/output-file.js +90 -0
- package/build/services/output-file.js.map +1 -0
- package/build/services/progress-registry.d.ts +12 -0
- package/build/services/progress-registry.d.ts.map +1 -0
- package/build/services/progress-registry.js +97 -0
- package/build/services/progress-registry.js.map +1 -0
- package/build/services/question-registry.d.ts +79 -0
- package/build/services/question-registry.d.ts.map +1 -0
- package/build/services/question-registry.js +249 -0
- package/build/services/question-registry.js.map +1 -0
- package/build/services/retry-queue.d.ts +41 -0
- package/build/services/retry-queue.d.ts.map +1 -0
- package/build/services/retry-queue.js +195 -0
- package/build/services/retry-queue.js.map +1 -0
- package/build/services/sdk-client-manager.d.ts +149 -0
- package/build/services/sdk-client-manager.d.ts.map +1 -0
- package/build/services/sdk-client-manager.js +632 -0
- package/build/services/sdk-client-manager.js.map +1 -0
- package/build/services/sdk-session-adapter.d.ts +203 -0
- package/build/services/sdk-session-adapter.d.ts.map +1 -0
- package/build/services/sdk-session-adapter.js +1088 -0
- package/build/services/sdk-session-adapter.js.map +1 -0
- package/build/services/sdk-spawner.d.ts +42 -0
- package/build/services/sdk-spawner.d.ts.map +1 -0
- package/build/services/sdk-spawner.js +488 -0
- package/build/services/sdk-spawner.js.map +1 -0
- package/build/services/session-hooks.d.ts +24 -0
- package/build/services/session-hooks.d.ts.map +1 -0
- package/build/services/session-hooks.js +130 -0
- package/build/services/session-hooks.js.map +1 -0
- package/build/services/session-snapshot.d.ts +19 -0
- package/build/services/session-snapshot.d.ts.map +1 -0
- package/build/services/session-snapshot.js +203 -0
- package/build/services/session-snapshot.js.map +1 -0
- package/build/services/subscription-registry.d.ts +12 -0
- package/build/services/subscription-registry.d.ts.map +1 -0
- package/build/services/subscription-registry.js +27 -0
- package/build/services/subscription-registry.js.map +1 -0
- package/build/services/task-manager.d.ts +150 -0
- package/build/services/task-manager.d.ts.map +1 -0
- package/build/services/task-manager.js +765 -0
- package/build/services/task-manager.js.map +1 -0
- package/build/services/task-persistence.d.ts +29 -0
- package/build/services/task-persistence.d.ts.map +1 -0
- package/build/services/task-persistence.js +159 -0
- package/build/services/task-persistence.js.map +1 -0
- package/build/services/task-status-mapper.d.ts +21 -0
- package/build/services/task-status-mapper.d.ts.map +1 -0
- package/build/services/task-status-mapper.js +171 -0
- package/build/services/task-status-mapper.js.map +1 -0
- package/build/templates/index.d.ts +22 -0
- package/build/templates/index.d.ts.map +1 -0
- package/build/templates/index.js +147 -0
- package/build/templates/index.js.map +1 -0
- package/build/templates/overlays/coder-csharp.mdx +58 -0
- package/build/templates/overlays/coder-go.mdx +53 -0
- package/build/templates/overlays/coder-java.mdx +54 -0
- package/build/templates/overlays/coder-kotlin.mdx +56 -0
- package/build/templates/overlays/coder-nextjs.mdx +65 -0
- package/build/templates/overlays/coder-python.mdx +53 -0
- package/build/templates/overlays/coder-react.mdx +55 -0
- package/build/templates/overlays/coder-ruby.mdx +59 -0
- package/build/templates/overlays/coder-rust.mdx +48 -0
- package/build/templates/overlays/coder-supabase.mdx +268 -0
- package/build/templates/overlays/coder-supastarter.mdx +313 -0
- package/build/templates/overlays/coder-swift.mdx +56 -0
- package/build/templates/overlays/coder-tauri.mdx +566 -0
- package/build/templates/overlays/coder-triggerdev.mdx +296 -0
- package/build/templates/overlays/coder-typescript.mdx +45 -0
- package/build/templates/overlays/coder-vue.mdx +62 -0
- package/build/templates/overlays/planner-architecture.mdx +78 -0
- package/build/templates/overlays/planner-bugfix.mdx +36 -0
- package/build/templates/overlays/planner-feature.mdx +38 -0
- package/build/templates/overlays/planner-migration.mdx +50 -0
- package/build/templates/overlays/planner-refactor.mdx +57 -0
- package/build/templates/overlays/researcher-library.mdx +59 -0
- package/build/templates/overlays/researcher-performance.mdx +68 -0
- package/build/templates/overlays/researcher-security.mdx +86 -0
- package/build/templates/overlays/tester-graphql.mdx +191 -0
- package/build/templates/overlays/tester-playwright.mdx +621 -0
- package/build/templates/overlays/tester-rest.mdx +101 -0
- package/build/templates/overlays/tester-suite.mdx +177 -0
- package/build/templates/super-coder.mdx +529 -0
- package/build/templates/super-planner.mdx +568 -0
- package/build/templates/super-researcher.mdx +406 -0
- package/build/templates/super-tester.mdx +243 -0
- package/build/tools/answer-question.d.ts +30 -0
- package/build/tools/answer-question.d.ts.map +1 -0
- package/build/tools/answer-question.js +108 -0
- package/build/tools/answer-question.js.map +1 -0
- package/build/tools/cancel-task.d.ts +44 -0
- package/build/tools/cancel-task.d.ts.map +1 -0
- package/build/tools/cancel-task.js +144 -0
- package/build/tools/cancel-task.js.map +1 -0
- package/build/tools/send-message.d.ts +39 -0
- package/build/tools/send-message.d.ts.map +1 -0
- package/build/tools/send-message.js +124 -0
- package/build/tools/send-message.js.map +1 -0
- package/build/tools/shared-spawn.d.ts +56 -0
- package/build/tools/shared-spawn.d.ts.map +1 -0
- package/build/tools/shared-spawn.js +114 -0
- package/build/tools/shared-spawn.js.map +1 -0
- package/build/tools/spawn-agent.d.ts +85 -0
- package/build/tools/spawn-agent.d.ts.map +1 -0
- package/build/tools/spawn-agent.js +133 -0
- package/build/tools/spawn-agent.js.map +1 -0
- package/build/tools/spawn-coder.d.ts +70 -0
- package/build/tools/spawn-coder.d.ts.map +1 -0
- package/build/tools/spawn-coder.js +71 -0
- package/build/tools/spawn-coder.js.map +1 -0
- package/build/tools/spawn-planner.d.ts +70 -0
- package/build/tools/spawn-planner.d.ts.map +1 -0
- package/build/tools/spawn-planner.js +71 -0
- package/build/tools/spawn-planner.js.map +1 -0
- package/build/tools/spawn-researcher.d.ts +70 -0
- package/build/tools/spawn-researcher.d.ts.map +1 -0
- package/build/tools/spawn-researcher.js +70 -0
- package/build/tools/spawn-researcher.js.map +1 -0
- package/build/tools/spawn-task.d.ts +74 -0
- package/build/tools/spawn-task.d.ts.map +1 -0
- package/build/tools/spawn-task.js +107 -0
- package/build/tools/spawn-task.js.map +1 -0
- package/build/tools/spawn-tester.d.ts +70 -0
- package/build/tools/spawn-tester.d.ts.map +1 -0
- package/build/tools/spawn-tester.js +69 -0
- package/build/tools/spawn-tester.js.map +1 -0
- package/build/types.d.ts +101 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +28 -0
- package/build/types.js.map +1 -0
- package/build/utils/brief-validator.d.ts +30 -0
- package/build/utils/brief-validator.d.ts.map +1 -0
- package/build/utils/brief-validator.js +254 -0
- package/build/utils/brief-validator.js.map +1 -0
- package/build/utils/format.d.ts +34 -0
- package/build/utils/format.d.ts.map +1 -0
- package/build/utils/format.js +55 -0
- package/build/utils/format.js.map +1 -0
- package/build/utils/sanitize.d.ts +240 -0
- package/build/utils/sanitize.d.ts.map +1 -0
- package/build/utils/sanitize.js +89 -0
- package/build/utils/sanitize.js.map +1 -0
- package/build/utils/task-id-generator.d.ts +10 -0
- package/build/utils/task-id-generator.d.ts.map +1 -0
- package/build/utils/task-id-generator.js +22 -0
- package/build/utils/task-id-generator.js.map +1 -0
- 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
|