titan-agent 5.5.10 → 5.5.12

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.
@@ -5,6 +5,7 @@ import { join } from "path";
5
5
  import { TITAN_HOME } from "../utils/constants.js";
6
6
  import { mkdirIfNotExists } from "../utils/helpers.js";
7
7
  import { titanEvents } from "./daemon.js";
8
+ import { createMtimeCache } from "../utils/mtimeCache.js";
8
9
  import logger from "../utils/logger.js";
9
10
  const COMPONENT = "Goals";
10
11
  const GOALS_PATH = join(TITAN_HOME, "goals.json");
@@ -56,26 +57,20 @@ function isRateLimited(force) {
56
57
  saveRateState({ creations: recent });
57
58
  return { limited: false };
58
59
  }
59
- let goalsCache = null;
60
- function loadGoals() {
61
- if (goalsCache) return goalsCache;
62
- if (!existsSync(GOALS_PATH)) {
63
- goalsCache = [];
64
- return goalsCache;
65
- }
66
- try {
67
- const raw = readFileSync(GOALS_PATH, "utf-8");
60
+ const goalsCache = createMtimeCache({
61
+ path: GOALS_PATH,
62
+ parse: (raw) => {
68
63
  const store = JSON.parse(raw);
69
- goalsCache = store.goals || [];
70
- return goalsCache;
71
- } catch (err) {
72
- logger.warn(COMPONENT, `Failed to load goals: ${err.message}`);
73
- goalsCache = [];
74
- return goalsCache;
75
- }
64
+ return store.goals || [];
65
+ },
66
+ initial: () => [],
67
+ component: COMPONENT
68
+ });
69
+ function loadGoals() {
70
+ return goalsCache.read();
76
71
  }
77
- function saveGoals() {
78
- const goals = goalsCache || [];
72
+ function saveGoals(goalsOverride) {
73
+ const goals = goalsOverride ?? goalsCache.read();
79
74
  try {
80
75
  mkdirIfNotExists(TITAN_HOME);
81
76
  const store = {
@@ -83,6 +78,7 @@ function saveGoals() {
83
78
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
84
79
  };
85
80
  writeFileSync(GOALS_PATH, JSON.stringify(store, null, 2), "utf-8");
81
+ goalsCache.set(goals, "saveGoals");
86
82
  } catch (err) {
87
83
  logger.error(COMPONENT, `Failed to save goals: ${err.message}`);
88
84
  }
@@ -177,8 +173,7 @@ function createGoal(options) {
177
173
  parentGoalId: options.parentGoalId
178
174
  };
179
175
  goals.push(goal);
180
- goalsCache = goals;
181
- saveGoals();
176
+ saveGoals(goals);
182
177
  logger.info(COMPONENT, `Goal created: "${goal.title}" (${goal.id}) with ${goal.subtasks.length} subtasks`);
183
178
  titanEvents.emit("goal:created", { goalId: goal.id, title: goal.title, subtasks: goal.subtasks.length });
184
179
  return goal;
@@ -208,8 +203,7 @@ function updateGoal(goalId, updates) {
208
203
  goal.progress = 100;
209
204
  }
210
205
  goal.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
211
- goalsCache = goals;
212
- saveGoals();
206
+ saveGoals(goals);
213
207
  logger.info(COMPONENT, `Goal updated: "${goal.title}" (${goal.id})`);
214
208
  return goal;
215
209
  }
@@ -218,8 +212,7 @@ function deleteGoal(goalId) {
218
212
  const idx = goals.findIndex((g) => g.id === goalId);
219
213
  if (idx === -1) return false;
220
214
  goals.splice(idx, 1);
221
- goalsCache = goals;
222
- saveGoals();
215
+ saveGoals(goals);
223
216
  logger.info(COMPONENT, `Goal deleted: ${goalId}`);
224
217
  return true;
225
218
  }
@@ -384,7 +377,7 @@ function addDynamicSubtask(goalId, afterSubtaskId, title, description) {
384
377
  return subtask;
385
378
  }
386
379
  function reloadGoals() {
387
- goalsCache = null;
380
+ goalsCache.invalidate();
388
381
  loadGoals();
389
382
  }
390
383
  function dedupeGoalsBulk() {
@@ -408,8 +401,7 @@ function dedupeGoalsBulk() {
408
401
  }
409
402
  }
410
403
  if (closed > 0) {
411
- goalsCache = goals;
412
- saveGoals();
404
+ saveGoals(goals);
413
405
  }
414
406
  return { scanned: goals.length, closed, kept: seen.size };
415
407
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/agent/goals.ts"],"sourcesContent":["/**\n * TITAN — Goal Management System\n * Persistent goals with subtasks, scheduling, budget tracking, and progress monitoring.\n * Goals drive the autopilot system — each cycle picks the next actionable subtask.\n */\nimport { v4 as uuid } from 'uuid';\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { mkdirIfNotExists } from '../utils/helpers.js';\nimport { titanEvents } from './daemon.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Goals';\nconst GOALS_PATH = join(TITAN_HOME, 'goals.json');\nconst RATE_PATH = join(TITAN_HOME, 'goal-creation-rate.json');\n\n/** Safety limits — prevent runaway goal proliferation (SOMA or agent loops). */\nconst MAX_TOTAL_GOALS = 150;\nconst MAX_ACTIVE_GOALS = 50;\nconst MAX_GOALS_PER_HOUR = 10;\nconst RECENT_DEDUPE_HOURS = 24;\nconst SIMILARITY_THRESHOLD = 0.82; // Jaccard — catches \"Publish content: AI agents\" vs \"Publish content: tech\"\n\n/** Load rate-limit state from disk. */\nfunction loadRateState(): { creations: string[] } {\n if (!existsSync(RATE_PATH)) return { creations: [] };\n try {\n const raw = readFileSync(RATE_PATH, 'utf-8');\n const parsed = JSON.parse(raw) as { creations?: string[] };\n return { creations: Array.isArray(parsed?.creations) ? parsed.creations : [] };\n } catch {\n return { creations: [] };\n }\n}\n\nfunction saveRateState(state: { creations: string[] }): void {\n try {\n mkdirIfNotExists(TITAN_HOME);\n writeFileSync(RATE_PATH, JSON.stringify(state, null, 2), 'utf-8');\n } catch { /* non-critical */ }\n}\n\n/** Jaccard similarity for fuzzy dedupe. 0–1, higher = more similar. */\nfunction titleSimilarity(a: string, b: string): number {\n const tokenize = (s: string) => new Set(\n s.toLowerCase()\n .replace(/[^\\w\\s]/g, ' ')\n .split(/\\s+/)\n .filter(w => w.length > 2)\n );\n const ta = tokenize(a);\n const tb = tokenize(b);\n if (ta.size === 0 || tb.size === 0) return 0;\n let intersection = 0;\n for (const t of ta) if (tb.has(t)) intersection++;\n const union = ta.size + tb.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\n/** Returns true if we should block creation due to rate limits. */\nfunction isRateLimited(force: boolean): { limited: boolean; reason?: string } {\n if (force) return { limited: false };\n const state = loadRateState();\n const now = Date.now();\n const hourMs = 60 * 60 * 1000;\n const recent = state.creations.filter(t => now - new Date(t).getTime() < hourMs);\n if (recent.length >= MAX_GOALS_PER_HOUR) {\n return { limited: true, reason: `rate limit: ${recent.length} goals created in the last hour (max ${MAX_GOALS_PER_HOUR})` };\n }\n recent.push(new Date().toISOString());\n saveRateState({ creations: recent });\n return { limited: false };\n}\n\nexport type GoalStatus = 'active' | 'paused' | 'completed' | 'failed';\nexport type SubtaskStatus = 'pending' | 'running' | 'done' | 'failed' | 'skipped';\n\nexport interface SubtaskTrigger {\n type: 'schedule' | 'event' | 'dependency' | 'manual';\n /** Event name to match (e.g., 'health:ollama:down', 'cron:stuck') */\n event?: string;\n /** Cron expression for scheduled triggers */\n schedule?: string;\n /** LLM-evaluated condition (e.g., \"when error rate exceeds 50%\") */\n condition?: string;\n}\n\nexport interface Subtask {\n id: string;\n title: string;\n description: string;\n status: SubtaskStatus;\n result?: string;\n error?: string;\n completedAt?: string;\n retries: number;\n /** Subtask IDs within the same goal that must complete before this one can start */\n dependsOn?: string[];\n /** Optional trigger — when set, subtask activates on matching events instead of linearly */\n trigger?: SubtaskTrigger;\n}\n\nexport interface Goal {\n id: string;\n title: string;\n description: string;\n status: GoalStatus;\n priority: number; // 1 = highest\n subtasks: Subtask[];\n schedule?: string; // cron expression (e.g., \"0 9 * * 1,4\" for Mon+Thu 9am)\n budgetLimit?: number; // max USD spend for this goal\n totalCost: number;\n progress: number; // 0-100 percentage\n createdAt: string;\n updatedAt: string;\n completedAt?: string;\n tags?: string[];\n /** Parent goal ID for ancestry chain (Command Post) */\n parentGoalId?: string;\n}\n\ninterface GoalsStore {\n goals: Goal[];\n lastUpdated: string;\n}\n\n/** In-memory cache of goals */\nlet goalsCache: Goal[] | null = null;\n\n/** Load goals from disk */\nfunction loadGoals(): Goal[] {\n if (goalsCache) return goalsCache;\n\n if (!existsSync(GOALS_PATH)) {\n goalsCache = [];\n return goalsCache;\n }\n\n try {\n const raw = readFileSync(GOALS_PATH, 'utf-8');\n const store = JSON.parse(raw) as GoalsStore;\n goalsCache = store.goals || [];\n return goalsCache;\n } catch (err) {\n logger.warn(COMPONENT, `Failed to load goals: ${(err as Error).message}`);\n goalsCache = [];\n return goalsCache;\n }\n}\n\n/** Save goals to disk */\nfunction saveGoals(): void {\n const goals = goalsCache || [];\n try {\n mkdirIfNotExists(TITAN_HOME);\n const store: GoalsStore = {\n goals,\n lastUpdated: new Date().toISOString(),\n };\n writeFileSync(GOALS_PATH, JSON.stringify(store, null, 2), 'utf-8');\n } catch (err) {\n logger.error(COMPONENT, `Failed to save goals: ${(err as Error).message}`);\n }\n}\n\n/** Create a new goal */\nexport function createGoal(options: {\n title: string;\n description: string;\n priority?: number;\n schedule?: string;\n budgetLimit?: number;\n tags?: string[];\n parentGoalId?: string;\n subtasks?: Array<{ title: string; description: string; dependsOn?: string[] }>;\n /** Bypass rate limits and soft caps (human-initiated only). */\n force?: boolean;\n}): Goal {\n const goals = loadGoals();\n\n // ── v5.0.0: Multi-layer dedupe + runaway prevention ──────────────\n\n // 1. Exact title match against ACTIVE goals (existing v4.10 behavior)\n const existingActive = goals.find(g =>\n g.status === 'active' && g.title.trim() === options.title.trim()\n );\n if (existingActive) {\n logger.info(COMPONENT, `createGoal dedupe: \"${options.title}\" already active as ${existingActive.id} — returning existing`);\n return existingActive;\n }\n\n // 2. Fuzzy similarity match against ACTIVE goals (catches \"Publish content: X\" variants)\n const fuzzyDup = goals.find(g =>\n g.status === 'active' && titleSimilarity(g.title, options.title) >= SIMILARITY_THRESHOLD\n );\n if (fuzzyDup) {\n logger.info(COMPONENT, `createGoal fuzzy dedupe: \"${options.title}\" similar to active goal \"${fuzzyDup.title}\" (${fuzzyDup.id}) — returning existing`);\n return fuzzyDup;\n }\n\n // 3. Recent exact match against ANY status (prevents rapid re-creation of completed/failed goals)\n const cutoffMs = RECENT_DEDUPE_HOURS * 60 * 60 * 1000;\n const recentDup = goals.find(g => {\n if (g.title.trim() !== options.title.trim()) return false;\n const age = Date.now() - new Date(g.createdAt).getTime();\n return age < cutoffMs;\n });\n if (recentDup) {\n logger.info(COMPONENT, `createGoal recent dedupe: \"${options.title}\" created ${recentDup.id} within ${RECENT_DEDUPE_HOURS}h — returning existing`);\n return recentDup;\n }\n\n // 4. Hard caps\n const activeCount = goals.filter(g => g.status === 'active').length;\n if (!options.force && activeCount >= MAX_ACTIVE_GOALS) {\n logger.warn(COMPONENT, `createGoal blocked: ${activeCount} active goals >= cap ${MAX_ACTIVE_GOALS}. Use force=true to override.`);\n throw new Error(`Goal cap exceeded: ${activeCount} active goals (max ${MAX_ACTIVE_GOALS}). Close some goals first.`);\n }\n if (!options.force && goals.length >= MAX_TOTAL_GOALS) {\n logger.warn(COMPONENT, `createGoal blocked: ${goals.length} total goals >= cap ${MAX_TOTAL_GOALS}. Use force=true to override.`);\n throw new Error(`Goal cap exceeded: ${goals.length} total goals (max ${MAX_TOTAL_GOALS}). Close some goals first.`);\n }\n\n // 5. Rate limit\n const rateCheck = isRateLimited(!!options.force);\n if (rateCheck.limited) {\n logger.warn(COMPONENT, `createGoal blocked: ${rateCheck.reason}`);\n throw new Error(`Goal creation rate limited: ${rateCheck.reason}`);\n }\n\n const subtasks: Subtask[] = (options.subtasks || []).map((st, i) => ({\n id: `st-${i + 1}`,\n title: st.title,\n description: st.description,\n status: 'pending' as SubtaskStatus,\n retries: 0,\n dependsOn: st.dependsOn,\n }));\n\n // Validate no circular dependencies (DFS cycle check)\n if (subtasks.some(st => st.dependsOn?.length)) {\n const idSet = new Set(subtasks.map(st => st.id));\n const adjList = new Map<string, string[]>();\n for (const st of subtasks) {\n adjList.set(st.id, (st.dependsOn || []).filter(d => idSet.has(d)));\n }\n const visited = new Set<string>();\n const inStack = new Set<string>();\n const hasCycle = (node: string): boolean => {\n if (inStack.has(node)) return true;\n if (visited.has(node)) return false;\n visited.add(node);\n inStack.add(node);\n for (const dep of adjList.get(node) || []) {\n if (hasCycle(dep)) return true;\n }\n inStack.delete(node);\n return false;\n };\n for (const st of subtasks) {\n if (hasCycle(st.id)) {\n throw new Error(`Circular dependency detected in subtask ${st.id}`);\n }\n }\n }\n\n const goal: Goal = {\n id: uuid().slice(0, 8),\n title: options.title,\n description: options.description,\n status: 'active',\n priority: options.priority || Math.min(goals.length + 1, 99),\n subtasks,\n schedule: options.schedule,\n budgetLimit: options.budgetLimit,\n totalCost: 0,\n progress: 0,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n tags: options.tags,\n parentGoalId: options.parentGoalId,\n };\n\n goals.push(goal);\n goalsCache = goals;\n saveGoals();\n\n logger.info(COMPONENT, `Goal created: \"${goal.title}\" (${goal.id}) with ${goal.subtasks.length} subtasks`);\n titanEvents.emit('goal:created', { goalId: goal.id, title: goal.title, subtasks: goal.subtasks.length });\n return goal;\n}\n\n/** Get all goals, optionally filtered by status */\nexport function listGoals(status?: GoalStatus): Goal[] {\n const goals = loadGoals();\n if (status) return goals.filter(g => g.status === status);\n return goals;\n}\n\n/** Get a single goal by ID */\nexport function getGoal(goalId: string): Goal | undefined {\n return loadGoals().find(g => g.id === goalId);\n}\n\n/** Update a goal's properties */\nexport function updateGoal(goalId: string, updates: {\n title?: string;\n description?: string;\n status?: GoalStatus;\n priority?: number;\n progress?: number;\n schedule?: string;\n budgetLimit?: number;\n tags?: string[];\n}): Goal | undefined {\n const goals = loadGoals();\n const goal = goals.find(g => g.id === goalId);\n if (!goal) return undefined;\n\n if (updates.title !== undefined) goal.title = updates.title;\n if (updates.description !== undefined) goal.description = updates.description;\n if (updates.status !== undefined) goal.status = updates.status;\n if (updates.priority !== undefined) goal.priority = updates.priority;\n if (updates.progress !== undefined) goal.progress = updates.progress;\n if (updates.schedule !== undefined) goal.schedule = updates.schedule;\n if (updates.budgetLimit !== undefined) goal.budgetLimit = updates.budgetLimit;\n if (updates.tags !== undefined) goal.tags = updates.tags;\n\n if (updates.status === 'completed') {\n goal.completedAt = new Date().toISOString();\n goal.progress = 100;\n }\n\n goal.updatedAt = new Date().toISOString();\n goalsCache = goals;\n saveGoals();\n\n logger.info(COMPONENT, `Goal updated: \"${goal.title}\" (${goal.id})`);\n return goal;\n}\n\n/** Delete a goal */\nexport function deleteGoal(goalId: string): boolean {\n const goals = loadGoals();\n const idx = goals.findIndex(g => g.id === goalId);\n if (idx === -1) return false;\n\n goals.splice(idx, 1);\n goalsCache = goals;\n saveGoals();\n\n logger.info(COMPONENT, `Goal deleted: ${goalId}`);\n return true;\n}\n\n/** Add a subtask to a goal */\nexport function addSubtask(goalId: string, title: string, description: string): Subtask | undefined {\n const goal = getGoal(goalId);\n if (!goal) return undefined;\n\n const subtask: Subtask = {\n id: `st-${goal.subtasks.length + 1}`,\n title,\n description,\n status: 'pending',\n retries: 0,\n };\n\n goal.subtasks.push(subtask);\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n return subtask;\n}\n\n/** Complete a subtask and update goal progress */\nexport function completeSubtask(goalId: string, subtaskId: string, result: string): boolean {\n const goal = getGoal(goalId);\n if (!goal) return false;\n\n const subtask = goal.subtasks.find(st => st.id === subtaskId);\n if (!subtask) return false;\n\n subtask.status = 'done';\n subtask.result = result;\n subtask.completedAt = new Date().toISOString();\n\n // Recalculate progress\n const done = goal.subtasks.filter(st => st.status === 'done' || st.status === 'skipped').length;\n goal.progress = Math.round((done / goal.subtasks.length) * 100);\n\n // Auto-complete goal if all subtasks done\n if (goal.subtasks.every(st => st.status === 'done' || st.status === 'skipped')) {\n goal.status = 'completed';\n goal.completedAt = new Date().toISOString();\n logger.info(COMPONENT, `Goal auto-completed: \"${goal.title}\"`);\n titanEvents.emit('goal:completed', { goalId: goal.id, title: goal.title });\n } else {\n titanEvents.emit('goal:progress', { goalId: goal.id, title: goal.title, progress: goal.progress, subtaskId, result });\n }\n\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n return true;\n}\n\n/** Fail a subtask (with retry logic) */\nexport function failSubtask(goalId: string, subtaskId: string, error: string): boolean {\n const goal = getGoal(goalId);\n if (!goal) return false;\n\n const subtask = goal.subtasks.find(st => st.id === subtaskId);\n if (!subtask) return false;\n\n subtask.retries++;\n if (subtask.retries >= 3) {\n subtask.status = 'failed';\n subtask.error = error;\n titanEvents.emit('goal:failed', { goalId, subtaskId, title: subtask.title, error, retries: subtask.retries });\n } else {\n subtask.status = 'pending'; // Will retry\n }\n\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n return true;\n}\n\n/** Retry a subtask: reset to pending, clear error, zero retry counter.\n * v4.1: UI path for \"Retry\" button on failed subtasks in WorkflowsPanel. */\nexport function retrySubtask(goalId: string, subtaskId: string): boolean {\n const goal = getGoal(goalId);\n if (!goal) return false;\n const subtask = goal.subtasks.find(st => st.id === subtaskId);\n if (!subtask) return false;\n subtask.status = 'pending';\n subtask.error = undefined;\n subtask.retries = 0;\n subtask.completedAt = undefined;\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n return true;\n}\n\n/** Update a subtask's title and/or description. */\nexport function updateSubtask(\n goalId: string,\n subtaskId: string,\n updates: { title?: string; description?: string },\n): boolean {\n const goal = getGoal(goalId);\n if (!goal) return false;\n const subtask = goal.subtasks.find(st => st.id === subtaskId);\n if (!subtask) return false;\n if (typeof updates.title === 'string') subtask.title = updates.title;\n if (typeof updates.description === 'string') subtask.description = updates.description;\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n return true;\n}\n\n/** Get the next ready subtasks across all active goals (sorted by priority) */\nexport function getReadyTasks(): Array<{ goal: Goal; subtask: Subtask }> {\n const goals = loadGoals()\n .filter(g => g.status === 'active')\n .sort((a, b) => a.priority - b.priority);\n\n const ready: Array<{ goal: Goal; subtask: Subtask }> = [];\n\n for (const goal of goals) {\n // Check budget\n if (goal.budgetLimit && goal.totalCost >= goal.budgetLimit) continue;\n\n // Build a set of completed subtask IDs for dependency checking\n const completedIds = new Set(\n goal.subtasks.filter(st => st.status === 'done' || st.status === 'skipped').map(st => st.id)\n );\n\n for (const subtask of goal.subtasks) {\n if (subtask.status !== 'pending') continue;\n\n // Check all dependencies are satisfied\n const deps = subtask.dependsOn || [];\n const depsReady = deps.every(depId => completedIds.has(depId));\n if (depsReady) {\n ready.push({ goal, subtask });\n }\n }\n }\n\n return ready;\n}\n\n/** Record cost against a goal */\nexport function recordGoalCost(goalId: string, cost: number): void {\n const goal = getGoal(goalId);\n if (!goal) return;\n\n goal.totalCost += cost;\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n}\n\n/** Get a summary of all goals for reporting */\nexport function getGoalsSummary(): string {\n const goals = loadGoals();\n if (goals.length === 0) return 'No goals defined.';\n\n const lines: string[] = ['## Goals Summary', ''];\n\n for (const goal of goals.sort((a, b) => a.priority - b.priority)) {\n const icon = goal.status === 'completed' ? '✅' : goal.status === 'active' ? '🎯' : goal.status === 'paused' ? '⏸️' : '❌';\n const done = goal.subtasks.filter(st => st.status === 'done').length;\n lines.push(`${icon} **${goal.title}** [${goal.status}] — ${done}/${goal.subtasks.length} subtasks (${goal.progress}%)`);\n if (goal.schedule) lines.push(` Schedule: ${goal.schedule}`);\n if (goal.budgetLimit) lines.push(` Budget: $${goal.totalCost.toFixed(2)} / $${goal.budgetLimit.toFixed(2)}`);\n }\n\n return lines.join('\\n');\n}\n\n/** Check if any subtasks match a given event and mark them as ready */\nexport function matchEventTriggers(eventName: string): Array<{ goal: Goal; subtask: Subtask }> {\n const goals = loadGoals().filter(g => g.status === 'active');\n const matched: Array<{ goal: Goal; subtask: Subtask }> = [];\n\n for (const goal of goals) {\n for (const subtask of goal.subtasks) {\n if (subtask.status !== 'pending') continue;\n if (!subtask.trigger || subtask.trigger.type !== 'event') continue;\n if (!subtask.trigger.event) continue;\n\n // Match exact event name or wildcard prefix (e.g., 'health:*' matches 'health:ollama:down')\n const pattern = subtask.trigger.event;\n const matches = pattern.endsWith('*')\n ? eventName.startsWith(pattern.slice(0, -1))\n : eventName === pattern;\n\n if (matches) {\n matched.push({ goal, subtask });\n logger.info(COMPONENT, `Event trigger matched: \"${subtask.title}\" (goal: ${goal.title}) on event: ${eventName}`);\n }\n }\n }\n\n return matched;\n}\n\n/** Dynamically add a subtask after another completes (for adaptive goal planning) */\nexport function addDynamicSubtask(goalId: string, afterSubtaskId: string, title: string, description: string): Subtask | undefined {\n const goal = getGoal(goalId);\n if (!goal) return undefined;\n\n const maxSubtasks = 30; // Safety cap\n if (goal.subtasks.length >= maxSubtasks) {\n logger.warn(COMPONENT, `Cannot add dynamic subtask to \"${goal.title}\" — max ${maxSubtasks} reached`);\n return undefined;\n }\n\n const subtask: Subtask = {\n id: `st-dyn-${goal.subtasks.length + 1}`,\n title,\n description,\n status: 'pending',\n retries: 0,\n dependsOn: [afterSubtaskId],\n };\n\n goal.subtasks.push(subtask);\n goal.updatedAt = new Date().toISOString();\n\n // Recalculate progress with new subtask\n const done = goal.subtasks.filter(st => st.status === 'done' || st.status === 'skipped').length;\n goal.progress = Math.round((done / goal.subtasks.length) * 100);\n\n saveGoals();\n logger.info(COMPONENT, `Dynamic subtask added to \"${goal.title}\": \"${title}\" (depends on ${afterSubtaskId})`);\n titanEvents.emit('goal:subtask:added', { goalId, subtaskId: subtask.id, title, afterSubtaskId });\n return subtask;\n}\n\n/** Force reload from disk (useful after external edits) */\nexport function reloadGoals(): void {\n goalsCache = null;\n loadGoals();\n}\n\n/** v5.0.0: Bulk close duplicate goals. Keeps the newest active goal for each\n * exact title and marks the rest as failed. Returns counts for logging. */\nexport function dedupeGoalsBulk(): { scanned: number; closed: number; kept: number } {\n const goals = loadGoals();\n const seen = new Map<string, Goal>(); // title -> newest kept goal\n let closed = 0;\n\n // Sort by createdAt desc so we keep the newest\n const sorted = [...goals].sort((a, b) =>\n new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()\n );\n\n for (const g of sorted) {\n const key = g.title.trim().toLowerCase();\n if (!seen.has(key)) {\n seen.set(key, g);\n continue;\n }\n // Duplicate — close it if active\n if (g.status === 'active') {\n g.status = 'failed';\n g.updatedAt = new Date().toISOString();\n closed++;\n logger.info(COMPONENT, `Bulk dedupe closed duplicate goal \"${g.title}\" (${g.id})`);\n }\n }\n\n if (closed > 0) {\n goalsCache = goals;\n saveGoals();\n }\n return { scanned: goals.length, closed, kept: seen.size };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/agent/goals.ts"],"sourcesContent":["/**\n * TITAN — Goal Management System\n * Persistent goals with subtasks, scheduling, budget tracking, and progress monitoring.\n * Goals drive the autopilot system — each cycle picks the next actionable subtask.\n */\nimport { v4 as uuid } from 'uuid';\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { mkdirIfNotExists } from '../utils/helpers.js';\nimport { titanEvents } from './daemon.js';\nimport { createMtimeCache } from '../utils/mtimeCache.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Goals';\nconst GOALS_PATH = join(TITAN_HOME, 'goals.json');\nconst RATE_PATH = join(TITAN_HOME, 'goal-creation-rate.json');\n\n/** Safety limits — prevent runaway goal proliferation (SOMA or agent loops). */\nconst MAX_TOTAL_GOALS = 150;\nconst MAX_ACTIVE_GOALS = 50;\nconst MAX_GOALS_PER_HOUR = 10;\nconst RECENT_DEDUPE_HOURS = 24;\nconst SIMILARITY_THRESHOLD = 0.82; // Jaccard — catches \"Publish content: AI agents\" vs \"Publish content: tech\"\n\n/** Load rate-limit state from disk. */\nfunction loadRateState(): { creations: string[] } {\n if (!existsSync(RATE_PATH)) return { creations: [] };\n try {\n const raw = readFileSync(RATE_PATH, 'utf-8');\n const parsed = JSON.parse(raw) as { creations?: string[] };\n return { creations: Array.isArray(parsed?.creations) ? parsed.creations : [] };\n } catch {\n return { creations: [] };\n }\n}\n\nfunction saveRateState(state: { creations: string[] }): void {\n try {\n mkdirIfNotExists(TITAN_HOME);\n writeFileSync(RATE_PATH, JSON.stringify(state, null, 2), 'utf-8');\n } catch { /* non-critical */ }\n}\n\n/** Jaccard similarity for fuzzy dedupe. 0–1, higher = more similar. */\nfunction titleSimilarity(a: string, b: string): number {\n const tokenize = (s: string) => new Set(\n s.toLowerCase()\n .replace(/[^\\w\\s]/g, ' ')\n .split(/\\s+/)\n .filter(w => w.length > 2)\n );\n const ta = tokenize(a);\n const tb = tokenize(b);\n if (ta.size === 0 || tb.size === 0) return 0;\n let intersection = 0;\n for (const t of ta) if (tb.has(t)) intersection++;\n const union = ta.size + tb.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\n/** Returns true if we should block creation due to rate limits. */\nfunction isRateLimited(force: boolean): { limited: boolean; reason?: string } {\n if (force) return { limited: false };\n const state = loadRateState();\n const now = Date.now();\n const hourMs = 60 * 60 * 1000;\n const recent = state.creations.filter(t => now - new Date(t).getTime() < hourMs);\n if (recent.length >= MAX_GOALS_PER_HOUR) {\n return { limited: true, reason: `rate limit: ${recent.length} goals created in the last hour (max ${MAX_GOALS_PER_HOUR})` };\n }\n recent.push(new Date().toISOString());\n saveRateState({ creations: recent });\n return { limited: false };\n}\n\nexport type GoalStatus = 'active' | 'paused' | 'completed' | 'failed';\nexport type SubtaskStatus = 'pending' | 'running' | 'done' | 'failed' | 'skipped';\n\nexport interface SubtaskTrigger {\n type: 'schedule' | 'event' | 'dependency' | 'manual';\n /** Event name to match (e.g., 'health:ollama:down', 'cron:stuck') */\n event?: string;\n /** Cron expression for scheduled triggers */\n schedule?: string;\n /** LLM-evaluated condition (e.g., \"when error rate exceeds 50%\") */\n condition?: string;\n}\n\nexport interface Subtask {\n id: string;\n title: string;\n description: string;\n status: SubtaskStatus;\n result?: string;\n error?: string;\n completedAt?: string;\n retries: number;\n /** Subtask IDs within the same goal that must complete before this one can start */\n dependsOn?: string[];\n /** Optional trigger — when set, subtask activates on matching events instead of linearly */\n trigger?: SubtaskTrigger;\n}\n\nexport interface Goal {\n id: string;\n title: string;\n description: string;\n status: GoalStatus;\n priority: number; // 1 = highest\n subtasks: Subtask[];\n schedule?: string; // cron expression (e.g., \"0 9 * * 1,4\" for Mon+Thu 9am)\n budgetLimit?: number; // max USD spend for this goal\n totalCost: number;\n progress: number; // 0-100 percentage\n createdAt: string;\n updatedAt: string;\n completedAt?: string;\n tags?: string[];\n /** Parent goal ID for ancestry chain (Command Post) */\n parentGoalId?: string;\n}\n\ninterface GoalsStore {\n goals: Goal[];\n lastUpdated: string;\n}\n\n/**\n * v5.5.11: mtime-validated cache. Previously this was a bare module-scope\n * variable that never reloaded from disk — so external writes (admin script,\n * agent file-write tool, autopilot, manual edit) were invisible to the live\n * gateway until restart. mtimeCache stat()s the file on every read; if the\n * mtime has advanced since the last load, it reloads from disk before\n * returning. Cost: <0.1ms per read in exchange for correctness.\n */\nconst goalsCache = createMtimeCache<Goal[]>({\n path: GOALS_PATH,\n parse: (raw) => {\n const store = JSON.parse(raw) as GoalsStore;\n return store.goals || [];\n },\n initial: () => [],\n component: COMPONENT,\n});\n\n/** Load goals from disk (cached + mtime-checked) */\nfunction loadGoals(): Goal[] {\n return goalsCache.read();\n}\n\n/** Save goals to disk and update the cache so the next read sees the new state. */\nfunction saveGoals(goalsOverride?: Goal[]): void {\n const goals = goalsOverride ?? goalsCache.read();\n try {\n mkdirIfNotExists(TITAN_HOME);\n const store: GoalsStore = {\n goals,\n lastUpdated: new Date().toISOString(),\n };\n writeFileSync(GOALS_PATH, JSON.stringify(store, null, 2), 'utf-8');\n goalsCache.set(goals, 'saveGoals');\n } catch (err) {\n logger.error(COMPONENT, `Failed to save goals: ${(err as Error).message}`);\n }\n}\n\n/** Create a new goal */\nexport function createGoal(options: {\n title: string;\n description: string;\n priority?: number;\n schedule?: string;\n budgetLimit?: number;\n tags?: string[];\n parentGoalId?: string;\n subtasks?: Array<{ title: string; description: string; dependsOn?: string[] }>;\n /** Bypass rate limits and soft caps (human-initiated only). */\n force?: boolean;\n}): Goal {\n const goals = loadGoals();\n\n // ── v5.0.0: Multi-layer dedupe + runaway prevention ──────────────\n\n // 1. Exact title match against ACTIVE goals (existing v4.10 behavior)\n const existingActive = goals.find(g =>\n g.status === 'active' && g.title.trim() === options.title.trim()\n );\n if (existingActive) {\n logger.info(COMPONENT, `createGoal dedupe: \"${options.title}\" already active as ${existingActive.id} — returning existing`);\n return existingActive;\n }\n\n // 2. Fuzzy similarity match against ACTIVE goals (catches \"Publish content: X\" variants)\n const fuzzyDup = goals.find(g =>\n g.status === 'active' && titleSimilarity(g.title, options.title) >= SIMILARITY_THRESHOLD\n );\n if (fuzzyDup) {\n logger.info(COMPONENT, `createGoal fuzzy dedupe: \"${options.title}\" similar to active goal \"${fuzzyDup.title}\" (${fuzzyDup.id}) — returning existing`);\n return fuzzyDup;\n }\n\n // 3. Recent exact match against ANY status (prevents rapid re-creation of completed/failed goals)\n const cutoffMs = RECENT_DEDUPE_HOURS * 60 * 60 * 1000;\n const recentDup = goals.find(g => {\n if (g.title.trim() !== options.title.trim()) return false;\n const age = Date.now() - new Date(g.createdAt).getTime();\n return age < cutoffMs;\n });\n if (recentDup) {\n logger.info(COMPONENT, `createGoal recent dedupe: \"${options.title}\" created ${recentDup.id} within ${RECENT_DEDUPE_HOURS}h — returning existing`);\n return recentDup;\n }\n\n // 4. Hard caps\n const activeCount = goals.filter(g => g.status === 'active').length;\n if (!options.force && activeCount >= MAX_ACTIVE_GOALS) {\n logger.warn(COMPONENT, `createGoal blocked: ${activeCount} active goals >= cap ${MAX_ACTIVE_GOALS}. Use force=true to override.`);\n throw new Error(`Goal cap exceeded: ${activeCount} active goals (max ${MAX_ACTIVE_GOALS}). Close some goals first.`);\n }\n if (!options.force && goals.length >= MAX_TOTAL_GOALS) {\n logger.warn(COMPONENT, `createGoal blocked: ${goals.length} total goals >= cap ${MAX_TOTAL_GOALS}. Use force=true to override.`);\n throw new Error(`Goal cap exceeded: ${goals.length} total goals (max ${MAX_TOTAL_GOALS}). Close some goals first.`);\n }\n\n // 5. Rate limit\n const rateCheck = isRateLimited(!!options.force);\n if (rateCheck.limited) {\n logger.warn(COMPONENT, `createGoal blocked: ${rateCheck.reason}`);\n throw new Error(`Goal creation rate limited: ${rateCheck.reason}`);\n }\n\n const subtasks: Subtask[] = (options.subtasks || []).map((st, i) => ({\n id: `st-${i + 1}`,\n title: st.title,\n description: st.description,\n status: 'pending' as SubtaskStatus,\n retries: 0,\n dependsOn: st.dependsOn,\n }));\n\n // Validate no circular dependencies (DFS cycle check)\n if (subtasks.some(st => st.dependsOn?.length)) {\n const idSet = new Set(subtasks.map(st => st.id));\n const adjList = new Map<string, string[]>();\n for (const st of subtasks) {\n adjList.set(st.id, (st.dependsOn || []).filter(d => idSet.has(d)));\n }\n const visited = new Set<string>();\n const inStack = new Set<string>();\n const hasCycle = (node: string): boolean => {\n if (inStack.has(node)) return true;\n if (visited.has(node)) return false;\n visited.add(node);\n inStack.add(node);\n for (const dep of adjList.get(node) || []) {\n if (hasCycle(dep)) return true;\n }\n inStack.delete(node);\n return false;\n };\n for (const st of subtasks) {\n if (hasCycle(st.id)) {\n throw new Error(`Circular dependency detected in subtask ${st.id}`);\n }\n }\n }\n\n const goal: Goal = {\n id: uuid().slice(0, 8),\n title: options.title,\n description: options.description,\n status: 'active',\n priority: options.priority || Math.min(goals.length + 1, 99),\n subtasks,\n schedule: options.schedule,\n budgetLimit: options.budgetLimit,\n totalCost: 0,\n progress: 0,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n tags: options.tags,\n parentGoalId: options.parentGoalId,\n };\n\n goals.push(goal);\n saveGoals(goals);\n\n logger.info(COMPONENT, `Goal created: \"${goal.title}\" (${goal.id}) with ${goal.subtasks.length} subtasks`);\n titanEvents.emit('goal:created', { goalId: goal.id, title: goal.title, subtasks: goal.subtasks.length });\n return goal;\n}\n\n/** Get all goals, optionally filtered by status */\nexport function listGoals(status?: GoalStatus): Goal[] {\n const goals = loadGoals();\n if (status) return goals.filter(g => g.status === status);\n return goals;\n}\n\n/** Get a single goal by ID */\nexport function getGoal(goalId: string): Goal | undefined {\n return loadGoals().find(g => g.id === goalId);\n}\n\n/** Update a goal's properties */\nexport function updateGoal(goalId: string, updates: {\n title?: string;\n description?: string;\n status?: GoalStatus;\n priority?: number;\n progress?: number;\n schedule?: string;\n budgetLimit?: number;\n tags?: string[];\n}): Goal | undefined {\n const goals = loadGoals();\n const goal = goals.find(g => g.id === goalId);\n if (!goal) return undefined;\n\n if (updates.title !== undefined) goal.title = updates.title;\n if (updates.description !== undefined) goal.description = updates.description;\n if (updates.status !== undefined) goal.status = updates.status;\n if (updates.priority !== undefined) goal.priority = updates.priority;\n if (updates.progress !== undefined) goal.progress = updates.progress;\n if (updates.schedule !== undefined) goal.schedule = updates.schedule;\n if (updates.budgetLimit !== undefined) goal.budgetLimit = updates.budgetLimit;\n if (updates.tags !== undefined) goal.tags = updates.tags;\n\n if (updates.status === 'completed') {\n goal.completedAt = new Date().toISOString();\n goal.progress = 100;\n }\n\n goal.updatedAt = new Date().toISOString();\n saveGoals(goals);\n\n logger.info(COMPONENT, `Goal updated: \"${goal.title}\" (${goal.id})`);\n return goal;\n}\n\n/** Delete a goal */\nexport function deleteGoal(goalId: string): boolean {\n const goals = loadGoals();\n const idx = goals.findIndex(g => g.id === goalId);\n if (idx === -1) return false;\n\n goals.splice(idx, 1);\n saveGoals(goals);\n\n logger.info(COMPONENT, `Goal deleted: ${goalId}`);\n return true;\n}\n\n/** Add a subtask to a goal */\nexport function addSubtask(goalId: string, title: string, description: string): Subtask | undefined {\n const goal = getGoal(goalId);\n if (!goal) return undefined;\n\n const subtask: Subtask = {\n id: `st-${goal.subtasks.length + 1}`,\n title,\n description,\n status: 'pending',\n retries: 0,\n };\n\n goal.subtasks.push(subtask);\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n return subtask;\n}\n\n/** Complete a subtask and update goal progress */\nexport function completeSubtask(goalId: string, subtaskId: string, result: string): boolean {\n const goal = getGoal(goalId);\n if (!goal) return false;\n\n const subtask = goal.subtasks.find(st => st.id === subtaskId);\n if (!subtask) return false;\n\n subtask.status = 'done';\n subtask.result = result;\n subtask.completedAt = new Date().toISOString();\n\n // Recalculate progress\n const done = goal.subtasks.filter(st => st.status === 'done' || st.status === 'skipped').length;\n goal.progress = Math.round((done / goal.subtasks.length) * 100);\n\n // Auto-complete goal if all subtasks done\n if (goal.subtasks.every(st => st.status === 'done' || st.status === 'skipped')) {\n goal.status = 'completed';\n goal.completedAt = new Date().toISOString();\n logger.info(COMPONENT, `Goal auto-completed: \"${goal.title}\"`);\n titanEvents.emit('goal:completed', { goalId: goal.id, title: goal.title });\n } else {\n titanEvents.emit('goal:progress', { goalId: goal.id, title: goal.title, progress: goal.progress, subtaskId, result });\n }\n\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n return true;\n}\n\n/** Fail a subtask (with retry logic) */\nexport function failSubtask(goalId: string, subtaskId: string, error: string): boolean {\n const goal = getGoal(goalId);\n if (!goal) return false;\n\n const subtask = goal.subtasks.find(st => st.id === subtaskId);\n if (!subtask) return false;\n\n subtask.retries++;\n if (subtask.retries >= 3) {\n subtask.status = 'failed';\n subtask.error = error;\n titanEvents.emit('goal:failed', { goalId, subtaskId, title: subtask.title, error, retries: subtask.retries });\n } else {\n subtask.status = 'pending'; // Will retry\n }\n\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n return true;\n}\n\n/** Retry a subtask: reset to pending, clear error, zero retry counter.\n * v4.1: UI path for \"Retry\" button on failed subtasks in WorkflowsPanel. */\nexport function retrySubtask(goalId: string, subtaskId: string): boolean {\n const goal = getGoal(goalId);\n if (!goal) return false;\n const subtask = goal.subtasks.find(st => st.id === subtaskId);\n if (!subtask) return false;\n subtask.status = 'pending';\n subtask.error = undefined;\n subtask.retries = 0;\n subtask.completedAt = undefined;\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n return true;\n}\n\n/** Update a subtask's title and/or description. */\nexport function updateSubtask(\n goalId: string,\n subtaskId: string,\n updates: { title?: string; description?: string },\n): boolean {\n const goal = getGoal(goalId);\n if (!goal) return false;\n const subtask = goal.subtasks.find(st => st.id === subtaskId);\n if (!subtask) return false;\n if (typeof updates.title === 'string') subtask.title = updates.title;\n if (typeof updates.description === 'string') subtask.description = updates.description;\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n return true;\n}\n\n/** Get the next ready subtasks across all active goals (sorted by priority) */\nexport function getReadyTasks(): Array<{ goal: Goal; subtask: Subtask }> {\n const goals = loadGoals()\n .filter(g => g.status === 'active')\n .sort((a, b) => a.priority - b.priority);\n\n const ready: Array<{ goal: Goal; subtask: Subtask }> = [];\n\n for (const goal of goals) {\n // Check budget\n if (goal.budgetLimit && goal.totalCost >= goal.budgetLimit) continue;\n\n // Build a set of completed subtask IDs for dependency checking\n const completedIds = new Set(\n goal.subtasks.filter(st => st.status === 'done' || st.status === 'skipped').map(st => st.id)\n );\n\n for (const subtask of goal.subtasks) {\n if (subtask.status !== 'pending') continue;\n\n // Check all dependencies are satisfied\n const deps = subtask.dependsOn || [];\n const depsReady = deps.every(depId => completedIds.has(depId));\n if (depsReady) {\n ready.push({ goal, subtask });\n }\n }\n }\n\n return ready;\n}\n\n/** Record cost against a goal */\nexport function recordGoalCost(goalId: string, cost: number): void {\n const goal = getGoal(goalId);\n if (!goal) return;\n\n goal.totalCost += cost;\n goal.updatedAt = new Date().toISOString();\n saveGoals();\n}\n\n/** Get a summary of all goals for reporting */\nexport function getGoalsSummary(): string {\n const goals = loadGoals();\n if (goals.length === 0) return 'No goals defined.';\n\n const lines: string[] = ['## Goals Summary', ''];\n\n for (const goal of goals.sort((a, b) => a.priority - b.priority)) {\n const icon = goal.status === 'completed' ? '✅' : goal.status === 'active' ? '🎯' : goal.status === 'paused' ? '⏸️' : '❌';\n const done = goal.subtasks.filter(st => st.status === 'done').length;\n lines.push(`${icon} **${goal.title}** [${goal.status}] — ${done}/${goal.subtasks.length} subtasks (${goal.progress}%)`);\n if (goal.schedule) lines.push(` Schedule: ${goal.schedule}`);\n if (goal.budgetLimit) lines.push(` Budget: $${goal.totalCost.toFixed(2)} / $${goal.budgetLimit.toFixed(2)}`);\n }\n\n return lines.join('\\n');\n}\n\n/** Check if any subtasks match a given event and mark them as ready */\nexport function matchEventTriggers(eventName: string): Array<{ goal: Goal; subtask: Subtask }> {\n const goals = loadGoals().filter(g => g.status === 'active');\n const matched: Array<{ goal: Goal; subtask: Subtask }> = [];\n\n for (const goal of goals) {\n for (const subtask of goal.subtasks) {\n if (subtask.status !== 'pending') continue;\n if (!subtask.trigger || subtask.trigger.type !== 'event') continue;\n if (!subtask.trigger.event) continue;\n\n // Match exact event name or wildcard prefix (e.g., 'health:*' matches 'health:ollama:down')\n const pattern = subtask.trigger.event;\n const matches = pattern.endsWith('*')\n ? eventName.startsWith(pattern.slice(0, -1))\n : eventName === pattern;\n\n if (matches) {\n matched.push({ goal, subtask });\n logger.info(COMPONENT, `Event trigger matched: \"${subtask.title}\" (goal: ${goal.title}) on event: ${eventName}`);\n }\n }\n }\n\n return matched;\n}\n\n/** Dynamically add a subtask after another completes (for adaptive goal planning) */\nexport function addDynamicSubtask(goalId: string, afterSubtaskId: string, title: string, description: string): Subtask | undefined {\n const goal = getGoal(goalId);\n if (!goal) return undefined;\n\n const maxSubtasks = 30; // Safety cap\n if (goal.subtasks.length >= maxSubtasks) {\n logger.warn(COMPONENT, `Cannot add dynamic subtask to \"${goal.title}\" — max ${maxSubtasks} reached`);\n return undefined;\n }\n\n const subtask: Subtask = {\n id: `st-dyn-${goal.subtasks.length + 1}`,\n title,\n description,\n status: 'pending',\n retries: 0,\n dependsOn: [afterSubtaskId],\n };\n\n goal.subtasks.push(subtask);\n goal.updatedAt = new Date().toISOString();\n\n // Recalculate progress with new subtask\n const done = goal.subtasks.filter(st => st.status === 'done' || st.status === 'skipped').length;\n goal.progress = Math.round((done / goal.subtasks.length) * 100);\n\n saveGoals();\n logger.info(COMPONENT, `Dynamic subtask added to \"${goal.title}\": \"${title}\" (depends on ${afterSubtaskId})`);\n titanEvents.emit('goal:subtask:added', { goalId, subtaskId: subtask.id, title, afterSubtaskId });\n return subtask;\n}\n\n/**\n * Force reload from disk.\n *\n * v5.5.11: largely obsolete since the cache now mtime-checks every read,\n * but kept for backward compatibility and for callers that want an explicit\n * \"throw away in-memory state\" signal (e.g. test setup).\n */\nexport function reloadGoals(): void {\n goalsCache.invalidate();\n loadGoals();\n}\n\n/** v5.0.0: Bulk close duplicate goals. Keeps the newest active goal for each\n * exact title and marks the rest as failed. Returns counts for logging. */\nexport function dedupeGoalsBulk(): { scanned: number; closed: number; kept: number } {\n const goals = loadGoals();\n const seen = new Map<string, Goal>(); // title -> newest kept goal\n let closed = 0;\n\n // Sort by createdAt desc so we keep the newest\n const sorted = [...goals].sort((a, b) =>\n new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()\n );\n\n for (const g of sorted) {\n const key = g.title.trim().toLowerCase();\n if (!seen.has(key)) {\n seen.set(key, g);\n continue;\n }\n // Duplicate — close it if active\n if (g.status === 'active') {\n g.status = 'failed';\n g.updatedAt = new Date().toISOString();\n closed++;\n logger.info(COMPONENT, `Bulk dedupe closed duplicate goal \"${g.title}\" (${g.id})`);\n }\n }\n\n if (closed > 0) {\n saveGoals(goals);\n }\n return { scanned: goals.length, closed, kept: seen.size };\n}\n"],"mappings":"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","names":[]}
@@ -237,9 +237,15 @@ ${template.source || ""}`;
237
237
  const health = await healthCheckAll();
238
238
  res.json(health);
239
239
  });
240
- router.get("/health", (_req, res) => {
240
+ router.get("/health", async (_req, res) => {
241
241
  const cfg = loadConfig();
242
- res.json({ status: "ok", version: TITAN_VERSION, uptime: process.uptime(), onboarded: cfg.onboarded });
242
+ let restarts = null;
243
+ try {
244
+ const { getRestartStats } = await import("../../utils/restartTracker.js");
245
+ restarts = getRestartStats();
246
+ } catch {
247
+ }
248
+ res.json({ status: "ok", version: TITAN_VERSION, uptime: process.uptime(), onboarded: cfg.onboarded, restarts });
243
249
  });
244
250
  router.get("/bug-reports", async (req, res) => {
245
251
  try {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/gateway/routes/skills.ts"],"sourcesContent":["/**\n * Skills & Meta Router\n *\n * Extracted from gateway/server.ts.\n * Consolidates all /api/skills, /api/specialists, /api/marketplace,\n * /api/personas, /api/widget-gallery, /api/tools, /api/channels,\n * /api/security, /api/providers, /api/health, /api/bug-reports,\n * and /api/sandbox routes.\n */\n\nimport { Router, type Request, type Response } from 'express';\nimport logger from '../../utils/logger.js';\nimport { loadConfig, updateConfig } from '../../config/config.js';\nimport { getSkills, toggleSkill, getSkillTools } from '../../skills/registry.js';\nimport { listPersonas, getPersona, invalidatePersonaCache } from '../../personas/manager.js';\nimport {\n searchSkills as marketplaceSearch,\n installSkill,\n uninstallSkill,\n listSkills as listMarketplaceSkills,\n listInstalled as listInstalledMarketplace,\n} from '../../skills/marketplace.js';\nimport { getRegisteredTools } from '../../agent/toolRunner.js';\nimport { auditSecurity } from '../../security/sandbox.js';\nimport { healthCheckAll } from '../../providers/router.js';\nimport { TITAN_VERSION } from '../../utils/constants.js';\nimport type { ChannelAdapter } from '../../channels/base.js';\n\nconst COMPONENT = 'SkillsRouter';\n\nexport function createSkillsRouter(channels: Map<string, ChannelAdapter>): Router {\n const router = Router();\n\n // ── Skills ──────────────────────────────────────────────────\n router.get('/skills', (_req, res) => {\n const skills = getSkills();\n res.json(skills);\n });\n\n router.post('/skills/:name/toggle', (req, res) => {\n try {\n const { name } = req.params;\n const enabled = toggleSkill(name);\n const tools = getSkillTools(name);\n res.json({ ok: true, skill: name, enabled, tools });\n } catch (e) {\n res.status(404).json({ error: (e as Error).message });\n }\n });\n\n // ── Specialists ─────────────────────────────────────────────\n router.get('/specialists', async (_req, res) => {\n try {\n const { SPECIALISTS } = await import('../../agent/specialists.js');\n const cfg = loadConfig();\n const overrides = (cfg as unknown as { specialists?: { overrides?: Record<string, { model?: string }> } }).specialists?.overrides || {};\n const out = SPECIALISTS.map((s) => ({\n id: s.id,\n name: s.name,\n role: s.role,\n title: s.title,\n defaultModel: s.model,\n activeModel: overrides[s.id]?.model || s.model,\n overridden: Boolean(overrides[s.id]?.model && overrides[s.id].model !== s.model),\n templateMatches: s.templateMatches,\n reportsTo: s.reportsTo,\n }));\n res.json(out);\n } catch (e) {\n logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' });\n }\n });\n\n router.patch('/specialists/:id', async (req, res) => {\n try {\n const { id } = req.params;\n const { model } = (req.body || {}) as { model?: string | null };\n const { SPECIALISTS } = await import('../../agent/specialists.js');\n const specialist = SPECIALISTS.find((s) => s.id === id);\n if (!specialist) { res.status(404).json({ error: `Unknown specialist: ${id}` }); return; }\n const cfg = loadConfig();\n const cfgAny = cfg as unknown as { specialists?: { overrides?: Record<string, { model?: string }> } };\n const overrides = { ...(cfgAny.specialists?.overrides || {}) };\n if (model === null || model === '' || model === undefined) {\n delete overrides[id];\n } else if (typeof model === 'string') {\n overrides[id] = { model };\n } else {\n res.status(400).json({ error: 'model must be a string or null' });\n return;\n }\n updateConfig({ specialists: { overrides } } as unknown as Parameters<typeof updateConfig>[0]);\n res.json({ ok: true, id, activeModel: overrides[id]?.model || specialist.model });\n } catch (e) {\n logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' });\n }\n });\n\n // ── Marketplace ─────────────────────────────────────────────\n router.get('/marketplace', async (_req, res) => {\n try {\n const skills = await listMarketplaceSkills();\n const installed = listInstalledMarketplace();\n res.json({ skills: skills.map(s => ({ ...s, installed: installed.includes(s.file.replace('.js', '')) })), installed });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n router.get('/marketplace/search', async (req, res) => {\n try {\n const q = (req.query.q as string) || '';\n const results = await marketplaceSearch(q, 50);\n const installed = listInstalledMarketplace();\n res.json({ ...results, skills: results.skills.map(s => ({ ...s, installed: installed.includes(s.file.replace('.js', '')) })) });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n router.post('/marketplace/install', async (req, res): Promise<void> => {\n try {\n const { skill } = req.body as { skill: string };\n if (!skill) { res.status(400).json({ error: 'Missing \"skill\" field' }); return; }\n const result = await installSkill(skill);\n res.json(result);\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n router.post('/marketplace/uninstall', (req, res): void => {\n try {\n const { skill } = req.body as { skill: string };\n if (!skill) { res.status(400).json({ error: 'Missing \"skill\" field' }); return; }\n const result = uninstallSkill(skill);\n res.json(result);\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n // ── Personas ──────────────────────────────────────────────────\n router.get('/personas', (_req, res) => {\n try {\n const cfg = loadConfig();\n res.json({ personas: listPersonas(), active: cfg.agent.persona || 'default' });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n router.post('/persona/switch', (req, res): void => {\n try {\n const { persona } = req.body as { persona: string };\n if (!persona || typeof persona !== 'string') { res.status(400).json({ error: 'Missing persona ID' }); return; }\n if (persona !== 'default' && !getPersona(persona)) { res.status(404).json({ error: `Persona \"${persona}\" not found` }); return; }\n const cfg = loadConfig();\n updateConfig({ agent: { ...cfg.agent, persona } });\n invalidatePersonaCache();\n res.json({ ok: true, active: persona });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n // ── Widget Gallery ──────────────────────────────────────────\n router.get('/widget-gallery', async (_req, res) => {\n try {\n const { listTemplates, listCategories } = await import('../../skills/builtin/widget_gallery.js');\n const templates = listTemplates();\n const categories = listCategories();\n res.json({ count: templates.length, categories, templates });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n router.get('/widget-gallery/:id', async (req, res): Promise<void> => {\n try {\n const { getTemplate } = await import('../../skills/builtin/widget_gallery.js');\n const template = getTemplate(String(req.params.id || ''));\n if (!template) {\n res.status(404).json({ error: `Template not found: ${req.params.id}` });\n return;\n }\n\n const w = template.defaultSize?.w ?? 4;\n const h = template.defaultSize?.h ?? 4;\n const source = template.source.startsWith('system:')\n ? template.source\n : `// __WIDGET_META__ w=${w} h=${h}\\n${template.source || ''}`;\n\n res.json({\n id: template.id,\n name: template.name,\n category: template.category,\n defaultSize: template.defaultSize,\n source,\n placeholders: template.placeholders ?? [],\n });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n // ── Tools ───────────────────────────────────────────────────\n router.get('/tools', (req, res) => {\n const includeSchema = req.query.include === 'schema';\n const q = typeof req.query.q === 'string' ? req.query.q.toLowerCase() : '';\n const limit = Math.min(parseInt(req.query.limit as string, 10) || 100, 1000);\n const offset = Math.max(parseInt(req.query.offset as string, 10) || 0, 0);\n\n let tools = getRegisteredTools().map((t) => {\n const item: Record<string, unknown> = {\n name: t.name,\n description: t.description,\n };\n if (includeSchema) {\n item.parameters = t.parameters;\n }\n return item;\n });\n\n if (q) {\n tools = tools.filter((t) =>\n (t.name as string).toLowerCase().includes(q) ||\n (t.description as string).toLowerCase().includes(q),\n );\n }\n\n const total = tools.length;\n const paginated = tools.slice(offset, offset + limit);\n\n res.json({\n total,\n count: paginated.length,\n offset,\n tools: paginated,\n });\n });\n\n // ── Channels ──────────────────────────────────────────────\n router.get('/channels', (_req, res) => {\n const statuses = Array.from(channels.values()).map((ch) => ch.getStatus());\n res.json(statuses);\n });\n\n // ── Security ────────────────────────────────────────────────\n router.get('/security', (_req, res) => {\n const audit = auditSecurity();\n res.json(audit);\n });\n\n // ── Providers ─────────────────────────────────────────────\n router.get('/providers', async (_req, res) => {\n const health = await healthCheckAll();\n res.json(health);\n });\n\n // ── Health ──────────────────────────────────────────────────\n router.get('/health', (_req, res) => {\n const cfg = loadConfig();\n res.json({ status: 'ok', version: TITAN_VERSION, uptime: process.uptime(), onboarded: cfg.onboarded });\n });\n\n // ── Bug Reports ─────────────────────────────────────────────\n router.get('/bug-reports', async (req, res) => {\n try {\n const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '50'), 10) || 50, 1), 200);\n const { listRecentBugReports } = await import('../../analytics/bugReports.js');\n const reports = listRecentBugReports(limit);\n res.json({ count: reports.length, reports });\n } catch (err) {\n res.status(500).json({ error: 'bug_reports_unavailable', message: (err as Error).message });\n }\n });\n\n router.get('/bug-reports/:id', async (req, res) => {\n try {\n const { getBugReport } = await import('../../analytics/bugReports.js');\n const r = getBugReport(req.params.id);\n if (!r) {\n res.status(404).json({ error: 'not_found' });\n return;\n }\n res.json(r);\n } catch (err) {\n res.status(500).json({ error: 'bug_report_unavailable', message: (err as Error).message });\n }\n });\n\n // ── Sandbox ─────────────────────────────────────────────────\n router.post('/sandbox/execute', async (req, res) => {\n try {\n const { code, language, timeoutMs } = req.body;\n if (!code || typeof code !== 'string') {\n res.status(400).json({ error: 'code is required' });\n return;\n }\n const { executeInSandbox } = await import('../../agent/sandbox.js');\n const result = await executeInSandbox(code, language || 'javascript', timeoutMs || 60000);\n res.json(result);\n } catch (err) {\n logger.error(COMPONENT, `Sandbox execute error: ${(err as Error).message}`);\n res.status(500).json({ error: 'Sandbox execution failed', message: (err as Error).message });\n }\n });\n\n router.get('/sandbox/status', async (_req, res) => {\n try {\n const { getSandboxStatus } = await import('../../agent/sandbox.js');\n res.json(getSandboxStatus());\n } catch (err) {\n res.status(500).json({ error: 'Sandbox status unavailable', message: (err as Error).message });\n }\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/gateway/routes/skills.ts"],"sourcesContent":["/**\n * Skills & Meta Router\n *\n * Extracted from gateway/server.ts.\n * Consolidates all /api/skills, /api/specialists, /api/marketplace,\n * /api/personas, /api/widget-gallery, /api/tools, /api/channels,\n * /api/security, /api/providers, /api/health, /api/bug-reports,\n * and /api/sandbox routes.\n */\n\nimport { Router, type Request, type Response } from 'express';\nimport logger from '../../utils/logger.js';\nimport { loadConfig, updateConfig } from '../../config/config.js';\nimport { getSkills, toggleSkill, getSkillTools } from '../../skills/registry.js';\nimport { listPersonas, getPersona, invalidatePersonaCache } from '../../personas/manager.js';\nimport {\n searchSkills as marketplaceSearch,\n installSkill,\n uninstallSkill,\n listSkills as listMarketplaceSkills,\n listInstalled as listInstalledMarketplace,\n} from '../../skills/marketplace.js';\nimport { getRegisteredTools } from '../../agent/toolRunner.js';\nimport { auditSecurity } from '../../security/sandbox.js';\nimport { healthCheckAll } from '../../providers/router.js';\nimport { TITAN_VERSION } from '../../utils/constants.js';\nimport type { ChannelAdapter } from '../../channels/base.js';\n\nconst COMPONENT = 'SkillsRouter';\n\nexport function createSkillsRouter(channels: Map<string, ChannelAdapter>): Router {\n const router = Router();\n\n // ── Skills ──────────────────────────────────────────────────\n router.get('/skills', (_req, res) => {\n const skills = getSkills();\n res.json(skills);\n });\n\n router.post('/skills/:name/toggle', (req, res) => {\n try {\n const { name } = req.params;\n const enabled = toggleSkill(name);\n const tools = getSkillTools(name);\n res.json({ ok: true, skill: name, enabled, tools });\n } catch (e) {\n res.status(404).json({ error: (e as Error).message });\n }\n });\n\n // ── Specialists ─────────────────────────────────────────────\n router.get('/specialists', async (_req, res) => {\n try {\n const { SPECIALISTS } = await import('../../agent/specialists.js');\n const cfg = loadConfig();\n const overrides = (cfg as unknown as { specialists?: { overrides?: Record<string, { model?: string }> } }).specialists?.overrides || {};\n const out = SPECIALISTS.map((s) => ({\n id: s.id,\n name: s.name,\n role: s.role,\n title: s.title,\n defaultModel: s.model,\n activeModel: overrides[s.id]?.model || s.model,\n overridden: Boolean(overrides[s.id]?.model && overrides[s.id].model !== s.model),\n templateMatches: s.templateMatches,\n reportsTo: s.reportsTo,\n }));\n res.json(out);\n } catch (e) {\n logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' });\n }\n });\n\n router.patch('/specialists/:id', async (req, res) => {\n try {\n const { id } = req.params;\n const { model } = (req.body || {}) as { model?: string | null };\n const { SPECIALISTS } = await import('../../agent/specialists.js');\n const specialist = SPECIALISTS.find((s) => s.id === id);\n if (!specialist) { res.status(404).json({ error: `Unknown specialist: ${id}` }); return; }\n const cfg = loadConfig();\n const cfgAny = cfg as unknown as { specialists?: { overrides?: Record<string, { model?: string }> } };\n const overrides = { ...(cfgAny.specialists?.overrides || {}) };\n if (model === null || model === '' || model === undefined) {\n delete overrides[id];\n } else if (typeof model === 'string') {\n overrides[id] = { model };\n } else {\n res.status(400).json({ error: 'model must be a string or null' });\n return;\n }\n updateConfig({ specialists: { overrides } } as unknown as Parameters<typeof updateConfig>[0]);\n res.json({ ok: true, id, activeModel: overrides[id]?.model || specialist.model });\n } catch (e) {\n logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' });\n }\n });\n\n // ── Marketplace ─────────────────────────────────────────────\n router.get('/marketplace', async (_req, res) => {\n try {\n const skills = await listMarketplaceSkills();\n const installed = listInstalledMarketplace();\n res.json({ skills: skills.map(s => ({ ...s, installed: installed.includes(s.file.replace('.js', '')) })), installed });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n router.get('/marketplace/search', async (req, res) => {\n try {\n const q = (req.query.q as string) || '';\n const results = await marketplaceSearch(q, 50);\n const installed = listInstalledMarketplace();\n res.json({ ...results, skills: results.skills.map(s => ({ ...s, installed: installed.includes(s.file.replace('.js', '')) })) });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n router.post('/marketplace/install', async (req, res): Promise<void> => {\n try {\n const { skill } = req.body as { skill: string };\n if (!skill) { res.status(400).json({ error: 'Missing \"skill\" field' }); return; }\n const result = await installSkill(skill);\n res.json(result);\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n router.post('/marketplace/uninstall', (req, res): void => {\n try {\n const { skill } = req.body as { skill: string };\n if (!skill) { res.status(400).json({ error: 'Missing \"skill\" field' }); return; }\n const result = uninstallSkill(skill);\n res.json(result);\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n // ── Personas ──────────────────────────────────────────────────\n router.get('/personas', (_req, res) => {\n try {\n const cfg = loadConfig();\n res.json({ personas: listPersonas(), active: cfg.agent.persona || 'default' });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n router.post('/persona/switch', (req, res): void => {\n try {\n const { persona } = req.body as { persona: string };\n if (!persona || typeof persona !== 'string') { res.status(400).json({ error: 'Missing persona ID' }); return; }\n if (persona !== 'default' && !getPersona(persona)) { res.status(404).json({ error: `Persona \"${persona}\" not found` }); return; }\n const cfg = loadConfig();\n updateConfig({ agent: { ...cfg.agent, persona } });\n invalidatePersonaCache();\n res.json({ ok: true, active: persona });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n // ── Widget Gallery ──────────────────────────────────────────\n router.get('/widget-gallery', async (_req, res) => {\n try {\n const { listTemplates, listCategories } = await import('../../skills/builtin/widget_gallery.js');\n const templates = listTemplates();\n const categories = listCategories();\n res.json({ count: templates.length, categories, templates });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n router.get('/widget-gallery/:id', async (req, res): Promise<void> => {\n try {\n const { getTemplate } = await import('../../skills/builtin/widget_gallery.js');\n const template = getTemplate(String(req.params.id || ''));\n if (!template) {\n res.status(404).json({ error: `Template not found: ${req.params.id}` });\n return;\n }\n\n const w = template.defaultSize?.w ?? 4;\n const h = template.defaultSize?.h ?? 4;\n const source = template.source.startsWith('system:')\n ? template.source\n : `// __WIDGET_META__ w=${w} h=${h}\\n${template.source || ''}`;\n\n res.json({\n id: template.id,\n name: template.name,\n category: template.category,\n defaultSize: template.defaultSize,\n source,\n placeholders: template.placeholders ?? [],\n });\n } catch (e) { logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' }); }\n });\n\n // ── Tools ───────────────────────────────────────────────────\n router.get('/tools', (req, res) => {\n const includeSchema = req.query.include === 'schema';\n const q = typeof req.query.q === 'string' ? req.query.q.toLowerCase() : '';\n const limit = Math.min(parseInt(req.query.limit as string, 10) || 100, 1000);\n const offset = Math.max(parseInt(req.query.offset as string, 10) || 0, 0);\n\n let tools = getRegisteredTools().map((t) => {\n const item: Record<string, unknown> = {\n name: t.name,\n description: t.description,\n };\n if (includeSchema) {\n item.parameters = t.parameters;\n }\n return item;\n });\n\n if (q) {\n tools = tools.filter((t) =>\n (t.name as string).toLowerCase().includes(q) ||\n (t.description as string).toLowerCase().includes(q),\n );\n }\n\n const total = tools.length;\n const paginated = tools.slice(offset, offset + limit);\n\n res.json({\n total,\n count: paginated.length,\n offset,\n tools: paginated,\n });\n });\n\n // ── Channels ──────────────────────────────────────────────\n router.get('/channels', (_req, res) => {\n const statuses = Array.from(channels.values()).map((ch) => ch.getStatus());\n res.json(statuses);\n });\n\n // ── Security ────────────────────────────────────────────────\n router.get('/security', (_req, res) => {\n const audit = auditSecurity();\n res.json(audit);\n });\n\n // ── Providers ─────────────────────────────────────────────\n router.get('/providers', async (_req, res) => {\n const health = await healthCheckAll();\n res.json(health);\n });\n\n // ── Health ──────────────────────────────────────────────────\n router.get('/health', async (_req, res) => {\n const cfg = loadConfig();\n let restarts: unknown = null;\n try {\n const { getRestartStats } = await import('../../utils/restartTracker.js');\n restarts = getRestartStats();\n } catch { /* tracker unavailable */ }\n res.json({ status: 'ok', version: TITAN_VERSION, uptime: process.uptime(), onboarded: cfg.onboarded, restarts });\n });\n\n // ── Bug Reports ─────────────────────────────────────────────\n router.get('/bug-reports', async (req, res) => {\n try {\n const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '50'), 10) || 50, 1), 200);\n const { listRecentBugReports } = await import('../../analytics/bugReports.js');\n const reports = listRecentBugReports(limit);\n res.json({ count: reports.length, reports });\n } catch (err) {\n res.status(500).json({ error: 'bug_reports_unavailable', message: (err as Error).message });\n }\n });\n\n router.get('/bug-reports/:id', async (req, res) => {\n try {\n const { getBugReport } = await import('../../analytics/bugReports.js');\n const r = getBugReport(req.params.id);\n if (!r) {\n res.status(404).json({ error: 'not_found' });\n return;\n }\n res.json(r);\n } catch (err) {\n res.status(500).json({ error: 'bug_report_unavailable', message: (err as Error).message });\n }\n });\n\n // ── Sandbox ─────────────────────────────────────────────────\n router.post('/sandbox/execute', async (req, res) => {\n try {\n const { code, language, timeoutMs } = req.body;\n if (!code || typeof code !== 'string') {\n res.status(400).json({ error: 'code is required' });\n return;\n }\n const { executeInSandbox } = await import('../../agent/sandbox.js');\n const result = await executeInSandbox(code, language || 'javascript', timeoutMs || 60000);\n res.json(result);\n } catch (err) {\n logger.error(COMPONENT, `Sandbox execute error: ${(err as Error).message}`);\n res.status(500).json({ error: 'Sandbox execution failed', message: (err as Error).message });\n }\n });\n\n router.get('/sandbox/status', async (_req, res) => {\n try {\n const { getSandboxStatus } = await import('../../agent/sandbox.js');\n res.json(getSandboxStatus());\n } catch (err) {\n res.status(500).json({ error: 'Sandbox status unavailable', message: (err as Error).message });\n }\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
@@ -39,6 +39,7 @@ import { initAgents, routeMessage, listAgents } from "../agent/multiAgent.js";
39
39
  import { createOpenAICompatRouter } from "../gateway/openai-compat.js";
40
40
  import logger, { initFileLogger } from "../utils/logger.js";
41
41
  import { TITAN_VERSION, TITAN_NAME, TITAN_LOGS_DIR, TITAN_HOME } from "../utils/constants.js";
42
+ import { getRestartStats as getRestartStatsSync } from "../utils/restartTracker.js";
42
43
  import { recordStartupAnalytics, startHeartbeatAnalytics } from "../analytics/collector.js";
43
44
  import { getUpdateInfo } from "../utils/updater.js";
44
45
  import { getMissionControlHTML } from "./dashboard.js";
@@ -605,6 +606,28 @@ async function startGateway(options) {
605
606
  logger.warn(COMPONENT, `[HttpPool] Failed to install global dispatcher: ${e.message} \u2014 fetch() will use Node defaults (unbounded pool)`);
606
607
  }
607
608
  logger.info(COMPONENT, `Starting ${TITAN_NAME} Gateway v${TITAN_VERSION}`);
609
+ try {
610
+ const { recordBoot, getRestartStats } = await import("../utils/restartTracker.js");
611
+ recordBoot(TITAN_VERSION);
612
+ const stats = getRestartStats();
613
+ if (stats.severity !== "ok" && stats.reason) {
614
+ const { sendAlert } = await import("../agent/alerts.js");
615
+ sendAlert(
616
+ stats.severity,
617
+ "Gateway restart loop detected",
618
+ stats.reason,
619
+ "restart-tracker",
620
+ {
621
+ restartsLast10Min: stats.restartsLast10Min,
622
+ restartsLastHour: stats.restartsLastHour,
623
+ nRestartsSystemd: stats.nRestartsSystemd,
624
+ previousBootAt: stats.previousBootAt
625
+ }
626
+ );
627
+ }
628
+ } catch (err) {
629
+ logger.warn(COMPONENT, `restart tracker init failed: ${err.message}`);
630
+ }
608
631
  if (!options?.skipUsableCheck) {
609
632
  const { hasUsableProvider } = await import("../config/config.js");
610
633
  const usable = await hasUsableProvider();
@@ -991,7 +1014,14 @@ async function startGateway(options) {
991
1014
  stuckDetected: healthState.stuckDetected,
992
1015
  uptimeSeconds: Math.round(process.uptime()),
993
1016
  memoryUsageMB: Math.round(mem.heapUsed / 1024 / 1024),
994
- activeLlmRequests
1017
+ activeLlmRequests,
1018
+ restarts: (() => {
1019
+ try {
1020
+ return getRestartStatsSync();
1021
+ } catch {
1022
+ return null;
1023
+ }
1024
+ })()
995
1025
  },
996
1026
  activeAgents,
997
1027
  activeSessions