UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

179 lines (178 loc) 6.89 kB
/** * FileTaskStore — File-based persistence for TaskManager. * Used automatically when backend is "node-timeout". * * Storage layout: * {storePath} — tasks.json (all task definitions) * {logsPath}/{taskId}.jsonl — run log per task (append-only) * Continuation history is in-memory only (lost on restart). */ import { existsSync, mkdirSync, readFileSync } from "node:fs"; import { appendFile, readFile, rename, unlink, writeFile, } from "node:fs/promises"; import { dirname, join } from "node:path"; import { logger } from "../../utils/logger.js"; import { TaskError } from "../errors.js"; import { TASK_DEFAULTS, } from "../../types/index.js"; export class FileTaskStore { type = "file"; storePath; logsPath; maxRunLogs; maxHistoryEntries; tasks = new Map(); /** In-memory only — lost on restart */ history = new Map(); flushQueue = Promise.resolve(); constructor(config) { this.storePath = config.storePath ?? TASK_DEFAULTS.storePath; this.logsPath = config.logsPath ?? TASK_DEFAULTS.logsPath; this.maxRunLogs = config.maxRunLogs ?? TASK_DEFAULTS.maxRunLogs; this.maxHistoryEntries = config.maxHistoryEntries ?? TASK_DEFAULTS.maxHistoryEntries; } async initialize() { // Ensure directories exist mkdirSync(dirname(this.storePath), { recursive: true }); mkdirSync(this.logsPath, { recursive: true }); // Load existing tasks if (existsSync(this.storePath)) { try { const raw = readFileSync(this.storePath, "utf-8"); const data = JSON.parse(raw); for (const [id, task] of Object.entries(data.tasks)) { this.tasks.set(id, task); } logger.info("[TaskStore:File] Loaded tasks", { count: this.tasks.size, }); } catch (err) { logger.error("[TaskStore:File] Failed to load tasks file", { error: String(err), }); } } } async shutdown() { await this.flush(); this.tasks.clear(); this.history.clear(); } // ── Task CRUD ─────────────────────────────────────────── async save(task) { this.tasks.set(task.id, task); await this.flush(); } async get(taskId) { return this.tasks.get(taskId) ?? null; } async list(filter) { let tasks = Array.from(this.tasks.values()); if (filter?.status) { tasks = tasks.filter((t) => t.status === filter.status); } return tasks.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); } async update(taskId, updates) { const existing = this.tasks.get(taskId); if (!existing) { throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`); } const updated = { ...existing, ...updates, id: existing.id, // ID is immutable updatedAt: new Date().toISOString(), }; this.tasks.set(taskId, updated); await this.flush(); return updated; } async delete(taskId) { this.tasks.delete(taskId); this.history.delete(taskId); // Delete run log file const logPath = join(this.logsPath, `${taskId}.jsonl`); try { await unlink(logPath); } catch { // File may not exist if task never ran } await this.flush(); } // ── Run Logs ────────────────────────────────────────── async appendRun(taskId, run) { const logPath = join(this.logsPath, `${taskId}.jsonl`); mkdirSync(dirname(logPath), { recursive: true }); await appendFile(logPath, JSON.stringify(run) + "\n", "utf-8"); await this.pruneRunLog(logPath); } async getRuns(taskId, options) { const logPath = join(this.logsPath, `${taskId}.jsonl`); if (!existsSync(logPath)) { return []; } const content = await readFile(logPath, "utf-8"); const lines = content.trim().split("\n").filter(Boolean); let runs = lines.map((line) => JSON.parse(line)); if (options?.status) { runs = runs.filter((r) => r.status === options.status); } // Return newest first, limited runs.reverse(); const limit = options?.limit ?? 20; return runs.slice(0, limit); } // ── Continuation History (in-memory only) ───────────── async appendHistory(taskId, messages) { const existing = this.history.get(taskId) ?? []; existing.push(...messages); // Trim to keep only the most recent entries, preventing unbounded growth if (existing.length > this.maxHistoryEntries) { const trimmed = existing.slice(-this.maxHistoryEntries); this.history.set(taskId, trimmed); } else { this.history.set(taskId, existing); } } async getHistory(taskId) { return this.history.get(taskId) ?? []; } async clearHistory(taskId) { this.history.delete(taskId); } // ── Internal ────────────────────────────────────────── /** Write all tasks to disk atomically, serialized via promise queue */ async flush() { this.flushQueue = this.flushQueue.then(async () => { const data = { version: 1, tasks: Object.fromEntries(this.tasks), }; const dir = dirname(this.storePath); mkdirSync(dir, { recursive: true }); // Write to temp file first, then atomic rename const tmpPath = this.storePath + ".tmp"; await writeFile(tmpPath, JSON.stringify(data, null, 2), "utf-8"); await rename(tmpPath, this.storePath); }); await this.flushQueue; } /** Prune run log if it exceeds maxRunLogs entries */ async pruneRunLog(logPath) { try { const content = await readFile(logPath, "utf-8"); const lines = content.trim().split("\n").filter(Boolean); if (lines.length > this.maxRunLogs) { // Keep the most recent entries const trimmed = lines.slice(-this.maxRunLogs); await writeFile(logPath, trimmed.join("\n") + "\n", "utf-8"); } } catch { // Pruning is best-effort } } }