@johpaz/hive-core 0.1.1

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 (50) hide show
  1. package/package.json +43 -0
  2. package/src/agent/compaction.ts +161 -0
  3. package/src/agent/context-guard.ts +91 -0
  4. package/src/agent/context.ts +148 -0
  5. package/src/agent/ethics.ts +102 -0
  6. package/src/agent/hooks.ts +166 -0
  7. package/src/agent/index.ts +67 -0
  8. package/src/agent/providers/index.ts +278 -0
  9. package/src/agent/providers.ts +1 -0
  10. package/src/agent/soul.ts +89 -0
  11. package/src/agent/stuck-loop.ts +133 -0
  12. package/src/agent/user.ts +86 -0
  13. package/src/channels/base.ts +91 -0
  14. package/src/channels/discord.ts +185 -0
  15. package/src/channels/index.ts +7 -0
  16. package/src/channels/manager.ts +204 -0
  17. package/src/channels/slack.ts +209 -0
  18. package/src/channels/telegram.ts +177 -0
  19. package/src/channels/webchat.ts +83 -0
  20. package/src/channels/whatsapp.ts +305 -0
  21. package/src/config/index.ts +1 -0
  22. package/src/config/loader.ts +508 -0
  23. package/src/gateway/index.ts +5 -0
  24. package/src/gateway/lane-queue.ts +169 -0
  25. package/src/gateway/router.ts +124 -0
  26. package/src/gateway/server.ts +347 -0
  27. package/src/gateway/session.ts +131 -0
  28. package/src/gateway/slash-commands.ts +176 -0
  29. package/src/heartbeat/index.ts +157 -0
  30. package/src/index.ts +21 -0
  31. package/src/memory/index.ts +1 -0
  32. package/src/memory/notes.ts +170 -0
  33. package/src/multi-agent/bindings.ts +171 -0
  34. package/src/multi-agent/index.ts +4 -0
  35. package/src/multi-agent/manager.ts +182 -0
  36. package/src/multi-agent/sandbox.ts +130 -0
  37. package/src/multi-agent/subagents.ts +302 -0
  38. package/src/security/index.ts +187 -0
  39. package/src/tools/cron.ts +156 -0
  40. package/src/tools/exec.ts +105 -0
  41. package/src/tools/index.ts +6 -0
  42. package/src/tools/memory.ts +176 -0
  43. package/src/tools/notify.ts +53 -0
  44. package/src/tools/read.ts +154 -0
  45. package/src/tools/registry.ts +115 -0
  46. package/src/tools/web.ts +186 -0
  47. package/src/utils/crypto.ts +73 -0
  48. package/src/utils/index.ts +3 -0
  49. package/src/utils/logger.ts +254 -0
  50. package/src/utils/retry.ts +70 -0
@@ -0,0 +1,508 @@
1
+ import * as z from "zod";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as yaml from "js-yaml";
5
+
6
+ const LogLevelSchema = z.enum(["debug", "info", "warn", "error"]);
7
+ const DMPolicySchema = z.enum(["open", "pairing", "allowlist"]);
8
+ const TransportSchema = z.enum(["stdio", "sse", "websocket"]);
9
+
10
+ const expandPath = (p: string): string => {
11
+ if (p.startsWith("~")) {
12
+ return path.join(process.env.HOME || "", p.slice(1));
13
+ }
14
+ return p;
15
+ };
16
+
17
+ const expandEnvVars = (value: string): string => {
18
+ return value.replace(/\$\{([^}]+)\}/g, (_, key) => {
19
+ return process.env[key] || "";
20
+ });
21
+ };
22
+
23
+ const expandEnvInObject = <T>(obj: T): T => {
24
+ if (typeof obj === "string") {
25
+ return expandEnvVars(obj) as T;
26
+ }
27
+ if (Array.isArray(obj)) {
28
+ return obj.map(expandEnvInObject) as T;
29
+ }
30
+ if (obj !== null && typeof obj === "object") {
31
+ const result: Record<string, unknown> = {};
32
+ for (const [key, value] of Object.entries(obj)) {
33
+ result[key] = expandEnvInObject(value);
34
+ }
35
+ return result as T;
36
+ }
37
+ return obj;
38
+ };
39
+
40
+ const ProviderConfigSchema = z.object({
41
+ apiKey: z.string().optional(),
42
+ baseUrl: z.string().optional(),
43
+ rateLimit: z.number().optional(),
44
+ retries: z.number().optional(),
45
+ retryDelayMs: z.number().optional(),
46
+ });
47
+
48
+ const ToolRestrictionsSchema = z.object({
49
+ allow: z.array(z.string()).optional(),
50
+ deny: z.array(z.string()).optional(),
51
+ });
52
+
53
+ const ExecConfigSchema = z.object({
54
+ enabled: z.boolean().optional(),
55
+ allowlist: z.array(z.string()).optional(),
56
+ denylist: z.array(z.string()).optional(),
57
+ timeoutSeconds: z.number().optional(),
58
+ workDir: z.string().optional(),
59
+ });
60
+
61
+ const WebConfigSchema = z.object({
62
+ allowlist: z.array(z.string()).optional(),
63
+ denylist: z.array(z.string()).optional(),
64
+ timeoutSeconds: z.number().optional(),
65
+ });
66
+
67
+ const BrowserConfigSchema = z.object({
68
+ enabled: z.boolean().optional(),
69
+ browserPath: z.string().optional(),
70
+ headless: z.boolean().optional(),
71
+ timeoutSeconds: z.number().optional(),
72
+ });
73
+
74
+ const CanvasConfigSchema = z.object({
75
+ enabled: z.boolean().optional(),
76
+ port: z.number().optional(),
77
+ });
78
+
79
+ const SandboxConfigSchema = z.object({
80
+ dm: ToolRestrictionsSchema.optional(),
81
+ group: ToolRestrictionsSchema.optional(),
82
+ });
83
+
84
+ const ToolsConfigSchema = z.object({
85
+ allow: z.array(z.string()).optional(),
86
+ deny: z.array(z.string()).optional(),
87
+ exec: ExecConfigSchema.optional(),
88
+ web: WebConfigSchema.optional(),
89
+ browser: BrowserConfigSchema.optional(),
90
+ canvas: CanvasConfigSchema.optional(),
91
+ sandbox: SandboxConfigSchema.optional(),
92
+ });
93
+
94
+ const ContextConfigSchema = z.object({
95
+ maxTokens: z.number().optional(),
96
+ compactionThreshold: z.number().optional(),
97
+ minMessagesAfterCompaction: z.number().optional(),
98
+ maxCompactionRetries: z.number().optional(),
99
+ });
100
+
101
+ const AgentEntrySchema = z.object({
102
+ id: z.string(),
103
+ default: z.boolean().optional(),
104
+ workspace: z.string(),
105
+ description: z.string().optional(),
106
+ });
107
+
108
+ const AccountConfigSchema = z.object({
109
+ botToken: z.string().optional(),
110
+ applicationId: z.string().optional(),
111
+ appToken: z.string().optional(),
112
+ signingSecret: z.string().optional(),
113
+ dmPolicy: DMPolicySchema.optional(),
114
+ allowFrom: z.array(z.string()).optional(),
115
+ });
116
+
117
+ const ChannelConfigSchema = z.object({
118
+ enabled: z.boolean().optional(),
119
+ accounts: z.record(z.string(), AccountConfigSchema).optional(),
120
+ dmPolicy: DMPolicySchema.optional(),
121
+ allowFrom: z.array(z.string()).optional(),
122
+ groups: z.boolean().optional(),
123
+ guilds: z.record(z.string(), z.unknown()).optional(),
124
+ experimental: z.boolean().optional(),
125
+ });
126
+
127
+ const PeerMatchSchema = z.object({
128
+ kind: z.enum(["direct", "group"]).optional(),
129
+ id: z.string().optional(),
130
+ });
131
+
132
+ const BindingMatchSchema = z.object({
133
+ channel: z.string().optional(),
134
+ accountId: z.string().optional(),
135
+ peer: PeerMatchSchema.optional(),
136
+ guildId: z.string().optional(),
137
+ teamId: z.string().optional(),
138
+ roles: z.array(z.string()).optional(),
139
+ });
140
+
141
+ const BindingSchema = z.object({
142
+ agentId: z.string(),
143
+ match: BindingMatchSchema,
144
+ });
145
+
146
+ const MCPServerConfigSchema = z.object({
147
+ enabled: z.boolean().optional(),
148
+ transport: TransportSchema,
149
+ command: z.string().optional(),
150
+ args: z.array(z.string()).optional(),
151
+ env: z.record(z.string(), z.string()).optional(),
152
+ url: z.string().optional(),
153
+ headers: z.record(z.string(), z.string()).optional(),
154
+ reconnect: z.object({
155
+ enabled: z.boolean().optional(),
156
+ maxRetries: z.number().optional(),
157
+ delayMs: z.number().optional(),
158
+ backoffMultiplier: z.number().optional(),
159
+ }).optional(),
160
+ });
161
+
162
+ const MCPConfigSchema = z.object({
163
+ enabled: z.boolean().optional(),
164
+ servers: z.record(z.string(), MCPServerConfigSchema).optional(),
165
+ healthCheck: z.object({
166
+ enabled: z.boolean().optional(),
167
+ intervalSeconds: z.number().optional(),
168
+ }).optional(),
169
+ });
170
+
171
+ const EpisodicMemoryConfigSchema = z.object({
172
+ enabled: z.boolean().optional(),
173
+ provider: z.enum(["openai", "local"]).optional(),
174
+ maxEpisodesPerSession: z.number().optional(),
175
+ });
176
+
177
+ const MemoryConfigSchema = z.object({
178
+ dbPath: z.string().optional(),
179
+ notesDir: z.string().optional(),
180
+ episodic: EpisodicMemoryConfigSchema.optional(),
181
+ });
182
+
183
+ const CronConfigSchema = z.object({
184
+ enabled: z.boolean().optional(),
185
+ dbPath: z.string().optional(),
186
+ maxConcurrentJobs: z.number().optional(),
187
+ timezone: z.string().optional(),
188
+ });
189
+
190
+ const RetryConfigSchema = z.object({
191
+ maxAttempts: z.number().optional(),
192
+ initialDelayMs: z.number().optional(),
193
+ backoffMultiplier: z.number().optional(),
194
+ maxDelayMs: z.number().optional(),
195
+ });
196
+
197
+ const HooksConfigSchema = z.object({
198
+ scripts: z.object({
199
+ before_model_resolve: z.string().optional(),
200
+ before_prompt_build: z.string().optional(),
201
+ before_tool_call: z.string().optional(),
202
+ after_tool_call: z.string().optional(),
203
+ tool_result_persist: z.string().optional(),
204
+ before_compaction: z.string().optional(),
205
+ after_compaction: z.string().optional(),
206
+ message_received: z.string().optional(),
207
+ message_sending: z.string().optional(),
208
+ message_sent: z.string().optional(),
209
+ session_start: z.string().optional(),
210
+ session_end: z.string().optional(),
211
+ gateway_start: z.string().optional(),
212
+ gateway_stop: z.string().optional(),
213
+ }).optional(),
214
+ });
215
+
216
+ const LoggingConfigSchema = z.object({
217
+ level: LogLevelSchema.optional(),
218
+ dir: z.string().optional(),
219
+ maxSizeMB: z.number().optional(),
220
+ maxFiles: z.number().optional(),
221
+ redactSensitive: z.boolean().optional(),
222
+ console: z.boolean().optional(),
223
+ });
224
+
225
+ const GatewayConfigSchema = z.object({
226
+ host: z.string().optional(),
227
+ port: z.number().optional(),
228
+ authToken: z.string().optional(),
229
+ pidFile: z.string().optional(),
230
+ tools: ToolRestrictionsSchema.optional(),
231
+ });
232
+
233
+ const ModelsConfigSchema = z.object({
234
+ defaultProvider: z.enum(["openai", "anthropic", "gemini", "kimi", "ollama", "openrouter", "deepseek"]).optional(),
235
+ defaults: z.record(z.string(), z.string()).optional(),
236
+ providers: z.record(z.string(), ProviderConfigSchema).optional(),
237
+ });
238
+
239
+ const SessionsConfigSchema = z.object({
240
+ dir: z.string().optional(),
241
+ pruneAfterHours: z.number().optional(),
242
+ maxTranscriptSizeMB: z.number().optional(),
243
+ });
244
+
245
+ const SkillsConfigSchema = z.object({
246
+ allowBundled: z.array(z.string()).optional(),
247
+ managedDir: z.string().optional(),
248
+ extraDirs: z.array(z.string()).optional(),
249
+ hotReload: z.boolean().optional(),
250
+ maxSkillSizeKB: z.number().optional(),
251
+ });
252
+
253
+ const SecurityConfigSchema = z.object({
254
+ maxMessageLength: z.record(z.string(), z.number()).optional(),
255
+ skillScanning: z.boolean().optional(),
256
+ warnOnInsecureConfig: z.boolean().optional(),
257
+ allowedUsers: z.array(z.string()).optional(),
258
+ });
259
+
260
+ const ConfigSchema = z.object({
261
+ gateway: GatewayConfigSchema.optional(),
262
+ logging: LoggingConfigSchema.optional(),
263
+ agent: z.object({
264
+ defaultAgentId: z.string().optional(),
265
+ baseDir: z.string().optional(),
266
+ context: ContextConfigSchema.optional(),
267
+ }).optional(),
268
+ models: ModelsConfigSchema.optional(),
269
+ sessions: SessionsConfigSchema.optional(),
270
+ agents: z.object({
271
+ list: z.array(AgentEntrySchema).optional(),
272
+ }).optional(),
273
+ bindings: z.array(BindingSchema).optional(),
274
+ channels: z.record(z.string(), ChannelConfigSchema).optional(),
275
+ tools: ToolsConfigSchema.optional(),
276
+ skills: SkillsConfigSchema.optional(),
277
+ mcp: MCPConfigSchema.optional(),
278
+ memory: MemoryConfigSchema.optional(),
279
+ cron: CronConfigSchema.optional(),
280
+ retry: RetryConfigSchema.optional(),
281
+ security: SecurityConfigSchema.optional(),
282
+ hooks: HooksConfigSchema.optional(),
283
+ });
284
+
285
+ export type Config = z.infer<typeof ConfigSchema>;
286
+
287
+ export type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
288
+ export type MCPServerConfig = z.infer<typeof MCPServerConfigSchema>;
289
+ export type AgentEntry = z.infer<typeof AgentEntrySchema>;
290
+ export type Binding = z.infer<typeof BindingSchema>;
291
+
292
+ const DEFAULT_CONFIG: Config = {
293
+ gateway: {
294
+ host: "127.0.0.1",
295
+ port: 18790,
296
+ pidFile: "~/.hive/gateway.pid",
297
+ tools: {
298
+ allow: [],
299
+ deny: ["exec", "write", "edit", "apply_patch"],
300
+ },
301
+ },
302
+ logging: {
303
+ level: "info",
304
+ dir: "~/.hive/logs",
305
+ maxSizeMB: 10,
306
+ maxFiles: 5,
307
+ redactSensitive: true,
308
+ console: true,
309
+ },
310
+ agent: {
311
+ defaultAgentId: "main",
312
+ baseDir: "~/.hive/agents",
313
+ context: {
314
+ maxTokens: 0,
315
+ compactionThreshold: 0.8,
316
+ minMessagesAfterCompaction: 4,
317
+ maxCompactionRetries: 3,
318
+ },
319
+ },
320
+ models: {
321
+ defaultProvider: "openai",
322
+ defaults: {
323
+ openai: "gpt-4o",
324
+ anthropic: "claude-sonnet-4-20250514",
325
+ ollama: "llama3.2",
326
+ openrouter: "anthropic/claude-sonnet-4",
327
+ },
328
+ providers: {},
329
+ },
330
+ sessions: {
331
+ dir: "~/.hive/sessions",
332
+ pruneAfterHours: 24,
333
+ maxTranscriptSizeMB: 50,
334
+ },
335
+ agents: {
336
+ list: [
337
+ {
338
+ id: "main",
339
+ default: true,
340
+ workspace: "~/.hive/agents/main/workspace",
341
+ description: "Default personal assistant",
342
+ },
343
+ ],
344
+ },
345
+ bindings: [],
346
+ channels: {
347
+ webchat: { enabled: true },
348
+ },
349
+ tools: {
350
+ allow: ["read", "web_search", "web_fetch", "notify"],
351
+ deny: [],
352
+ exec: {
353
+ enabled: false,
354
+ allowlist: [],
355
+ denylist: ["rm -rf /", "sudo", "chmod 777", "> /dev/", "mkfs"],
356
+ timeoutSeconds: 30,
357
+ workDir: "~/.hive/exec",
358
+ },
359
+ web: {
360
+ allowlist: [],
361
+ denylist: ["file://", "ftp://"],
362
+ timeoutSeconds: 30,
363
+ },
364
+ browser: {
365
+ enabled: false,
366
+ headless: true,
367
+ timeoutSeconds: 30,
368
+ },
369
+ canvas: {
370
+ enabled: false,
371
+ port: 18793,
372
+ },
373
+ sandbox: {
374
+ dm: { deny: ["exec", "write", "edit", "apply_patch"] },
375
+ group: { deny: ["exec", "write", "edit", "apply_patch"] },
376
+ },
377
+ },
378
+ skills: {
379
+ allowBundled: [
380
+ "web_search",
381
+ "shell",
382
+ "file_manager",
383
+ "http_client",
384
+ "memory",
385
+ "cron_manager",
386
+ "system_notify",
387
+ "browser_automation",
388
+ "context_compact",
389
+ ],
390
+ managedDir: "~/.hive/skills",
391
+ extraDirs: [],
392
+ hotReload: true,
393
+ maxSkillSizeKB: 100,
394
+ },
395
+ mcp: {
396
+ enabled: true,
397
+ servers: {},
398
+ healthCheck: {
399
+ enabled: true,
400
+ intervalSeconds: 60,
401
+ },
402
+ },
403
+ memory: {
404
+ dbPath: "~/.hive/memory.db",
405
+ notesDir: "~/.hive/agents/main/workspace/memory",
406
+ episodic: {
407
+ enabled: false,
408
+ provider: "openai",
409
+ maxEpisodesPerSession: 100,
410
+ },
411
+ },
412
+ cron: {
413
+ enabled: true,
414
+ dbPath: "~/.hive/cron.db",
415
+ maxConcurrentJobs: 5,
416
+ timezone: "UTC",
417
+ },
418
+ retry: {
419
+ maxAttempts: 3,
420
+ initialDelayMs: 1000,
421
+ backoffMultiplier: 2,
422
+ maxDelayMs: 30000,
423
+ },
424
+ security: {
425
+ maxMessageLength: {
426
+ telegram: 4096,
427
+ discord: 2000,
428
+ slack: 40000,
429
+ webchat: 100000,
430
+ whatsapp: 65536,
431
+ },
432
+ skillScanning: true,
433
+ warnOnInsecureConfig: true,
434
+ },
435
+ hooks: {
436
+ scripts: {},
437
+ },
438
+ };
439
+
440
+ function deepMerge<T extends Record<string, unknown>>(target: T, source: Partial<T>): T {
441
+ const result = { ...target };
442
+
443
+ for (const key of Object.keys(source) as (keyof T)[]) {
444
+ const sourceValue = source[key];
445
+ const targetValue = result[key];
446
+
447
+ if (
448
+ sourceValue !== undefined &&
449
+ sourceValue !== null &&
450
+ typeof sourceValue === "object" &&
451
+ !Array.isArray(sourceValue) &&
452
+ targetValue !== undefined &&
453
+ targetValue !== null &&
454
+ typeof targetValue === "object" &&
455
+ !Array.isArray(targetValue)
456
+ ) {
457
+ result[key] = deepMerge(
458
+ targetValue as Record<string, unknown>,
459
+ sourceValue as Record<string, unknown>
460
+ ) as T[keyof T];
461
+ } else if (sourceValue !== undefined) {
462
+ result[key] = sourceValue as T[keyof T];
463
+ }
464
+ }
465
+
466
+ return result;
467
+ }
468
+
469
+ export function loadConfig(configPath?: string): Config {
470
+ const paths = configPath
471
+ ? [configPath]
472
+ : [
473
+ path.join(process.cwd(), "CONFIG.yaml"),
474
+ path.join(process.cwd(), "config.yaml"),
475
+ path.join(process.env.HOME || "", ".hive", "CONFIG.yaml"),
476
+ ];
477
+
478
+ let loadedConfig: Partial<Config> = {};
479
+
480
+ for (const p of paths) {
481
+ if (fs.existsSync(p)) {
482
+ try {
483
+ const content = fs.readFileSync(p, "utf-8");
484
+ const parsed = yaml.load(content);
485
+ loadedConfig = expandEnvInObject(parsed as Partial<Config>);
486
+ break;
487
+ } catch (error) {
488
+ throw new Error(`Failed to parse config file ${p}: ${error}`);
489
+ }
490
+ }
491
+ }
492
+
493
+ const merged = deepMerge(DEFAULT_CONFIG, loadedConfig);
494
+
495
+ const result = ConfigSchema.safeParse(merged);
496
+ if (!result.success) {
497
+ throw new Error(`Invalid configuration: ${result.error.message}`);
498
+ }
499
+
500
+ return result.data;
501
+ }
502
+
503
+ export function expandConfigPath(p: string | undefined): string | undefined {
504
+ if (!p) return undefined;
505
+ return expandPath(p);
506
+ }
507
+
508
+ export { expandPath };
@@ -0,0 +1,5 @@
1
+ import { loadConfig } from "../config/loader.ts";
2
+ import { startGateway } from "./server.ts";
3
+ import { logger } from "../utils/logger.ts";
4
+
5
+ export { loadConfig, startGateway, logger };
@@ -0,0 +1,169 @@
1
+ export type TaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
2
+
3
+ export interface Task {
4
+ id: string;
5
+ sessionId: string;
6
+ status: TaskStatus;
7
+ priority: number;
8
+ createdAt: Date;
9
+ startedAt?: Date;
10
+ completedAt?: Date;
11
+ error?: string;
12
+ abortController: AbortController;
13
+ }
14
+
15
+ export interface LaneQueueOptions {
16
+ maxConcurrency?: number;
17
+ taskTimeoutMs?: number;
18
+ }
19
+
20
+ type TaskHandler<T> = (task: Task, signal: AbortSignal) => Promise<T>;
21
+
22
+ export class LaneQueue {
23
+ private queues: Map<string, Task[]> = new Map();
24
+ private running: Map<string, Task> = new Map();
25
+ private handlers: Map<string, TaskHandler<unknown>> = new Map();
26
+ private taskIdCounter = 0;
27
+ private options: Required<LaneQueueOptions>;
28
+
29
+ constructor(options: LaneQueueOptions = {}) {
30
+ this.options = {
31
+ maxConcurrency: options.maxConcurrency ?? 1,
32
+ taskTimeoutMs: options.taskTimeoutMs ?? 300000,
33
+ };
34
+ }
35
+
36
+ private generateTaskId(): string {
37
+ return `task-${Date.now()}-${++this.taskIdCounter}`;
38
+ }
39
+
40
+ private getQueue(sessionId: string): Task[] {
41
+ let queue = this.queues.get(sessionId);
42
+ if (!queue) {
43
+ queue = [];
44
+ this.queues.set(sessionId, queue);
45
+ }
46
+ return queue;
47
+ }
48
+
49
+ enqueue<T>(
50
+ sessionId: string,
51
+ handler: TaskHandler<T>,
52
+ priority = 0
53
+ ): Task {
54
+ const task: Task = {
55
+ id: this.generateTaskId(),
56
+ sessionId,
57
+ status: "pending",
58
+ priority,
59
+ createdAt: new Date(),
60
+ abortController: new AbortController(),
61
+ };
62
+
63
+ this.handlers.set(task.id, handler as TaskHandler<unknown>);
64
+
65
+ const queue = this.getQueue(sessionId);
66
+ queue.push(task);
67
+ queue.sort((a, b) => b.priority - a.priority);
68
+
69
+ this.processQueue(sessionId);
70
+
71
+ return task;
72
+ }
73
+
74
+ private async processQueue(sessionId: string): Promise<void> {
75
+ const running = this.running.get(sessionId);
76
+ if (running) {
77
+ return;
78
+ }
79
+
80
+ const queue = this.getQueue(sessionId);
81
+ if (queue.length === 0) {
82
+ return;
83
+ }
84
+
85
+ const task = queue.shift();
86
+ if (!task) return;
87
+
88
+ task.status = "running";
89
+ task.startedAt = new Date();
90
+ this.running.set(sessionId, task);
91
+
92
+ const handler = this.handlers.get(task.id);
93
+
94
+ const timeoutId = setTimeout(() => {
95
+ task.abortController.abort();
96
+ }, this.options.taskTimeoutMs);
97
+
98
+ try {
99
+ if (handler) {
100
+ await handler(task, task.abortController.signal);
101
+ }
102
+ task.status = "completed";
103
+ task.completedAt = new Date();
104
+ } catch (error) {
105
+ if ((error as Error).name === "AbortError") {
106
+ task.status = "cancelled";
107
+ } else {
108
+ task.status = "failed";
109
+ task.error = (error as Error).message;
110
+ }
111
+ task.completedAt = new Date();
112
+ } finally {
113
+ clearTimeout(timeoutId);
114
+ this.running.delete(sessionId);
115
+ this.handlers.delete(task.id);
116
+
117
+ if (queue.length > 0) {
118
+ this.processQueue(sessionId);
119
+ }
120
+ }
121
+ }
122
+
123
+ cancel(sessionId: string): boolean {
124
+ const task = this.running.get(sessionId);
125
+ if (task) {
126
+ task.abortController.abort();
127
+ return true;
128
+ }
129
+
130
+ const queue = this.getQueue(sessionId);
131
+ const index = queue.findIndex((t) => t.status === "pending");
132
+ if (index >= 0) {
133
+ const cancelled = queue.splice(index, 1)[0];
134
+ if (cancelled) {
135
+ cancelled.status = "cancelled";
136
+ cancelled.completedAt = new Date();
137
+ }
138
+ return true;
139
+ }
140
+
141
+ return false;
142
+ }
143
+
144
+ getStatus(sessionId: string): {
145
+ queueLength: number;
146
+ running?: Task;
147
+ } {
148
+ const queue = this.getQueue(sessionId);
149
+ const running = this.running.get(sessionId);
150
+
151
+ return {
152
+ queueLength: queue.length,
153
+ running,
154
+ };
155
+ }
156
+
157
+ isProcessing(sessionId: string): boolean {
158
+ return this.running.has(sessionId);
159
+ }
160
+
161
+ prune(sessionId: string): void {
162
+ const queue = this.getQueue(sessionId);
163
+ if (queue.length === 0 && !this.running.has(sessionId)) {
164
+ this.queues.delete(sessionId);
165
+ }
166
+ }
167
+ }
168
+
169
+ export const laneQueue = new LaneQueue();