UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

398 lines (397 loc) 12.9 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { randomUUID } from "crypto"; import { logger } from "../../../core/monitoring/logger.js"; class CordHandlers { constructor(deps) { this.deps = deps; } MAX_DEPTH = 10; MAX_TASKS = 50; /** * cord_spawn — create a child task with clean context (only blocker results visible) */ async handleCordSpawn(args) { return this.createTask(args, "spawn"); } /** * cord_fork — create a child task with full sibling context */ async handleCordFork(args) { return this.createTask(args, "fork"); } /** * cord_complete — mark a task as completed and unblock dependents */ async handleCordComplete(args) { try { const { task_id, result } = args; if (!task_id) throw new Error("task_id is required"); if (result === void 0 || result === null) { throw new Error("result is required"); } const db = this.deps.dbAdapter.getRawDatabase(); if (!db) throw new Error("Database not available"); const task = db.prepare("SELECT * FROM cord_tasks WHERE task_id = ?").get(task_id); if (!task) throw new Error(`Task not found: ${task_id}`); if (task.status === "completed") { throw new Error(`Task already completed: ${task_id}`); } const now = Math.floor(Date.now() / 1e3); db.prepare( "UPDATE cord_tasks SET status = ?, result = ?, completed_at = ? WHERE task_id = ?" ).run("completed", String(result), now, task_id); const unblocked = this.checkAndUnblockDependents(db, task_id); logger.info("Cord task completed", { task_id, unblocked }); return { content: [ { type: "text", text: `Task ${task_id} completed.${unblocked.length > 0 ? ` Unblocked: ${unblocked.join(", ")}` : ""}` } ], metadata: { task_id, status: "completed", unblocked } }; } catch (error) { logger.error( "Error completing cord task", error instanceof Error ? error : new Error(String(error)) ); throw error; } } /** * cord_ask — create an "ask" task (question with optional options) */ async handleCordAsk(args) { try { const { question, options, parent_id } = args; if (!question) throw new Error("question is required"); const db = this.deps.dbAdapter.getRawDatabase(); if (!db) throw new Error("Database not available"); const projectId = this.getProjectId(); const runId = this.getRunId(); const taskId = randomUUID(); this.checkTaskLimit(db, projectId); let depth = 0; if (parent_id) { depth = this.computeDepth(db, parent_id); } const prompt = options ? JSON.stringify({ question, options }) : JSON.stringify({ question }); db.prepare( `INSERT INTO cord_tasks (task_id, parent_id, project_id, run_id, goal, prompt, status, context_mode, depth) VALUES (?, ?, ?, ?, ?, ?, 'asked', 'ask', ?)` ).run( taskId, parent_id || null, projectId, runId, question, prompt, depth ); logger.info("Cord ask created", { task_id: taskId }); return { content: [ { type: "text", text: `Ask created: ${taskId} \u2014 "${question}"` } ], metadata: { task_id: taskId, status: "asked", context_mode: "ask", question, options: options || null } }; } catch (error) { logger.error( "Error creating cord ask", error instanceof Error ? error : new Error(String(error)) ); throw error; } } /** * cord_tree — view the task tree with context scoping */ async handleCordTree(args) { try { const { task_id, include_results = false } = args; const db = this.deps.dbAdapter.getRawDatabase(); if (!db) throw new Error("Database not available"); const projectId = this.getProjectId(); let tasks; if (task_id) { tasks = this.getSubtree(db, task_id); } else { tasks = db.prepare( "SELECT * FROM cord_tasks WHERE project_id = ? ORDER BY depth ASC, created_at ASC" ).all(projectId); } if (tasks.length === 0) { return { content: [{ type: "text", text: "No cord tasks found." }], metadata: { tasks: [] } }; } const taskMap = /* @__PURE__ */ new Map(); for (const t of tasks) taskMap.set(t.task_id, t); const allTasks = db.prepare( "SELECT * FROM cord_tasks WHERE project_id = ? ORDER BY depth ASC, created_at ASC" ).all(projectId); const allTaskMap = /* @__PURE__ */ new Map(); for (const t of allTasks) allTaskMap.set(t.task_id, t); const treeNodes = tasks.map((t) => { const blockedBy = JSON.parse(t.blocked_by); const node = { task_id: t.task_id, goal: t.goal, status: t.status, context_mode: t.context_mode, depth: t.depth, blocked_by: blockedBy, parent_id: t.parent_id }; if (include_results && t.result !== null) { node.result = t.result; } node.visible_context = this.computeVisibleContext( t, allTaskMap, include_results ); return node; }); const summary = tasks.map( (t) => `${" ".repeat(t.depth)}[${t.status}] ${t.goal}${t.context_mode === "ask" ? " (ask)" : ""}` ).join("\n"); return { content: [ { type: "text", text: `Cord Tree (${tasks.length} tasks): ${summary}` } ], metadata: { tasks: treeNodes } }; } catch (error) { logger.error( "Error getting cord tree", error instanceof Error ? error : new Error(String(error)) ); throw error; } } // --- Private helpers --- async createTask(args, contextMode) { try { const { goal, prompt = "", blocked_by = [], parent_id } = args; if (!goal) throw new Error("goal is required"); const db = this.deps.dbAdapter.getRawDatabase(); if (!db) throw new Error("Database not available"); const projectId = this.getProjectId(); const runId = this.getRunId(); const taskId = randomUUID(); this.checkTaskLimit(db, projectId); let depth = 0; if (parent_id) { depth = this.computeDepth(db, parent_id); } const blockerIds = Array.isArray(blocked_by) ? blocked_by : []; if (blockerIds.length > 0) { this.validateBlockers(db, blockerIds); this.detectCircularDeps(db, taskId, blockerIds); } const status = this.initialStatus(db, blockerIds); db.prepare( `INSERT INTO cord_tasks (task_id, parent_id, project_id, run_id, goal, prompt, status, context_mode, blocked_by, depth) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run( taskId, parent_id || null, projectId, runId, goal, prompt, status, contextMode, JSON.stringify(blockerIds), depth ); logger.info("Cord task created", { task_id: taskId, context_mode: contextMode, status }); return { content: [ { type: "text", text: `Task ${taskId} created (${contextMode}, ${status}): ${goal}` } ], metadata: { task_id: taskId, status, context_mode: contextMode, depth, blocked_by: blockerIds } }; } catch (error) { logger.error( "Error creating cord task", error instanceof Error ? error : new Error(String(error)) ); throw error; } } checkTaskLimit(db, projectId) { const row = db.prepare("SELECT COUNT(*) as count FROM cord_tasks WHERE project_id = ?").get(projectId); if (row.count >= this.MAX_TASKS) { throw new Error( `Task limit reached: ${this.MAX_TASKS} tasks per project` ); } } computeDepth(db, parentId) { const parent = db.prepare("SELECT depth FROM cord_tasks WHERE task_id = ?").get(parentId); if (!parent) throw new Error(`Parent task not found: ${parentId}`); const depth = parent.depth + 1; if (depth >= this.MAX_DEPTH) { throw new Error(`Max depth exceeded: ${this.MAX_DEPTH}`); } return depth; } validateBlockers(db, blockerIds) { for (const id of blockerIds) { const exists = db.prepare("SELECT 1 FROM cord_tasks WHERE task_id = ?").get(id); if (!exists) throw new Error(`Blocker task not found: ${id}`); } } detectCircularDeps(db, newTaskId, blockerIds) { const visited = /* @__PURE__ */ new Set(); const queue = [...blockerIds]; while (queue.length > 0) { const current = queue.shift(); if (current === newTaskId) { throw new Error("Circular dependency detected"); } if (visited.has(current)) continue; visited.add(current); const task = db.prepare("SELECT blocked_by FROM cord_tasks WHERE task_id = ?").get(current); if (task) { const deps = JSON.parse(task.blocked_by); for (const dep of deps) { if (!visited.has(dep)) queue.push(dep); } } } } initialStatus(db, blockerIds) { if (blockerIds.length === 0) return "active"; for (const id of blockerIds) { const task = db.prepare("SELECT status FROM cord_tasks WHERE task_id = ?").get(id); if (!task || task.status !== "completed") return "blocked"; } return "active"; } checkAndUnblockDependents(db, completedTaskId) { const allBlocked = db.prepare("SELECT * FROM cord_tasks WHERE status = 'blocked'").all(); const unblocked = []; for (const task of allBlocked) { const blockers = JSON.parse(task.blocked_by); if (!blockers.includes(completedTaskId)) continue; const allDone = blockers.every((bid) => { if (bid === completedTaskId) return true; const blocker = db.prepare("SELECT status FROM cord_tasks WHERE task_id = ?").get(bid); return blocker?.status === "completed"; }); if (allDone) { db.prepare( "UPDATE cord_tasks SET status = 'active' WHERE task_id = ?" ).run(task.task_id); unblocked.push(task.task_id); } } return unblocked; } getSubtree(db, rootId) { const result = []; const queue = [rootId]; while (queue.length > 0) { const current = queue.shift(); const task = db.prepare("SELECT * FROM cord_tasks WHERE task_id = ?").get(current); if (task) { result.push(task); const children = db.prepare( "SELECT task_id FROM cord_tasks WHERE parent_id = ? ORDER BY created_at ASC" ).all(current); for (const c of children) queue.push(c.task_id); } } return result; } computeVisibleContext(task, allTaskMap, includeResults) { const ctx = { prompt: task.prompt }; if (task.context_mode === "ask") { try { const parsed = JSON.parse(task.prompt); ctx.question = parsed.question; ctx.options = parsed.options || null; } catch { ctx.question = task.goal; } if (task.status === "completed" && task.result !== null) { ctx.answer = task.result; } return ctx; } const blockerIds = JSON.parse(task.blocked_by); const blockerResults = []; for (const bid of blockerIds) { const blocker = allTaskMap.get(bid); if (blocker?.status === "completed" && blocker.result !== null) { blockerResults.push({ task_id: bid, goal: blocker.goal, result: includeResults ? blocker.result : "[completed]" }); } } if (blockerResults.length > 0) { ctx.blocker_results = blockerResults; } if (task.context_mode === "fork" && task.parent_id) { const siblingResults = []; for (const [, t] of allTaskMap) { if (t.parent_id === task.parent_id && t.task_id !== task.task_id && t.status === "completed" && t.result !== null) { siblingResults.push({ task_id: t.task_id, goal: t.goal, result: includeResults ? t.result : "[completed]" }); } } if (siblingResults.length > 0) { ctx.sibling_results = siblingResults; } } return ctx; } getProjectId() { return this.deps.dbAdapter.projectId; } getRunId() { return this.deps.frameManager.currentRunId; } } export { CordHandlers };