@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
JavaScript
/**
* 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
}
}
}