@agenticmail/enterprise 0.5.200 → 0.5.201

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.
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Centralized Task Queue System
3
+ *
4
+ * Tracks all agent tasks with rich metadata — before spawn, during execution,
5
+ * and after completion. Provides SSE for real-time dashboard updates.
6
+ *
7
+ * Every task flows through: created → assigned → in_progress → completed|failed|cancelled
8
+ */
9
+
10
+ import { randomUUID } from 'node:crypto';
11
+
12
+ // ─── Types ────────────────────────────────────────────────
13
+
14
+ export type TaskStatus = 'created' | 'assigned' | 'in_progress' | 'completed' | 'failed' | 'cancelled';
15
+ export type TaskPriority = 'low' | 'normal' | 'high' | 'urgent';
16
+
17
+ export interface TaskRecord {
18
+ id: string;
19
+ orgId: string;
20
+
21
+ // Who
22
+ assignedTo: string; // agent ID
23
+ assignedToName: string; // agent display name
24
+ createdBy: string; // 'system' | agent ID | user ID
25
+ createdByName: string;
26
+
27
+ // What
28
+ title: string; // short summary
29
+ description: string; // detailed task description
30
+ category: string; // 'email' | 'research' | 'meeting' | 'workflow' | 'custom'
31
+ tags: string[];
32
+
33
+ // Status
34
+ status: TaskStatus;
35
+ priority: TaskPriority;
36
+ progress: number; // 0-100
37
+
38
+ // Timing
39
+ createdAt: string;
40
+ assignedAt: string | null;
41
+ startedAt: string | null;
42
+ completedAt: string | null;
43
+ estimatedDurationMs: number | null;
44
+ actualDurationMs: number | null;
45
+
46
+ // Result
47
+ result: Record<string, unknown> | null; // outcome metadata
48
+ error: string | null;
49
+
50
+ // Relationships
51
+ parentTaskId: string | null; // for sub-tasks
52
+ relatedAgentIds: string[]; // other agents involved
53
+ sessionId: string | null; // linked session if any
54
+
55
+ // Model info
56
+ model: string | null;
57
+ fallbackModel: string | null;
58
+ modelUsed: string | null; // actual model that executed
59
+ tokensUsed: number;
60
+ costUsd: number;
61
+ }
62
+
63
+ type TaskListener = (event: TaskEvent) => void;
64
+
65
+ export interface TaskEvent {
66
+ type: 'task_created' | 'task_updated' | 'task_completed' | 'task_failed' | 'task_cancelled' | 'task_progress';
67
+ task: TaskRecord;
68
+ timestamp: string;
69
+ }
70
+
71
+ // ─── Task Queue Manager ───────────────────────────────────
72
+
73
+ export class TaskQueueManager {
74
+ private tasks = new Map<string, TaskRecord>();
75
+ private listeners = new Set<TaskListener>();
76
+ private db: any;
77
+ private initialized = false;
78
+
79
+ constructor(db?: any) {
80
+ this.db = db;
81
+ }
82
+
83
+ async init(): Promise<void> {
84
+ if (this.initialized) return;
85
+ if (this.db) {
86
+ try {
87
+ await this.db.run(`CREATE TABLE IF NOT EXISTS task_queue (
88
+ id TEXT PRIMARY KEY,
89
+ org_id TEXT NOT NULL,
90
+ assigned_to TEXT NOT NULL,
91
+ assigned_to_name TEXT NOT NULL DEFAULT '',
92
+ created_by TEXT NOT NULL DEFAULT 'system',
93
+ created_by_name TEXT NOT NULL DEFAULT '',
94
+ title TEXT NOT NULL,
95
+ description TEXT NOT NULL DEFAULT '',
96
+ category TEXT NOT NULL DEFAULT 'custom',
97
+ tags TEXT NOT NULL DEFAULT '[]',
98
+ status TEXT NOT NULL DEFAULT 'created',
99
+ priority TEXT NOT NULL DEFAULT 'normal',
100
+ progress INTEGER NOT NULL DEFAULT 0,
101
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
102
+ assigned_at TEXT,
103
+ started_at TEXT,
104
+ completed_at TEXT,
105
+ estimated_duration_ms INTEGER,
106
+ actual_duration_ms INTEGER,
107
+ result TEXT,
108
+ error TEXT,
109
+ parent_task_id TEXT,
110
+ related_agent_ids TEXT NOT NULL DEFAULT '[]',
111
+ session_id TEXT,
112
+ model TEXT,
113
+ fallback_model TEXT,
114
+ model_used TEXT,
115
+ tokens_used INTEGER NOT NULL DEFAULT 0,
116
+ cost_usd REAL NOT NULL DEFAULT 0
117
+ )`);
118
+ await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_queue_org ON task_queue(org_id)`);
119
+ await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_queue_agent ON task_queue(assigned_to)`);
120
+ await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_queue_status ON task_queue(status)`);
121
+ await this.db.run(`CREATE INDEX IF NOT EXISTS idx_task_queue_created ON task_queue(created_at)`);
122
+
123
+ // Load recent tasks into memory
124
+ const rows = await this.db.all(`SELECT * FROM task_queue WHERE status IN ('created','assigned','in_progress') OR created_at > datetime('now', '-24 hours') ORDER BY created_at DESC LIMIT 500`);
125
+ for (const row of rows || []) {
126
+ this.tasks.set(row.id, this.rowToTask(row));
127
+ }
128
+ } catch (e: any) {
129
+ console.error('[TaskQueue] DB init error:', e.message);
130
+ }
131
+ }
132
+ this.initialized = true;
133
+ }
134
+
135
+ // ─── CRUD ─────────────────────────────────────────────
136
+
137
+ async createTask(opts: {
138
+ orgId: string;
139
+ assignedTo: string;
140
+ assignedToName: string;
141
+ createdBy?: string;
142
+ createdByName?: string;
143
+ title: string;
144
+ description?: string;
145
+ category?: string;
146
+ tags?: string[];
147
+ priority?: TaskPriority;
148
+ parentTaskId?: string;
149
+ relatedAgentIds?: string[];
150
+ sessionId?: string;
151
+ model?: string;
152
+ fallbackModel?: string;
153
+ estimatedDurationMs?: number;
154
+ }): Promise<TaskRecord> {
155
+ await this.init();
156
+ const now = new Date().toISOString();
157
+ const task: TaskRecord = {
158
+ id: randomUUID(),
159
+ orgId: opts.orgId,
160
+ assignedTo: opts.assignedTo,
161
+ assignedToName: opts.assignedToName,
162
+ createdBy: opts.createdBy || 'system',
163
+ createdByName: opts.createdByName || 'System',
164
+ title: opts.title,
165
+ description: opts.description || '',
166
+ category: opts.category || 'custom',
167
+ tags: opts.tags || [],
168
+ status: 'created',
169
+ priority: opts.priority || 'normal',
170
+ progress: 0,
171
+ createdAt: now,
172
+ assignedAt: null,
173
+ startedAt: null,
174
+ completedAt: null,
175
+ estimatedDurationMs: opts.estimatedDurationMs || null,
176
+ actualDurationMs: null,
177
+ result: null,
178
+ error: null,
179
+ parentTaskId: opts.parentTaskId || null,
180
+ relatedAgentIds: opts.relatedAgentIds || [],
181
+ sessionId: opts.sessionId || null,
182
+ model: opts.model || null,
183
+ fallbackModel: opts.fallbackModel || null,
184
+ modelUsed: null,
185
+ tokensUsed: 0,
186
+ costUsd: 0,
187
+ };
188
+ this.tasks.set(task.id, task);
189
+ await this.persist(task);
190
+ this.emit({ type: 'task_created', task, timestamp: now });
191
+ return task;
192
+ }
193
+
194
+ async updateTask(taskId: string, updates: Partial<Pick<TaskRecord, 'status' | 'progress' | 'result' | 'error' | 'modelUsed' | 'tokensUsed' | 'costUsd' | 'sessionId' | 'title' | 'description' | 'priority'>>): Promise<TaskRecord | null> {
195
+ await this.init();
196
+ const task = this.tasks.get(taskId);
197
+ if (!task) return null;
198
+
199
+ const now = new Date().toISOString();
200
+
201
+ if (updates.status === 'assigned' && !task.assignedAt) task.assignedAt = now;
202
+ if (updates.status === 'in_progress' && !task.startedAt) task.startedAt = now;
203
+ if (updates.status === 'completed' || updates.status === 'failed' || updates.status === 'cancelled') {
204
+ task.completedAt = now;
205
+ if (task.startedAt) task.actualDurationMs = new Date(now).getTime() - new Date(task.startedAt).getTime();
206
+ if (updates.status === 'completed') task.progress = 100;
207
+ }
208
+
209
+ Object.assign(task, updates);
210
+ await this.persist(task);
211
+
212
+ const eventType = updates.status === 'completed' ? 'task_completed'
213
+ : updates.status === 'failed' ? 'task_failed'
214
+ : updates.status === 'cancelled' ? 'task_cancelled'
215
+ : updates.progress !== undefined ? 'task_progress'
216
+ : 'task_updated';
217
+
218
+ this.emit({ type: eventType, task, timestamp: now });
219
+ return task;
220
+ }
221
+
222
+ getTask(taskId: string): TaskRecord | undefined {
223
+ return this.tasks.get(taskId);
224
+ }
225
+
226
+ // ─── Queries ──────────────────────────────────────────
227
+
228
+ getActiveTasks(orgId?: string): TaskRecord[] {
229
+ const active: TaskRecord[] = [];
230
+ for (const t of this.tasks.values()) {
231
+ if (t.status === 'created' || t.status === 'assigned' || t.status === 'in_progress') {
232
+ if (!orgId || t.orgId === orgId) active.push(t);
233
+ }
234
+ }
235
+ return active.sort((a, b) => {
236
+ const pri = { urgent: 0, high: 1, normal: 2, low: 3 };
237
+ return (pri[a.priority] - pri[b.priority]) || (new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
238
+ });
239
+ }
240
+
241
+ getAllTasks(orgId?: string, limit = 100): TaskRecord[] {
242
+ const all: TaskRecord[] = [];
243
+ for (const t of this.tasks.values()) {
244
+ if (!orgId || t.orgId === orgId) all.push(t);
245
+ }
246
+ return all.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()).slice(0, limit);
247
+ }
248
+
249
+ getAgentTasks(agentId: string, includeCompleted = false): TaskRecord[] {
250
+ const res: TaskRecord[] = [];
251
+ for (const t of this.tasks.values()) {
252
+ if (t.assignedTo === agentId) {
253
+ if (includeCompleted || t.status === 'created' || t.status === 'assigned' || t.status === 'in_progress') {
254
+ res.push(t);
255
+ }
256
+ }
257
+ }
258
+ return res.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
259
+ }
260
+
261
+ async getTaskHistory(orgId: string, limit = 50, offset = 0): Promise<TaskRecord[]> {
262
+ if (this.db) {
263
+ try {
264
+ const rows = await this.db.all(
265
+ `SELECT * FROM task_queue WHERE org_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`,
266
+ [orgId, limit, offset]
267
+ );
268
+ return (rows || []).map((r: any) => this.rowToTask(r));
269
+ } catch { /* fall through */ }
270
+ }
271
+ return this.getAllTasks(orgId, limit);
272
+ }
273
+
274
+ getPipelineStats(orgId?: string): { created: number; assigned: number; inProgress: number; completed: number; failed: number; cancelled: number; total: number } {
275
+ const stats = { created: 0, assigned: 0, inProgress: 0, completed: 0, failed: 0, cancelled: 0, total: 0 };
276
+ for (const t of this.tasks.values()) {
277
+ if (orgId && t.orgId !== orgId) continue;
278
+ stats.total++;
279
+ if (t.status === 'created') stats.created++;
280
+ else if (t.status === 'assigned') stats.assigned++;
281
+ else if (t.status === 'in_progress') stats.inProgress++;
282
+ else if (t.status === 'completed') stats.completed++;
283
+ else if (t.status === 'failed') stats.failed++;
284
+ else if (t.status === 'cancelled') stats.cancelled++;
285
+ }
286
+ return stats;
287
+ }
288
+
289
+ // ─── SSE Subscriptions ────────────────────────────────
290
+
291
+ subscribe(listener: TaskListener): () => void {
292
+ this.listeners.add(listener);
293
+ return () => { this.listeners.delete(listener); };
294
+ }
295
+
296
+ private emit(event: TaskEvent): void {
297
+ for (const l of this.listeners) {
298
+ try { l(event); } catch { /* ignore */ }
299
+ }
300
+ }
301
+
302
+ // ─── Persistence ──────────────────────────────────────
303
+
304
+ private async persist(task: TaskRecord): Promise<void> {
305
+ if (!this.db) return;
306
+ try {
307
+ await this.db.run(`INSERT OR REPLACE INTO task_queue (
308
+ id, org_id, assigned_to, assigned_to_name, created_by, created_by_name,
309
+ title, description, category, tags, status, priority, progress,
310
+ created_at, assigned_at, started_at, completed_at,
311
+ estimated_duration_ms, actual_duration_ms, result, error,
312
+ parent_task_id, related_agent_ids, session_id,
313
+ model, fallback_model, model_used, tokens_used, cost_usd
314
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`, [
315
+ task.id, task.orgId, task.assignedTo, task.assignedToName,
316
+ task.createdBy, task.createdByName,
317
+ task.title, task.description, task.category, JSON.stringify(task.tags),
318
+ task.status, task.priority, task.progress,
319
+ task.createdAt, task.assignedAt, task.startedAt, task.completedAt,
320
+ task.estimatedDurationMs, task.actualDurationMs,
321
+ task.result ? JSON.stringify(task.result) : null,
322
+ task.error,
323
+ task.parentTaskId, JSON.stringify(task.relatedAgentIds), task.sessionId,
324
+ task.model, task.fallbackModel, task.modelUsed, task.tokensUsed, task.costUsd,
325
+ ]);
326
+ } catch (e: any) {
327
+ console.error('[TaskQueue] persist error:', e.message);
328
+ }
329
+ }
330
+
331
+ private rowToTask(row: any): TaskRecord {
332
+ return {
333
+ id: row.id,
334
+ orgId: row.org_id,
335
+ assignedTo: row.assigned_to,
336
+ assignedToName: row.assigned_to_name || '',
337
+ createdBy: row.created_by || 'system',
338
+ createdByName: row.created_by_name || '',
339
+ title: row.title,
340
+ description: row.description || '',
341
+ category: row.category || 'custom',
342
+ tags: safeJson(row.tags, []),
343
+ status: row.status as TaskStatus,
344
+ priority: (row.priority || 'normal') as TaskPriority,
345
+ progress: row.progress || 0,
346
+ createdAt: row.created_at,
347
+ assignedAt: row.assigned_at || null,
348
+ startedAt: row.started_at || null,
349
+ completedAt: row.completed_at || null,
350
+ estimatedDurationMs: row.estimated_duration_ms || null,
351
+ actualDurationMs: row.actual_duration_ms || null,
352
+ result: safeJson(row.result, null),
353
+ error: row.error || null,
354
+ parentTaskId: row.parent_task_id || null,
355
+ relatedAgentIds: safeJson(row.related_agent_ids, []),
356
+ sessionId: row.session_id || null,
357
+ model: row.model || null,
358
+ fallbackModel: row.fallback_model || null,
359
+ modelUsed: row.model_used || null,
360
+ tokensUsed: row.tokens_used || 0,
361
+ costUsd: row.cost_usd || 0,
362
+ };
363
+ }
364
+ }
365
+
366
+ function safeJson(v: any, fallback: any): any {
367
+ if (!v || typeof v !== 'string') return fallback;
368
+ try { return JSON.parse(v); } catch { return fallback; }
369
+ }