@finityno/claude-code-acp 0.14.0 → 0.16.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.
@@ -7,6 +7,8 @@ export class SubagentTracker {
7
7
  this.subagents = new Map();
8
8
  /** Map of session ID to subagent IDs in that session */
9
9
  this.sessionSubagents = new Map();
10
+ /** Map of agent ID to subagent ID (for resume lookups) */
11
+ this.agentIdToSubagent = new Map();
10
12
  /** Event listeners for subagent lifecycle events */
11
13
  this.listeners = new Map();
12
14
  this.client = client;
@@ -16,6 +18,11 @@ export class SubagentTracker {
16
18
  * Track a new subagent when Task tool is called
17
19
  */
18
20
  trackSubagent(toolUseId, sessionId, input, parentToolUseId) {
21
+ // Check if this is a resume operation
22
+ const isResumed = !!input.resume;
23
+ const originalTaskId = input.resume
24
+ ? this.agentIdToSubagent.get(input.resume)
25
+ : undefined;
19
26
  const subagent = {
20
27
  id: toolUseId,
21
28
  parentSessionId: sessionId,
@@ -28,6 +35,12 @@ export class SubagentTracker {
28
35
  createdAt: Date.now(),
29
36
  runInBackground: input.run_in_background ?? false,
30
37
  maxTurns: input.max_turns,
38
+ // SDK 0.2.17 fields
39
+ agentName: input.name,
40
+ teamName: input.team_name,
41
+ permissionMode: input.mode,
42
+ isResumed,
43
+ originalTaskId,
31
44
  };
32
45
  this.subagents.set(toolUseId, subagent);
33
46
  // Track by session
@@ -35,7 +48,7 @@ export class SubagentTracker {
35
48
  this.sessionSubagents.set(sessionId, new Set());
36
49
  }
37
50
  this.sessionSubagents.get(sessionId).add(toolUseId);
38
- this.logger.log(`[SubagentTracker] Tracked new subagent: ${toolUseId} (${input.subagent_type}: ${input.description})`);
51
+ this.logger.log(`[SubagentTracker] Tracked new subagent: ${toolUseId} (${input.subagent_type}: ${input.description})${isResumed ? " [RESUMED]" : ""}`);
39
52
  return subagent;
40
53
  }
41
54
  /**
@@ -66,10 +79,12 @@ export class SubagentTracker {
66
79
  subagent.result = result;
67
80
  if (agentId) {
68
81
  subagent.agentId = agentId;
82
+ // Track agent ID for resume lookups
83
+ this.agentIdToSubagent.set(agentId, toolUseId);
69
84
  }
70
85
  await this.emitEvent("subagent_completed", subagent);
71
86
  await this.sendSubagentNotification(subagent, "subagent_completed");
72
- this.logger.log(`[SubagentTracker] Subagent completed: ${toolUseId} (duration: ${this.getDuration(subagent)}ms)`);
87
+ this.logger.log(`[SubagentTracker] Subagent completed: ${toolUseId} (duration: ${this.getDuration(subagent)}ms)${agentId ? ` [agentId: ${agentId}]` : ""}`);
73
88
  }
74
89
  /**
75
90
  * Mark a subagent as failed
@@ -101,6 +116,40 @@ export class SubagentTracker {
101
116
  await this.sendSubagentNotification(subagent, "subagent_cancelled");
102
117
  this.logger.log(`[SubagentTracker] Subagent cancelled: ${toolUseId}`);
103
118
  }
119
+ /**
120
+ * Handle SDKTaskNotificationMessage from the Claude Agent SDK.
121
+ * This is called when a background task completes/fails/stops.
122
+ */
123
+ async handleTaskNotification(notification) {
124
+ const subagent = this.subagents.get(notification.task_id);
125
+ if (!subagent) {
126
+ this.logger.error(`[SubagentTracker] Received task notification for unknown subagent: ${notification.task_id}`);
127
+ return;
128
+ }
129
+ // Update subagent with notification data
130
+ subagent.outputFile = notification.output_file;
131
+ subagent.summary = notification.summary;
132
+ subagent.completedAt = Date.now();
133
+ // Map SDK status to our status
134
+ switch (notification.status) {
135
+ case "completed":
136
+ subagent.status = "completed";
137
+ await this.emitEvent("subagent_completed", subagent);
138
+ await this.sendSubagentNotification(subagent, "subagent_completed", notification);
139
+ break;
140
+ case "failed":
141
+ subagent.status = "failed";
142
+ await this.emitEvent("subagent_failed", subagent);
143
+ await this.sendSubagentNotification(subagent, "subagent_failed", notification);
144
+ break;
145
+ case "stopped":
146
+ subagent.status = "stopped";
147
+ await this.emitEvent("subagent_stopped", subagent);
148
+ await this.sendSubagentNotification(subagent, "subagent_stopped", notification);
149
+ break;
150
+ }
151
+ this.logger.log(`[SubagentTracker] Task notification: ${notification.task_id} -> ${notification.status}`);
152
+ }
104
153
  /**
105
154
  * Send progress update for a running subagent
106
155
  */
@@ -162,6 +211,22 @@ export class SubagentTracker {
162
211
  getAllSubagents() {
163
212
  return Array.from(this.subagents.values());
164
213
  }
214
+ /**
215
+ * Find a subagent by its agent ID (for resume operations)
216
+ */
217
+ findByAgentId(agentId) {
218
+ const subagentId = this.agentIdToSubagent.get(agentId);
219
+ if (!subagentId)
220
+ return undefined;
221
+ return this.subagents.get(subagentId);
222
+ }
223
+ /**
224
+ * Get tasks that can be resumed (have agentId and are completed/failed/stopped)
225
+ */
226
+ getResumableTasks() {
227
+ return Array.from(this.subagents.values()).filter((s) => s.agentId &&
228
+ (s.status === "completed" || s.status === "failed" || s.status === "stopped"));
229
+ }
165
230
  /**
166
231
  * Check if a tool use ID is a Task tool (subagent)
167
232
  */
@@ -192,16 +257,102 @@ export class SubagentTracker {
192
257
  for (const [id, subagent] of this.subagents) {
193
258
  if ((subagent.status === "completed" ||
194
259
  subagent.status === "failed" ||
195
- subagent.status === "cancelled") &&
260
+ subagent.status === "cancelled" ||
261
+ subagent.status === "stopped") &&
196
262
  subagent.completedAt &&
197
263
  now - subagent.completedAt > maxAgeMs) {
198
264
  this.subagents.delete(id);
199
265
  this.sessionSubagents.get(subagent.parentSessionId)?.delete(id);
266
+ if (subagent.agentId) {
267
+ this.agentIdToSubagent.delete(subagent.agentId);
268
+ }
200
269
  cleanedCount++;
201
270
  }
202
271
  }
203
272
  return cleanedCount;
204
273
  }
274
+ /**
275
+ * Export current state for persistence
276
+ */
277
+ exportState() {
278
+ const tasks = Array.from(this.subagents.values()).map((s) => ({
279
+ id: s.id,
280
+ parentSessionId: s.parentSessionId,
281
+ parentToolUseId: s.parentToolUseId,
282
+ subagentType: s.subagentType,
283
+ description: s.description,
284
+ prompt: s.prompt,
285
+ model: s.model,
286
+ status: s.status,
287
+ createdAt: s.createdAt,
288
+ startedAt: s.startedAt,
289
+ completedAt: s.completedAt,
290
+ result: s.result,
291
+ error: s.error,
292
+ runInBackground: s.runInBackground,
293
+ maxTurns: s.maxTurns,
294
+ agentId: s.agentId,
295
+ outputFile: s.outputFile,
296
+ summary: s.summary,
297
+ agentName: s.agentName,
298
+ teamName: s.teamName,
299
+ permissionMode: s.permissionMode,
300
+ isResumed: s.isResumed,
301
+ originalTaskId: s.originalTaskId,
302
+ }));
303
+ return {
304
+ version: 1,
305
+ tasks,
306
+ lastUpdated: Date.now(),
307
+ };
308
+ }
309
+ /**
310
+ * Import state from persistence
311
+ */
312
+ importState(state) {
313
+ if (state.version !== 1) {
314
+ this.logger.error(`[SubagentTracker] Unknown state version: ${state.version}`);
315
+ return;
316
+ }
317
+ for (const task of state.tasks) {
318
+ const subagent = {
319
+ id: task.id,
320
+ parentSessionId: task.parentSessionId,
321
+ parentToolUseId: task.parentToolUseId,
322
+ subagentType: task.subagentType,
323
+ description: task.description,
324
+ prompt: task.prompt,
325
+ model: task.model,
326
+ status: task.status,
327
+ createdAt: task.createdAt,
328
+ startedAt: task.startedAt,
329
+ completedAt: task.completedAt,
330
+ result: task.result,
331
+ error: task.error,
332
+ runInBackground: task.runInBackground,
333
+ maxTurns: task.maxTurns,
334
+ agentId: task.agentId,
335
+ outputFile: task.outputFile,
336
+ summary: task.summary,
337
+ agentName: task.agentName,
338
+ teamName: task.teamName,
339
+ permissionMode: task.permissionMode,
340
+ isResumed: task.isResumed,
341
+ originalTaskId: task.originalTaskId,
342
+ };
343
+ this.subagents.set(task.id, subagent);
344
+ // Rebuild session index
345
+ if (!this.sessionSubagents.has(task.parentSessionId)) {
346
+ this.sessionSubagents.set(task.parentSessionId, new Set());
347
+ }
348
+ this.sessionSubagents.get(task.parentSessionId).add(task.id);
349
+ // Rebuild agent ID index
350
+ if (task.agentId) {
351
+ this.agentIdToSubagent.set(task.agentId, task.id);
352
+ }
353
+ }
354
+ this.logger.log(`[SubagentTracker] Imported ${state.tasks.length} tasks from persistence`);
355
+ }
205
356
  /**
206
357
  * Get statistics about subagents
207
358
  */
@@ -214,6 +365,7 @@ export class SubagentTracker {
214
365
  completed: subagents.filter((s) => s.status === "completed").length,
215
366
  failed: subagents.filter((s) => s.status === "failed").length,
216
367
  cancelled: subagents.filter((s) => s.status === "cancelled").length,
368
+ stopped: subagents.filter((s) => s.status === "stopped").length,
217
369
  byType: this.countByType(subagents),
218
370
  averageDurationMs: this.calculateAverageDuration(subagents),
219
371
  };
@@ -224,6 +376,7 @@ export class SubagentTracker {
224
376
  clear() {
225
377
  this.subagents.clear();
226
378
  this.sessionSubagents.clear();
379
+ this.agentIdToSubagent.clear();
227
380
  }
228
381
  // Private helper methods
229
382
  async emitEvent(event, subagent, data) {
@@ -239,31 +392,43 @@ export class SubagentTracker {
239
392
  }
240
393
  }
241
394
  }
242
- async sendSubagentNotification(subagent, eventType) {
395
+ async sendSubagentNotification(subagent, eventType, taskNotification) {
243
396
  if (!this.client)
244
397
  return;
398
+ const meta = {
399
+ claudeCode: {
400
+ subagent: {
401
+ id: subagent.id,
402
+ eventType,
403
+ subagentType: subagent.subagentType,
404
+ description: subagent.description,
405
+ status: subagent.status,
406
+ parentSessionId: subagent.parentSessionId,
407
+ parentToolUseId: subagent.parentToolUseId,
408
+ model: subagent.model,
409
+ runInBackground: subagent.runInBackground,
410
+ agentId: subagent.agentId,
411
+ durationMs: this.getDuration(subagent),
412
+ outputFile: subagent.outputFile,
413
+ summary: subagent.summary,
414
+ },
415
+ },
416
+ };
417
+ // Include task notification data if available
418
+ if (taskNotification) {
419
+ meta.claudeCode.taskNotification = {
420
+ taskId: taskNotification.task_id,
421
+ status: taskNotification.status,
422
+ outputFile: taskNotification.output_file,
423
+ summary: taskNotification.summary,
424
+ };
425
+ }
245
426
  const notification = {
246
427
  sessionId: subagent.parentSessionId,
247
428
  update: {
248
429
  sessionUpdate: "tool_call_update",
249
430
  toolCallId: subagent.id,
250
- _meta: {
251
- claudeCode: {
252
- subagent: {
253
- id: subagent.id,
254
- eventType,
255
- subagentType: subagent.subagentType,
256
- description: subagent.description,
257
- status: subagent.status,
258
- parentSessionId: subagent.parentSessionId,
259
- parentToolUseId: subagent.parentToolUseId,
260
- model: subagent.model,
261
- runInBackground: subagent.runInBackground,
262
- agentId: subagent.agentId,
263
- durationMs: this.getDuration(subagent),
264
- },
265
- },
266
- },
431
+ _meta: meta,
267
432
  },
268
433
  };
269
434
  await this.client.sessionUpdate(notification);
@@ -282,7 +447,9 @@ export class SubagentTracker {
282
447
  return counts;
283
448
  }
284
449
  calculateAverageDuration(subagents) {
285
- const completed = subagents.filter((s) => (s.status === "completed" || s.status === "failed") && s.startedAt && s.completedAt);
450
+ const completed = subagents.filter((s) => (s.status === "completed" || s.status === "failed" || s.status === "stopped") &&
451
+ s.startedAt &&
452
+ s.completedAt);
286
453
  if (completed.length === 0)
287
454
  return undefined;
288
455
  const totalDuration = completed.reduce((sum, s) => sum + (s.completedAt - s.startedAt), 0);
@@ -0,0 +1,106 @@
1
+ import { TrackedSubagent, SubagentTracker, SubagentStatus } from "./subagent-tracker.js";
2
+ import { Logger } from "./acp-agent.js";
3
+ export interface TaskManagerOptions {
4
+ /** Path to persistence file. Defaults to ~/.claude/acp-task-state.json */
5
+ persistencePath?: string;
6
+ /** Logger for output. Defaults to console */
7
+ logger?: Logger;
8
+ /** Enable auto-save. Defaults to true */
9
+ autoSave?: boolean;
10
+ /** Auto-save interval in milliseconds. Defaults to 30000 (30 seconds) */
11
+ autoSaveIntervalMs?: number;
12
+ /** Maximum age of tasks to keep in milliseconds. Defaults to 24 hours */
13
+ maxTaskAgeMs?: number;
14
+ }
15
+ export interface TaskFilter {
16
+ /** Filter by task status */
17
+ status?: SubagentStatus[];
18
+ /** Filter by session ID */
19
+ sessionId?: string;
20
+ /** Filter by background execution */
21
+ runInBackground?: boolean;
22
+ /** Filter by subagent type */
23
+ subagentType?: string;
24
+ /** Only include tasks newer than this timestamp */
25
+ newerThan?: number;
26
+ /** Only include tasks older than this timestamp */
27
+ olderThan?: number;
28
+ }
29
+ /**
30
+ * TaskManager handles cross-session task persistence and management.
31
+ *
32
+ * It wraps SubagentTracker and provides:
33
+ * - Persistence to disk for background tasks
34
+ * - Cross-session task querying
35
+ * - Resume capability tracking
36
+ * - Task output file reading
37
+ */
38
+ export declare class TaskManager {
39
+ private tracker;
40
+ private persistencePath;
41
+ private logger;
42
+ private autoSaveInterval?;
43
+ private maxTaskAgeMs;
44
+ private isDirty;
45
+ constructor(tracker: SubagentTracker, options?: TaskManagerOptions);
46
+ /**
47
+ * Load persisted task state from disk
48
+ */
49
+ loadState(): Promise<void>;
50
+ /**
51
+ * Save current task state to disk
52
+ */
53
+ saveState(): Promise<void>;
54
+ /**
55
+ * Force save state immediately
56
+ */
57
+ forceSave(): Promise<void>;
58
+ /**
59
+ * Get all tasks across all sessions with optional filtering
60
+ */
61
+ getAllTasks(filter?: TaskFilter): TrackedSubagent[];
62
+ /**
63
+ * Get tasks that can be resumed
64
+ */
65
+ getResumableTasks(): TrackedSubagent[];
66
+ /**
67
+ * Get a task by ID
68
+ */
69
+ getTask(taskId: string): TrackedSubagent | undefined;
70
+ /**
71
+ * Get a task by agent ID (for resume operations)
72
+ */
73
+ getTaskByAgentId(agentId: string): TrackedSubagent | undefined;
74
+ /**
75
+ * Read output file content for a background task
76
+ */
77
+ getTaskOutput(taskId: string): Promise<string | null>;
78
+ /**
79
+ * Read last N lines of output file for a background task
80
+ */
81
+ getTaskOutputTail(taskId: string, lines: number): Promise<string | null>;
82
+ /**
83
+ * Cancel a running task
84
+ */
85
+ cancelTask(taskId: string): Promise<boolean>;
86
+ /**
87
+ * Get statistics about all tasks
88
+ */
89
+ getStats(): import("./subagent-tracker.js").SubagentStats;
90
+ /**
91
+ * Get the underlying tracker
92
+ */
93
+ getTracker(): SubagentTracker;
94
+ /**
95
+ * Clean up old completed tasks
96
+ */
97
+ cleanup(): Promise<number>;
98
+ /**
99
+ * Dispose of the task manager
100
+ */
101
+ dispose(): void;
102
+ private setupEventListeners;
103
+ private startAutoSave;
104
+ }
105
+ export type { SerializedTrackerState } from "./subagent-tracker.js";
106
+ //# sourceMappingURL=task-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-manager.d.ts","sourceRoot":"","sources":["../src/task-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EACf,eAAe,EACf,cAAc,EAEf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAKxC,MAAM,WAAW,kBAAkB;IACjC,0EAA0E;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,4BAA4B;IAC5B,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;IAC1B,2BAA2B;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,8BAA8B;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAC,CAAiB;IAC1C,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,OAAO,CAAkB;gBAErB,OAAO,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,kBAAkB;IAgBlE;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAyChC;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBhC;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAKhC;;OAEG;IACH,WAAW,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,eAAe,EAAE;IA0BnD;;OAEG;IACH,iBAAiB,IAAI,eAAe,EAAE;IAItC;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIpD;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAI9D;;OAEG;IACG,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiB3D;;OAEG;IACG,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAU9E;;OAEG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYlD;;OAEG;IACH,QAAQ;IAIR;;OAEG;IACH,UAAU,IAAI,eAAe;IAI7B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;IAShC;;OAEG;IACH,OAAO,IAAI,IAAI;IAYf,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,aAAa;CAYtB;AAGD,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,231 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ /**
5
+ * TaskManager handles cross-session task persistence and management.
6
+ *
7
+ * It wraps SubagentTracker and provides:
8
+ * - Persistence to disk for background tasks
9
+ * - Cross-session task querying
10
+ * - Resume capability tracking
11
+ * - Task output file reading
12
+ */
13
+ export class TaskManager {
14
+ constructor(tracker, options) {
15
+ this.isDirty = false;
16
+ this.tracker = tracker;
17
+ this.persistencePath =
18
+ options?.persistencePath ??
19
+ path.join(os.homedir(), ".claude", "acp-task-state.json");
20
+ this.logger = options?.logger ?? console;
21
+ this.maxTaskAgeMs = options?.maxTaskAgeMs ?? 24 * 60 * 60 * 1000; // 24 hours
22
+ // Set up event listeners to mark state as dirty
23
+ this.setupEventListeners();
24
+ if (options?.autoSave !== false) {
25
+ this.startAutoSave(options?.autoSaveIntervalMs ?? 30000);
26
+ }
27
+ }
28
+ /**
29
+ * Load persisted task state from disk
30
+ */
31
+ async loadState() {
32
+ try {
33
+ if (fs.existsSync(this.persistencePath)) {
34
+ const content = await fs.promises.readFile(this.persistencePath, "utf-8");
35
+ const state = JSON.parse(content);
36
+ // Validate state version
37
+ if (state.version !== 1) {
38
+ this.logger.error(`[TaskManager] Unknown state version: ${state.version}, skipping load`);
39
+ return;
40
+ }
41
+ // Filter out tasks older than maxTaskAgeMs
42
+ const cutoff = Date.now() - this.maxTaskAgeMs;
43
+ const filteredTasks = state.tasks.filter((task) => task.createdAt > cutoff || task.status === "running");
44
+ if (filteredTasks.length !== state.tasks.length) {
45
+ this.logger.log(`[TaskManager] Filtered out ${state.tasks.length - filteredTasks.length} old tasks`);
46
+ }
47
+ // Import filtered state
48
+ this.tracker.importState({
49
+ ...state,
50
+ tasks: filteredTasks,
51
+ });
52
+ this.logger.log(`[TaskManager] Loaded ${filteredTasks.length} tasks from persistence`);
53
+ }
54
+ }
55
+ catch (error) {
56
+ this.logger.error("[TaskManager] Failed to load persisted state:", error);
57
+ }
58
+ }
59
+ /**
60
+ * Save current task state to disk
61
+ */
62
+ async saveState() {
63
+ if (!this.isDirty) {
64
+ return; // No changes to save
65
+ }
66
+ try {
67
+ const state = this.tracker.exportState();
68
+ // Ensure directory exists
69
+ const dir = path.dirname(this.persistencePath);
70
+ await fs.promises.mkdir(dir, { recursive: true });
71
+ // Write atomically using temp file
72
+ const tempPath = `${this.persistencePath}.tmp`;
73
+ await fs.promises.writeFile(tempPath, JSON.stringify(state, null, 2), "utf-8");
74
+ await fs.promises.rename(tempPath, this.persistencePath);
75
+ this.isDirty = false;
76
+ this.logger.log(`[TaskManager] Saved ${state.tasks.length} tasks to persistence`);
77
+ }
78
+ catch (error) {
79
+ this.logger.error("[TaskManager] Failed to save state:", error);
80
+ }
81
+ }
82
+ /**
83
+ * Force save state immediately
84
+ */
85
+ async forceSave() {
86
+ this.isDirty = true;
87
+ await this.saveState();
88
+ }
89
+ /**
90
+ * Get all tasks across all sessions with optional filtering
91
+ */
92
+ getAllTasks(filter) {
93
+ let tasks = this.tracker.getAllSubagents();
94
+ if (filter?.status) {
95
+ tasks = tasks.filter((t) => filter.status.includes(t.status));
96
+ }
97
+ if (filter?.sessionId) {
98
+ tasks = tasks.filter((t) => t.parentSessionId === filter.sessionId);
99
+ }
100
+ if (filter?.runInBackground !== undefined) {
101
+ tasks = tasks.filter((t) => t.runInBackground === filter.runInBackground);
102
+ }
103
+ if (filter?.subagentType) {
104
+ tasks = tasks.filter((t) => t.subagentType === filter.subagentType);
105
+ }
106
+ if (filter?.newerThan !== undefined) {
107
+ tasks = tasks.filter((t) => t.createdAt > filter.newerThan);
108
+ }
109
+ if (filter?.olderThan !== undefined) {
110
+ tasks = tasks.filter((t) => t.createdAt < filter.olderThan);
111
+ }
112
+ // Sort by creation time, newest first
113
+ return tasks.sort((a, b) => b.createdAt - a.createdAt);
114
+ }
115
+ /**
116
+ * Get tasks that can be resumed
117
+ */
118
+ getResumableTasks() {
119
+ return this.tracker.getResumableTasks();
120
+ }
121
+ /**
122
+ * Get a task by ID
123
+ */
124
+ getTask(taskId) {
125
+ return this.tracker.getSubagent(taskId);
126
+ }
127
+ /**
128
+ * Get a task by agent ID (for resume operations)
129
+ */
130
+ getTaskByAgentId(agentId) {
131
+ return this.tracker.findByAgentId(agentId);
132
+ }
133
+ /**
134
+ * Read output file content for a background task
135
+ */
136
+ async getTaskOutput(taskId) {
137
+ const task = this.tracker.getSubagent(taskId);
138
+ if (!task?.outputFile) {
139
+ return null;
140
+ }
141
+ try {
142
+ return await fs.promises.readFile(task.outputFile, "utf-8");
143
+ }
144
+ catch (error) {
145
+ this.logger.error(`[TaskManager] Failed to read output file for task ${taskId}:`, error);
146
+ return null;
147
+ }
148
+ }
149
+ /**
150
+ * Read last N lines of output file for a background task
151
+ */
152
+ async getTaskOutputTail(taskId, lines) {
153
+ const content = await this.getTaskOutput(taskId);
154
+ if (content === null) {
155
+ return null;
156
+ }
157
+ const allLines = content.split("\n");
158
+ return allLines.slice(-lines).join("\n");
159
+ }
160
+ /**
161
+ * Cancel a running task
162
+ */
163
+ async cancelTask(taskId) {
164
+ const task = this.tracker.getSubagent(taskId);
165
+ if (!task || task.status !== "running") {
166
+ return false;
167
+ }
168
+ await this.tracker.cancelSubagent(taskId);
169
+ this.isDirty = true;
170
+ await this.saveState();
171
+ return true;
172
+ }
173
+ /**
174
+ * Get statistics about all tasks
175
+ */
176
+ getStats() {
177
+ return this.tracker.getStats();
178
+ }
179
+ /**
180
+ * Get the underlying tracker
181
+ */
182
+ getTracker() {
183
+ return this.tracker;
184
+ }
185
+ /**
186
+ * Clean up old completed tasks
187
+ */
188
+ async cleanup() {
189
+ const cleaned = this.tracker.cleanup(this.maxTaskAgeMs);
190
+ if (cleaned > 0) {
191
+ this.isDirty = true;
192
+ await this.saveState();
193
+ }
194
+ return cleaned;
195
+ }
196
+ /**
197
+ * Dispose of the task manager
198
+ */
199
+ dispose() {
200
+ if (this.autoSaveInterval) {
201
+ clearInterval(this.autoSaveInterval);
202
+ this.autoSaveInterval = undefined;
203
+ }
204
+ // Final save on dispose
205
+ this.saveState().catch((error) => {
206
+ this.logger.error("[TaskManager] Failed to save on dispose:", error);
207
+ });
208
+ }
209
+ setupEventListeners() {
210
+ // Mark state as dirty when subagent events occur
211
+ const markDirty = () => {
212
+ this.isDirty = true;
213
+ };
214
+ this.tracker.addEventListener("subagent_started", markDirty);
215
+ this.tracker.addEventListener("subagent_completed", markDirty);
216
+ this.tracker.addEventListener("subagent_failed", markDirty);
217
+ this.tracker.addEventListener("subagent_cancelled", markDirty);
218
+ this.tracker.addEventListener("subagent_stopped", markDirty);
219
+ }
220
+ startAutoSave(intervalMs) {
221
+ this.autoSaveInterval = setInterval(() => {
222
+ this.saveState().catch((error) => {
223
+ this.logger.error("[TaskManager] Auto-save failed:", error);
224
+ });
225
+ }, intervalMs);
226
+ // Don't prevent process exit
227
+ if (this.autoSaveInterval.unref) {
228
+ this.autoSaveInterval.unref();
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,16 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { TaskManager } from "./task-manager.js";
3
+ import { SubagentTracker } from "./subagent-tracker.js";
4
+ export interface TaskMcpToolsOptions {
5
+ /** The SubagentTracker instance */
6
+ tracker: SubagentTracker;
7
+ /** Optional TaskManager for persistence features */
8
+ taskManager?: TaskManager;
9
+ /** Session ID for context */
10
+ sessionId: string;
11
+ }
12
+ /**
13
+ * Register MCP tools for task management
14
+ */
15
+ export declare function registerTaskMcpTools(server: McpServer, options: TaskMcpToolsOptions): void;
16
+ //# sourceMappingURL=task-mcp-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-mcp-tools.d.ts","sourceRoot":"","sources":["../src/task-mcp-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExE,MAAM,WAAW,mBAAmB;IAClC,mCAAmC;IACnC,OAAO,EAAE,eAAe,CAAC;IACzB,oDAAoD;IACpD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,6BAA6B;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,mBAAmB,GAC3B,IAAI,CA0RN"}