titan-agent 5.5.10 → 5.5.11

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":";AAKA,SAAS,MAAM,YAAY;AAC3B,SAAS,YAAY,cAAc,qBAAqB;AACxD,SAAS,YAAY;AACrB,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AACjC,SAAS,mBAAmB;AAC5B,SAAS,wBAAwB;AACjC,OAAO,YAAY;AAEnB,MAAM,YAAY;AAClB,MAAM,aAAa,KAAK,YAAY,YAAY;AAChD,MAAM,YAAY,KAAK,YAAY,yBAAyB;AAG5D,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAG7B,SAAS,gBAAyC;AAC9C,MAAI,CAAC,WAAW,SAAS,EAAG,QAAO,EAAE,WAAW,CAAC,EAAE;AACnD,MAAI;AACA,UAAM,MAAM,aAAa,WAAW,OAAO;AAC3C,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO,EAAE,WAAW,MAAM,QAAQ,QAAQ,SAAS,IAAI,OAAO,YAAY,CAAC,EAAE;AAAA,EACjF,QAAQ;AACJ,WAAO,EAAE,WAAW,CAAC,EAAE;AAAA,EAC3B;AACJ;AAEA,SAAS,cAAc,OAAsC;AACzD,MAAI;AACA,qBAAiB,UAAU;AAC3B,kBAAc,WAAW,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,OAAO;AAAA,EACpE,QAAQ;AAAA,EAAqB;AACjC;AAGA,SAAS,gBAAgB,GAAW,GAAmB;AACnD,QAAM,WAAW,CAAC,MAAc,IAAI;AAAA,IAChC,EAAE,YAAY,EACT,QAAQ,YAAY,GAAG,EACvB,MAAM,KAAK,EACX,OAAO,OAAK,EAAE,SAAS,CAAC;AAAA,EACjC;AACA,QAAM,KAAK,SAAS,CAAC;AACrB,QAAM,KAAK,SAAS,CAAC;AACrB,MAAI,GAAG,SAAS,KAAK,GAAG,SAAS,EAAG,QAAO;AAC3C,MAAI,eAAe;AACnB,aAAW,KAAK,GAAI,KAAI,GAAG,IAAI,CAAC,EAAG;AACnC,QAAM,QAAQ,GAAG,OAAO,GAAG,OAAO;AAClC,SAAO,UAAU,IAAI,IAAI,eAAe;AAC5C;AAGA,SAAS,cAAc,OAAuD;AAC1E,MAAI,MAAO,QAAO,EAAE,SAAS,MAAM;AACnC,QAAM,QAAQ,cAAc;AAC5B,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,SAAS,KAAK,KAAK;AACzB,QAAM,SAAS,MAAM,UAAU,OAAO,OAAK,MAAM,IAAI,KAAK,CAAC,EAAE,QAAQ,IAAI,MAAM;AAC/E,MAAI,OAAO,UAAU,oBAAoB;AACrC,WAAO,EAAE,SAAS,MAAM,QAAQ,eAAe,OAAO,MAAM,wCAAwC,kBAAkB,IAAI;AAAA,EAC9H;AACA,SAAO,MAAK,oBAAI,KAAK,GAAE,YAAY,CAAC;AACpC,gBAAc,EAAE,WAAW,OAAO,CAAC;AACnC,SAAO,EAAE,SAAS,MAAM;AAC5B;AA8DA,MAAM,aAAa,iBAAyB;AAAA,EACxC,MAAM;AAAA,EACN,OAAO,CAAC,QAAQ;AACZ,UAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,WAAO,MAAM,SAAS,CAAC;AAAA,EAC3B;AAAA,EACA,SAAS,MAAM,CAAC;AAAA,EAChB,WAAW;AACf,CAAC;AAGD,SAAS,YAAoB;AACzB,SAAO,WAAW,KAAK;AAC3B;AAGA,SAAS,UAAU,eAA8B;AAC7C,QAAM,QAAQ,iBAAiB,WAAW,KAAK;AAC/C,MAAI;AACA,qBAAiB,UAAU;AAC3B,UAAM,QAAoB;AAAA,MACtB;AAAA,MACA,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACxC;AACA,kBAAc,YAAY,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,OAAO;AACjE,eAAW,IAAI,OAAO,WAAW;AAAA,EACrC,SAAS,KAAK;AACV,WAAO,MAAM,WAAW,yBAA0B,IAAc,OAAO,EAAE;AAAA,EAC7E;AACJ;AAGO,SAAS,WAAW,SAWlB;AACL,QAAM,QAAQ,UAAU;AAKxB,QAAM,iBAAiB,MAAM;AAAA,IAAK,OAC9B,EAAE,WAAW,YAAY,EAAE,MAAM,KAAK,MAAM,QAAQ,MAAM,KAAK;AAAA,EACnE;AACA,MAAI,gBAAgB;AAChB,WAAO,KAAK,WAAW,uBAAuB,QAAQ,KAAK,uBAAuB,eAAe,EAAE,4BAAuB;AAC1H,WAAO;AAAA,EACX;AAGA,QAAM,WAAW,MAAM;AAAA,IAAK,OACxB,EAAE,WAAW,YAAY,gBAAgB,EAAE,OAAO,QAAQ,KAAK,KAAK;AAAA,EACxE;AACA,MAAI,UAAU;AACV,WAAO,KAAK,WAAW,6BAA6B,QAAQ,KAAK,6BAA6B,SAAS,KAAK,MAAM,SAAS,EAAE,6BAAwB;AACrJ,WAAO;AAAA,EACX;AAGA,QAAM,WAAW,sBAAsB,KAAK,KAAK;AACjD,QAAM,YAAY,MAAM,KAAK,OAAK;AAC9B,QAAI,EAAE,MAAM,KAAK,MAAM,QAAQ,MAAM,KAAK,EAAG,QAAO;AACpD,UAAM,MAAM,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ;AACvD,WAAO,MAAM;AAAA,EACjB,CAAC;AACD,MAAI,WAAW;AACX,WAAO,KAAK,WAAW,8BAA8B,QAAQ,KAAK,aAAa,UAAU,EAAE,WAAW,mBAAmB,6BAAwB;AACjJ,WAAO;AAAA,EACX;AAGA,QAAM,cAAc,MAAM,OAAO,OAAK,EAAE,WAAW,QAAQ,EAAE;AAC7D,MAAI,CAAC,QAAQ,SAAS,eAAe,kBAAkB;AACnD,WAAO,KAAK,WAAW,uBAAuB,WAAW,wBAAwB,gBAAgB,+BAA+B;AAChI,UAAM,IAAI,MAAM,sBAAsB,WAAW,sBAAsB,gBAAgB,4BAA4B;AAAA,EACvH;AACA,MAAI,CAAC,QAAQ,SAAS,MAAM,UAAU,iBAAiB;AACnD,WAAO,KAAK,WAAW,uBAAuB,MAAM,MAAM,uBAAuB,eAAe,+BAA+B;AAC/H,UAAM,IAAI,MAAM,sBAAsB,MAAM,MAAM,qBAAqB,eAAe,4BAA4B;AAAA,EACtH;AAGA,QAAM,YAAY,cAAc,CAAC,CAAC,QAAQ,KAAK;AAC/C,MAAI,UAAU,SAAS;AACnB,WAAO,KAAK,WAAW,uBAAuB,UAAU,MAAM,EAAE;AAChE,UAAM,IAAI,MAAM,+BAA+B,UAAU,MAAM,EAAE;AAAA,EACrE;AAEA,QAAM,YAAuB,QAAQ,YAAY,CAAC,GAAG,IAAI,CAAC,IAAI,OAAO;AAAA,IACjE,IAAI,MAAM,IAAI,CAAC;AAAA,IACf,OAAO,GAAG;AAAA,IACV,aAAa,GAAG;AAAA,IAChB,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,WAAW,GAAG;AAAA,EAClB,EAAE;AAGF,MAAI,SAAS,KAAK,QAAM,GAAG,WAAW,MAAM,GAAG;AAC3C,UAAM,QAAQ,IAAI,IAAI,SAAS,IAAI,QAAM,GAAG,EAAE,CAAC;AAC/C,UAAM,UAAU,oBAAI,IAAsB;AAC1C,eAAW,MAAM,UAAU;AACvB,cAAQ,IAAI,GAAG,KAAK,GAAG,aAAa,CAAC,GAAG,OAAO,OAAK,MAAM,IAAI,CAAC,CAAC,CAAC;AAAA,IACrE;AACA,UAAM,UAAU,oBAAI,IAAY;AAChC,UAAM,UAAU,oBAAI,IAAY;AAChC,UAAM,WAAW,CAAC,SAA0B;AACxC,UAAI,QAAQ,IAAI,IAAI,EAAG,QAAO;AAC9B,UAAI,QAAQ,IAAI,IAAI,EAAG,QAAO;AAC9B,cAAQ,IAAI,IAAI;AAChB,cAAQ,IAAI,IAAI;AAChB,iBAAW,OAAO,QAAQ,IAAI,IAAI,KAAK,CAAC,GAAG;AACvC,YAAI,SAAS,GAAG,EAAG,QAAO;AAAA,MAC9B;AACA,cAAQ,OAAO,IAAI;AACnB,aAAO;AAAA,IACX;AACA,eAAW,MAAM,UAAU;AACvB,UAAI,SAAS,GAAG,EAAE,GAAG;AACjB,cAAM,IAAI,MAAM,2CAA2C,GAAG,EAAE,EAAE;AAAA,MACtE;AAAA,IACJ;AAAA,EACJ;AAEA,QAAM,OAAa;AAAA,IACf,IAAI,KAAK,EAAE,MAAM,GAAG,CAAC;AAAA,IACrB,OAAO,QAAQ;AAAA,IACf,aAAa,QAAQ;AAAA,IACrB,QAAQ;AAAA,IACR,UAAU,QAAQ,YAAY,KAAK,IAAI,MAAM,SAAS,GAAG,EAAE;AAAA,IAC3D;AAAA,IACA,UAAU,QAAQ;AAAA,IAClB,aAAa,QAAQ;AAAA,IACrB,WAAW;AAAA,IACX,UAAU;AAAA,IACV,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,MAAM,QAAQ;AAAA,IACd,cAAc,QAAQ;AAAA,EAC1B;AAEA,QAAM,KAAK,IAAI;AACf,YAAU,KAAK;AAEf,SAAO,KAAK,WAAW,kBAAkB,KAAK,KAAK,MAAM,KAAK,EAAE,UAAU,KAAK,SAAS,MAAM,WAAW;AACzG,cAAY,KAAK,gBAAgB,EAAE,QAAQ,KAAK,IAAI,OAAO,KAAK,OAAO,UAAU,KAAK,SAAS,OAAO,CAAC;AACvG,SAAO;AACX;AAGO,SAAS,UAAU,QAA6B;AACnD,QAAM,QAAQ,UAAU;AACxB,MAAI,OAAQ,QAAO,MAAM,OAAO,OAAK,EAAE,WAAW,MAAM;AACxD,SAAO;AACX;AAGO,SAAS,QAAQ,QAAkC;AACtD,SAAO,UAAU,EAAE,KAAK,OAAK,EAAE,OAAO,MAAM;AAChD;AAGO,SAAS,WAAW,QAAgB,SAStB;AACjB,QAAM,QAAQ,UAAU;AACxB,QAAM,OAAO,MAAM,KAAK,OAAK,EAAE,OAAO,MAAM;AAC5C,MAAI,CAAC,KAAM,QAAO;AAElB,MAAI,QAAQ,UAAU,OAAW,MAAK,QAAQ,QAAQ;AACtD,MAAI,QAAQ,gBAAgB,OAAW,MAAK,cAAc,QAAQ;AAClE,MAAI,QAAQ,WAAW,OAAW,MAAK,SAAS,QAAQ;AACxD,MAAI,QAAQ,aAAa,OAAW,MAAK,WAAW,QAAQ;AAC5D,MAAI,QAAQ,aAAa,OAAW,MAAK,WAAW,QAAQ;AAC5D,MAAI,QAAQ,aAAa,OAAW,MAAK,WAAW,QAAQ;AAC5D,MAAI,QAAQ,gBAAgB,OAAW,MAAK,cAAc,QAAQ;AAClE,MAAI,QAAQ,SAAS,OAAW,MAAK,OAAO,QAAQ;AAEpD,MAAI,QAAQ,WAAW,aAAa;AAChC,SAAK,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC1C,SAAK,WAAW;AAAA,EACpB;AAEA,OAAK,aAAY,oBAAI,KAAK,GAAE,YAAY;AACxC,YAAU,KAAK;AAEf,SAAO,KAAK,WAAW,kBAAkB,KAAK,KAAK,MAAM,KAAK,EAAE,GAAG;AACnE,SAAO;AACX;AAGO,SAAS,WAAW,QAAyB;AAChD,QAAM,QAAQ,UAAU;AACxB,QAAM,MAAM,MAAM,UAAU,OAAK,EAAE,OAAO,MAAM;AAChD,MAAI,QAAQ,GAAI,QAAO;AAEvB,QAAM,OAAO,KAAK,CAAC;AACnB,YAAU,KAAK;AAEf,SAAO,KAAK,WAAW,iBAAiB,MAAM,EAAE;AAChD,SAAO;AACX;AAGO,SAAS,WAAW,QAAgB,OAAe,aAA0C;AAChG,QAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,UAAmB;AAAA,IACrB,IAAI,MAAM,KAAK,SAAS,SAAS,CAAC;AAAA,IAClC;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,SAAS;AAAA,EACb;AAEA,OAAK,SAAS,KAAK,OAAO;AAC1B,OAAK,aAAY,oBAAI,KAAK,GAAE,YAAY;AACxC,YAAU;AACV,SAAO;AACX;AAGO,SAAS,gBAAgB,QAAgB,WAAmB,QAAyB;AACxF,QAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,UAAU,KAAK,SAAS,KAAK,QAAM,GAAG,OAAO,SAAS;AAC5D,MAAI,CAAC,QAAS,QAAO;AAErB,UAAQ,SAAS;AACjB,UAAQ,SAAS;AACjB,UAAQ,eAAc,oBAAI,KAAK,GAAE,YAAY;AAG7C,QAAM,OAAO,KAAK,SAAS,OAAO,QAAM,GAAG,WAAW,UAAU,GAAG,WAAW,SAAS,EAAE;AACzF,OAAK,WAAW,KAAK,MAAO,OAAO,KAAK,SAAS,SAAU,GAAG;AAG9D,MAAI,KAAK,SAAS,MAAM,QAAM,GAAG,WAAW,UAAU,GAAG,WAAW,SAAS,GAAG;AAC5E,SAAK,SAAS;AACd,SAAK,eAAc,oBAAI,KAAK,GAAE,YAAY;AAC1C,WAAO,KAAK,WAAW,yBAAyB,KAAK,KAAK,GAAG;AAC7D,gBAAY,KAAK,kBAAkB,EAAE,QAAQ,KAAK,IAAI,OAAO,KAAK,MAAM,CAAC;AAAA,EAC7E,OAAO;AACH,gBAAY,KAAK,iBAAiB,EAAE,QAAQ,KAAK,IAAI,OAAO,KAAK,OAAO,UAAU,KAAK,UAAU,WAAW,OAAO,CAAC;AAAA,EACxH;AAEA,OAAK,aAAY,oBAAI,KAAK,GAAE,YAAY;AACxC,YAAU;AACV,SAAO;AACX;AAGO,SAAS,YAAY,QAAgB,WAAmB,OAAwB;AACnF,QAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,UAAU,KAAK,SAAS,KAAK,QAAM,GAAG,OAAO,SAAS;AAC5D,MAAI,CAAC,QAAS,QAAO;AAErB,UAAQ;AACR,MAAI,QAAQ,WAAW,GAAG;AACtB,YAAQ,SAAS;AACjB,YAAQ,QAAQ;AAChB,gBAAY,KAAK,eAAe,EAAE,QAAQ,WAAW,OAAO,QAAQ,OAAO,OAAO,SAAS,QAAQ,QAAQ,CAAC;AAAA,EAChH,OAAO;AACH,YAAQ,SAAS;AAAA,EACrB;AAEA,OAAK,aAAY,oBAAI,KAAK,GAAE,YAAY;AACxC,YAAU;AACV,SAAO;AACX;AAIO,SAAS,aAAa,QAAgB,WAA4B;AACrE,QAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,UAAU,KAAK,SAAS,KAAK,QAAM,GAAG,OAAO,SAAS;AAC5D,MAAI,CAAC,QAAS,QAAO;AACrB,UAAQ,SAAS;AACjB,UAAQ,QAAQ;AAChB,UAAQ,UAAU;AAClB,UAAQ,cAAc;AACtB,OAAK,aAAY,oBAAI,KAAK,GAAE,YAAY;AACxC,YAAU;AACV,SAAO;AACX;AAGO,SAAS,cACZ,QACA,WACA,SACO;AACP,QAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,UAAU,KAAK,SAAS,KAAK,QAAM,GAAG,OAAO,SAAS;AAC5D,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,QAAQ,UAAU,SAAU,SAAQ,QAAQ,QAAQ;AAC/D,MAAI,OAAO,QAAQ,gBAAgB,SAAU,SAAQ,cAAc,QAAQ;AAC3E,OAAK,aAAY,oBAAI,KAAK,GAAE,YAAY;AACxC,YAAU;AACV,SAAO;AACX;AAGO,SAAS,gBAAyD;AACrE,QAAM,QAAQ,UAAU,EACnB,OAAO,OAAK,EAAE,WAAW,QAAQ,EACjC,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAE3C,QAAM,QAAiD,CAAC;AAExD,aAAW,QAAQ,OAAO;AAEtB,QAAI,KAAK,eAAe,KAAK,aAAa,KAAK,YAAa;AAG5D,UAAM,eAAe,IAAI;AAAA,MACrB,KAAK,SAAS,OAAO,QAAM,GAAG,WAAW,UAAU,GAAG,WAAW,SAAS,EAAE,IAAI,QAAM,GAAG,EAAE;AAAA,IAC/F;AAEA,eAAW,WAAW,KAAK,UAAU;AACjC,UAAI,QAAQ,WAAW,UAAW;AAGlC,YAAM,OAAO,QAAQ,aAAa,CAAC;AACnC,YAAM,YAAY,KAAK,MAAM,WAAS,aAAa,IAAI,KAAK,CAAC;AAC7D,UAAI,WAAW;AACX,cAAM,KAAK,EAAE,MAAM,QAAQ,CAAC;AAAA,MAChC;AAAA,IACJ;AAAA,EACJ;AAEA,SAAO;AACX;AAGO,SAAS,eAAe,QAAgB,MAAoB;AAC/D,QAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,CAAC,KAAM;AAEX,OAAK,aAAa;AAClB,OAAK,aAAY,oBAAI,KAAK,GAAE,YAAY;AACxC,YAAU;AACd;AAGO,SAAS,kBAA0B;AACtC,QAAM,QAAQ,UAAU;AACxB,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QAAM,QAAkB,CAAC,oBAAoB,EAAE;AAE/C,aAAW,QAAQ,MAAM,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ,GAAG;AAC9D,UAAM,OAAO,KAAK,WAAW,cAAc,WAAM,KAAK,WAAW,WAAW,cAAO,KAAK,WAAW,WAAW,iBAAO;AACrH,UAAM,OAAO,KAAK,SAAS,OAAO,QAAM,GAAG,WAAW,MAAM,EAAE;AAC9D,UAAM,KAAK,GAAG,IAAI,MAAM,KAAK,KAAK,OAAO,KAAK,MAAM,YAAO,IAAI,IAAI,KAAK,SAAS,MAAM,cAAc,KAAK,QAAQ,IAAI;AACtH,QAAI,KAAK,SAAU,OAAM,KAAK,gBAAgB,KAAK,QAAQ,EAAE;AAC7D,QAAI,KAAK,YAAa,OAAM,KAAK,eAAe,KAAK,UAAU,QAAQ,CAAC,CAAC,OAAO,KAAK,YAAY,QAAQ,CAAC,CAAC,EAAE;AAAA,EACjH;AAEA,SAAO,MAAM,KAAK,IAAI;AAC1B;AAGO,SAAS,mBAAmB,WAA4D;AAC3F,QAAM,QAAQ,UAAU,EAAE,OAAO,OAAK,EAAE,WAAW,QAAQ;AAC3D,QAAM,UAAmD,CAAC;AAE1D,aAAW,QAAQ,OAAO;AACtB,eAAW,WAAW,KAAK,UAAU;AACjC,UAAI,QAAQ,WAAW,UAAW;AAClC,UAAI,CAAC,QAAQ,WAAW,QAAQ,QAAQ,SAAS,QAAS;AAC1D,UAAI,CAAC,QAAQ,QAAQ,MAAO;AAG5B,YAAM,UAAU,QAAQ,QAAQ;AAChC,YAAM,UAAU,QAAQ,SAAS,GAAG,IAC9B,UAAU,WAAW,QAAQ,MAAM,GAAG,EAAE,CAAC,IACzC,cAAc;AAEpB,UAAI,SAAS;AACT,gBAAQ,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC9B,eAAO,KAAK,WAAW,2BAA2B,QAAQ,KAAK,YAAY,KAAK,KAAK,eAAe,SAAS,EAAE;AAAA,MACnH;AAAA,IACJ;AAAA,EACJ;AAEA,SAAO;AACX;AAGO,SAAS,kBAAkB,QAAgB,gBAAwB,OAAe,aAA0C;AAC/H,QAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,cAAc;AACpB,MAAI,KAAK,SAAS,UAAU,aAAa;AACrC,WAAO,KAAK,WAAW,kCAAkC,KAAK,KAAK,gBAAW,WAAW,UAAU;AACnG,WAAO;AAAA,EACX;AAEA,QAAM,UAAmB;AAAA,IACrB,IAAI,UAAU,KAAK,SAAS,SAAS,CAAC;AAAA,IACtC;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,WAAW,CAAC,cAAc;AAAA,EAC9B;AAEA,OAAK,SAAS,KAAK,OAAO;AAC1B,OAAK,aAAY,oBAAI,KAAK,GAAE,YAAY;AAGxC,QAAM,OAAO,KAAK,SAAS,OAAO,QAAM,GAAG,WAAW,UAAU,GAAG,WAAW,SAAS,EAAE;AACzF,OAAK,WAAW,KAAK,MAAO,OAAO,KAAK,SAAS,SAAU,GAAG;AAE9D,YAAU;AACV,SAAO,KAAK,WAAW,6BAA6B,KAAK,KAAK,OAAO,KAAK,iBAAiB,cAAc,GAAG;AAC5G,cAAY,KAAK,sBAAsB,EAAE,QAAQ,WAAW,QAAQ,IAAI,OAAO,eAAe,CAAC;AAC/F,SAAO;AACX;AASO,SAAS,cAAoB;AAChC,aAAW,WAAW;AACtB,YAAU;AACd;AAIO,SAAS,kBAAqE;AACjF,QAAM,QAAQ,UAAU;AACxB,QAAM,OAAO,oBAAI,IAAkB;AACnC,MAAI,SAAS;AAGb,QAAM,SAAS,CAAC,GAAG,KAAK,EAAE;AAAA,IAAK,CAAC,GAAG,MAC/B,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ;AAAA,EACpE;AAEA,aAAW,KAAK,QAAQ;AACpB,UAAM,MAAM,EAAE,MAAM,KAAK,EAAE,YAAY;AACvC,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAChB,WAAK,IAAI,KAAK,CAAC;AACf;AAAA,IACJ;AAEA,QAAI,EAAE,WAAW,UAAU;AACvB,QAAE,SAAS;AACX,QAAE,aAAY,oBAAI,KAAK,GAAE,YAAY;AACrC;AACA,aAAO,KAAK,WAAW,sCAAsC,EAAE,KAAK,MAAM,EAAE,EAAE,GAAG;AAAA,IACrF;AAAA,EACJ;AAEA,MAAI,SAAS,GAAG;AACZ,cAAU,KAAK;AAAA,EACnB;AACA,SAAO,EAAE,SAAS,MAAM,QAAQ,QAAQ,MAAM,KAAK,KAAK;AAC5D;","names":[]}
@@ -1,12 +1,28 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, writeFileSync, renameSync } from "fs";
2
+ import { writeFileSync, renameSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { TITAN_HOME } from "../utils/constants.js";
5
5
  import { mkdirIfNotExists } from "../utils/helpers.js";
6
+ import { createMtimeCache } from "../utils/mtimeCache.js";
6
7
  import logger from "../utils/logger.js";
7
8
  const COMPONENT = "Learning";
8
9
  const KNOWLEDGE_FILE = join(TITAN_HOME, "knowledge.json");
9
- let kb = null;
10
+ const kbCache = createMtimeCache({
11
+ path: KNOWLEDGE_FILE,
12
+ parse: (raw) => {
13
+ const parsed = JSON.parse(raw);
14
+ parsed.entries = parsed.entries || [];
15
+ parsed.toolSuccessRates = parsed.toolSuccessRates || {};
16
+ parsed.toolPreferencesByType = parsed.toolPreferencesByType || {};
17
+ parsed.strategies = parsed.strategies || [];
18
+ parsed.errorPatterns = parsed.errorPatterns || {};
19
+ parsed.userCorrections = parsed.userCorrections || [];
20
+ parsed.conversationInsights = parsed.conversationInsights || [];
21
+ return parsed;
22
+ },
23
+ initial: () => createEmptyKB(),
24
+ component: COMPONENT
25
+ });
10
26
  let dirty = false;
11
27
  let successfulStrategiesCache = null;
12
28
  let lastDecayRun = 0;
@@ -24,25 +40,8 @@ function getPatternWords(pattern) {
24
40
  return words;
25
41
  }
26
42
  function loadKnowledgeBase() {
27
- if (kb) return kb;
28
43
  mkdirIfNotExists(TITAN_HOME);
29
- if (existsSync(KNOWLEDGE_FILE)) {
30
- try {
31
- kb = JSON.parse(readFileSync(KNOWLEDGE_FILE, "utf-8"));
32
- kb.entries = kb.entries || [];
33
- kb.toolSuccessRates = kb.toolSuccessRates || {};
34
- kb.toolPreferencesByType = kb.toolPreferencesByType || {};
35
- kb.strategies = kb.strategies || [];
36
- kb.errorPatterns = kb.errorPatterns || {};
37
- kb.userCorrections = kb.userCorrections || [];
38
- kb.conversationInsights = kb.conversationInsights || [];
39
- } catch {
40
- kb = createEmptyKB();
41
- }
42
- } else {
43
- kb = createEmptyKB();
44
- }
45
- return kb;
44
+ return kbCache.read();
46
45
  }
47
46
  function createEmptyKB() {
48
47
  return {
@@ -56,12 +55,13 @@ function createEmptyKB() {
56
55
  };
57
56
  }
58
57
  function doSave() {
59
- if (!kb) return;
58
+ const kb = kbCache.read();
60
59
  mkdirIfNotExists(TITAN_HOME);
61
60
  try {
62
61
  const tmpFile = KNOWLEDGE_FILE + ".tmp";
63
62
  writeFileSync(tmpFile, JSON.stringify(kb, null, 2), "utf-8");
64
63
  renameSync(tmpFile, KNOWLEDGE_FILE);
64
+ kbCache.set(kb, "doSave");
65
65
  dirty = false;
66
66
  } catch (err) {
67
67
  dirty = true;
@@ -79,8 +79,8 @@ function debouncedSave() {
79
79
  saveTimeout.unref();
80
80
  }
81
81
  function initLearning() {
82
- loadKnowledgeBase();
83
- logger.info(COMPONENT, `Learning engine initialized (${kb?.entries.length ?? 0} knowledge entries)`);
82
+ const kb = loadKnowledgeBase();
83
+ logger.info(COMPONENT, `Learning engine initialized (${kb.entries.length} knowledge entries)`);
84
84
  }
85
85
  function classifyErrorPattern(error) {
86
86
  const trimmed = error.trim();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/memory/learning.ts"],"sourcesContent":["/**\n * TITAN — Learning Engine\n * Continuous self-improvement: learns from interactions, tracks patterns,\n * builds a knowledge base, and improves tool selection over time.\n */\nimport { existsSync, readFileSync, writeFileSync, renameSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { mkdirIfNotExists } from '../utils/helpers.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Learning';\nconst KNOWLEDGE_FILE = join(TITAN_HOME, 'knowledge.json');\n\nexport interface LearningEntry {\n id: string;\n type: 'skill_usage' | 'error_pattern' | 'user_correction' | 'successful_pattern' | 'fact' | 'preference';\n category: string;\n content: string;\n context?: string;\n score: number; // confidence/usefulness score (0-1)\n accessCount: number; // how often this knowledge was retrieved\n createdAt: string;\n updatedAt: string;\n}\n\n/** Tool preference by task type (Phase 4 — Active Learning) */\ninterface ToolPreference {\n tool: string;\n successRate: number;\n totalUses: number;\n}\n\n/** Strategy memory — records what approaches worked for task patterns */\ninterface StrategyEntry {\n pattern: string; // Task pattern description\n toolsUsed: string[]; // Deduplicated set\n toolSequence?: string[]; // Ordered sequence of tool calls (preserves order + repeats)\n taskType?: string; // Classified task type (coding, research, etc.)\n roundCount: number;\n success: boolean;\n successCount?: number; // How many times this sequence has succeeded\n failCount?: number; // How many times this strategy failed after being applied\n lastValidated?: string; // ISO timestamp of last successful validation\n timestamp: string;\n}\n\ninterface KnowledgeBase {\n entries: LearningEntry[];\n toolSuccessRates: Record<string, { success: number; fail: number; total: number }>;\n /** Tool success rates segmented by task type (coding, research, analysis, etc.) */\n toolPreferencesByType: Record<string, Record<string, { success: number; total: number }>>;\n /** Strategy memory — top 50 strategies, evicted by age + success */\n strategies: StrategyEntry[];\n errorPatterns: Record<string, { count: number; lastSeen: string; resolution?: string }>;\n userCorrections: Array<{ original: string; correction: string; timestamp: string }>;\n conversationInsights: Array<{ topic: string; outcome: string; toolsUsed: string[]; timestamp: string }>;\n}\n\nlet kb: KnowledgeBase | null = null;\nlet dirty = false;\n\n// ── Strategy hint caches ──────────────────────────────────────────\nlet successfulStrategiesCache: StrategyEntry[] | null = null;\nlet lastDecayRun = 0;\nconst DECAY_INTERVAL_MS = 3600_000; // 1 hour\nconst patternWordsCache = new Map<string, Set<string>>();\n\nfunction invalidateStrategyCaches(): void {\n successfulStrategiesCache = null;\n}\n\nfunction getPatternWords(pattern: string): Set<string> {\n let words = patternWordsCache.get(pattern);\n if (!words) {\n words = new Set(pattern.toLowerCase().split(/\\s+/).filter(w => w.length > 3));\n patternWordsCache.set(pattern, words);\n }\n return words;\n}\n\n// NOTE: Sync I/O is intentional — runs only once at cold start, then cached in-memory.\nfunction loadKnowledgeBase(): KnowledgeBase {\n if (kb) return kb;\n mkdirIfNotExists(TITAN_HOME);\n if (existsSync(KNOWLEDGE_FILE)) {\n try {\n kb = JSON.parse(readFileSync(KNOWLEDGE_FILE, 'utf-8'));\n // Ensure fields exist\n kb!.entries = kb!.entries || [];\n kb!.toolSuccessRates = kb!.toolSuccessRates || {};\n kb!.toolPreferencesByType = kb!.toolPreferencesByType || {};\n kb!.strategies = kb!.strategies || [];\n kb!.errorPatterns = kb!.errorPatterns || {};\n kb!.userCorrections = kb!.userCorrections || [];\n kb!.conversationInsights = kb!.conversationInsights || [];\n } catch {\n kb = createEmptyKB();\n }\n } else {\n kb = createEmptyKB();\n }\n return kb!;\n}\n\nfunction createEmptyKB(): KnowledgeBase {\n return {\n entries: [],\n toolSuccessRates: {},\n toolPreferencesByType: {},\n strategies: [],\n errorPatterns: {},\n userCorrections: [],\n conversationInsights: [],\n };\n}\n\nfunction doSave(): void {\n if (!kb) return;\n mkdirIfNotExists(TITAN_HOME);\n try {\n const tmpFile = KNOWLEDGE_FILE + '.tmp';\n writeFileSync(tmpFile, JSON.stringify(kb, null, 2), 'utf-8');\n renameSync(tmpFile, KNOWLEDGE_FILE);\n dirty = false;\n } catch (err) {\n dirty = true;\n logger.error(COMPONENT, `Failed to save knowledge base: ${(err as Error).message}`);\n }\n}\n\nlet saveTimeout: ReturnType<typeof setTimeout> | null = null;\nfunction debouncedSave(): void {\n if (dirty) { doSave(); return; }\n if (saveTimeout) clearTimeout(saveTimeout);\n saveTimeout = setTimeout(doSave, 2000);\n saveTimeout.unref();\n}\n\n/** Initialize the learning engine */\nexport function initLearning(): void {\n loadKnowledgeBase();\n logger.info(COMPONENT, `Learning engine initialized (${kb?.entries.length ?? 0} knowledge entries)`);\n}\n\n/**\n * v4.9.0-local.9: detect \"error\" strings that are actually file content\n * echoed back as context (common when build tools dump failing source).\n * The pre-.9 behavior was to slice(0, 200) and dedupe on that prefix,\n * which made the same file show up many times and inflated Curiosity's\n * \"unresolved patterns\" count by ~5-10x. Observed on 2026-04-18: 327\n * total patterns, of which >50% were just slices of titan-saas source.\n *\n * Heuristic: if the string looks like source code being printed back,\n * don't record it as a distinct error pattern. Specifically:\n * - starts with \"File: /path/to/...\" (compiler output convention)\n * - contains \"--- 1:\" or \"--- 2:\" etc. (line-numbered code dumps)\n * - is >1200 chars (real error strings are usually short; anything\n * longer is almost always dumped source)\n * - is an entire TypeScript/JS import block (≥3 `import ` lines)\n *\n * When detected, we record a GENERIC rollup pattern (\"build-dumped-source:\n * <basename>\") instead of the raw content, so Curiosity sees one entry per\n * file instead of N copies of the same file's content.\n */\nfunction classifyErrorPattern(error: string): { pattern: string; isFileDump: boolean } {\n const trimmed = error.trim();\n const basename = (m: RegExpMatchArray | null): string => {\n const path = m?.[1] ?? '';\n return path.split('/').pop() ?? 'unknown';\n };\n\n // \"File: /path/to/foo.ts (N lines) --- ...\"\n const fileHeaderMatch = trimmed.match(/^File:\\s+(\\S+)\\s+\\(\\d+\\s+lines\\)/);\n if (fileHeaderMatch) {\n return { pattern: `build-dumped-source:${basename(fileHeaderMatch)}`, isFileDump: true };\n }\n\n // Multi-line numbered code dump (\"--- 1: ... --- 2: ...\")\n if (/---\\s*\\d+:/.test(trimmed) && /---\\s*2:/.test(trimmed)) {\n return { pattern: 'build-dumped-source:numbered-code-block', isFileDump: true };\n }\n\n // Import blocks (TypeScript/JS) — ≥3 import lines → probably source\n const importLines = (trimmed.match(/^import\\s+/gm) || []).length;\n if (importLines >= 3) {\n return { pattern: 'build-dumped-source:import-block', isFileDump: true };\n }\n\n // v5.5.8: Next.js build output — \"▲ Next.js X.Y.Z ... Compiled successfully ...\"\n // Was being recorded as 100s of \"error patterns\" because builds dump to stderr\n // even on success. Roll up to a single canonical entry.\n if (/▲\\s+Next\\.js\\s+[\\d.]+|Creating an optimized production build|Linting and checking validity/.test(trimmed)) {\n return { pattern: 'build-noise:nextjs-output', isFileDump: true };\n }\n\n // v5.5.8: Vitest assertion failures — \"expected X to be Y // Object.is equality\"\n // These are stale test failures from past runs, not recurring runtime errors.\n // Roll up by assertion shape rather than specific values.\n const vitestMatch = trimmed.match(/^expected\\s+.+\\s+to\\s+(be|deeply equal|have|contain|match)\\b/);\n if (vitestMatch) {\n return { pattern: `test-noise:vitest-assertion:${vitestMatch[1]}`, isFileDump: true };\n }\n\n // v5.5.8: bare \"build-dumped-source:...\" lines (already-classified entries\n // that got re-recorded as errors). Treat as same canonical pattern.\n if (trimmed.startsWith('build-dumped-source:')) {\n return { pattern: trimmed.split('\\n')[0].slice(0, 200), isFileDump: true };\n }\n\n // Too long to be a useful signature\n if (trimmed.length > 1200) {\n return { pattern: 'oversized-error:' + trimmed.slice(0, 60).replace(/\\s+/g, ' '), isFileDump: true };\n }\n\n // Normal short error — keep the original 200-char slice behavior\n return { pattern: trimmed.slice(0, 200), isFileDump: false };\n}\n\n/** Record a tool execution result for learning */\nexport function recordToolResult(toolName: string, success: boolean, context?: string, error?: string): void {\n const k = loadKnowledgeBase();\n\n // Update tool success rates\n if (!k.toolSuccessRates[toolName]) {\n k.toolSuccessRates[toolName] = { success: 0, fail: 0, total: 0 };\n }\n k.toolSuccessRates[toolName].total++;\n if (success) {\n k.toolSuccessRates[toolName].success++;\n } else {\n k.toolSuccessRates[toolName].fail++;\n // Track error patterns — with file-content detection (v4.9.0-local.9)\n if (error) {\n const { pattern } = classifyErrorPattern(error);\n if (!k.errorPatterns[pattern]) {\n k.errorPatterns[pattern] = { count: 0, lastSeen: '' };\n }\n k.errorPatterns[pattern].count++;\n k.errorPatterns[pattern].lastSeen = new Date().toISOString();\n }\n }\n\n debouncedSave();\n}\n\n// Exposed for tests + one-time migration tooling\nexport { classifyErrorPattern };\n\n/** Record a successful interaction pattern */\nexport function recordSuccessPattern(pattern: {\n topic: string;\n toolsUsed: string[];\n outcome: string;\n}): void {\n const k = loadKnowledgeBase();\n k.conversationInsights.push({\n ...pattern,\n timestamp: new Date().toISOString(),\n });\n // Keep last 500 insights\n if (k.conversationInsights.length > 500) {\n k.conversationInsights = k.conversationInsights.slice(-500);\n }\n debouncedSave();\n}\n\n/** Record a user correction to learn from mistakes */\nexport function recordUserCorrection(original: string, correction: string): void {\n const k = loadKnowledgeBase();\n k.userCorrections.push({\n original,\n correction,\n timestamp: new Date().toISOString(),\n });\n if (k.userCorrections.length > 200) {\n k.userCorrections = k.userCorrections.slice(-200);\n }\n debouncedSave();\n}\n\n/** Learn a new fact or update an existing one */\nexport function learnFact(category: string, content: string, context?: string): void {\n const k = loadKnowledgeBase();\n const id = `${category}:${content.slice(0, 50)}`;\n const existing = k.entries.findIndex((e) => e.id === id);\n\n if (existing >= 0) {\n k.entries[existing].score = Math.min(k.entries[existing].score + 0.1, 1.0);\n k.entries[existing].accessCount++;\n k.entries[existing].updatedAt = new Date().toISOString();\n } else {\n k.entries.push({\n id,\n type: 'fact',\n category,\n content,\n context,\n score: 0.5,\n accessCount: 0,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n });\n }\n // Keep entries under 2000\n if (k.entries.length > 2000) {\n // Remove lowest-scored entries\n k.entries.sort((a, b) => b.score - a.score);\n k.entries = k.entries.slice(0, 1500);\n }\n debouncedSave();\n}\n\n/** Query the knowledge base for relevant information */\nexport function queryKnowledge(query: string, category?: string, limit: number = 10): LearningEntry[] {\n const k = loadKnowledgeBase();\n const q = query.toLowerCase();\n\n const results = k.entries.filter((e) => {\n const matchesQuery = e.content.toLowerCase().includes(q) ||\n e.category.toLowerCase().includes(q) ||\n (e.context && e.context.toLowerCase().includes(q));\n const matchesCategory = !category || e.category === category;\n return matchesQuery && matchesCategory;\n });\n\n // Sort by score (most useful first), then by recency\n results.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n return b.updatedAt.localeCompare(a.updatedAt);\n });\n\n // Boost access count for retrieved entries\n for (const entry of results.slice(0, limit)) {\n entry.accessCount++;\n entry.score = Math.min(entry.score + 0.01, 1.0);\n }\n\n debouncedSave();\n return results.slice(0, limit);\n}\n\n/** Get tool recommendations based on historical success rates */\nexport function getToolRecommendations(): Record<string, number> {\n const k = loadKnowledgeBase();\n const recommendations: Record<string, number> = {};\n\n for (const [tool, stats] of Object.entries(k.toolSuccessRates)) {\n if (stats.total > 0) {\n recommendations[tool] = stats.success / stats.total;\n }\n }\n\n return recommendations;\n}\n\n/**\n * TITAN pattern: Memory staleness verification\n * Before acting on learned knowledge, verify it's still current.\n * Strategies unvalidated for 30+ days lose 20% successCount.\n * Knowledge entries older than 60 days get flagged as potentially stale.\n */\nexport function verifyMemoryStaleness(): { pruned: number; decayed: number } {\n const k = loadKnowledgeBase();\n const now = Date.now();\n const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;\n const SIXTY_DAYS = 60 * 24 * 60 * 60 * 1000;\n let pruned = 0;\n let decayed = 0;\n\n // Decay strategies unvalidated for 30+ days\n for (const strategy of k.strategies) {\n const lastValidated = strategy.lastValidated ? new Date(strategy.lastValidated).getTime() : new Date(strategy.timestamp).getTime();\n if (now - lastValidated > THIRTY_DAYS && (strategy.successCount || 0) > 0) {\n strategy.successCount = Math.floor((strategy.successCount || 0) * 0.8);\n decayed++;\n }\n }\n\n // Remove knowledge entries older than 60 days with low scores\n const before = k.entries.length;\n k.entries = k.entries.filter(e => {\n const age = now - new Date(e.createdAt).getTime();\n return age < SIXTY_DAYS || e.score > 0.7;\n });\n pruned = before - k.entries.length;\n\n // Remove error patterns not seen in 30+ days\n // v5.5.8: ALSO remove unresolved patterns not seen in 7+ days. These have\n // stopped recurring (no count increment for a week) so they're stale signal,\n // not a current problem. Was inflating Curiosity's `unresolvedErrorPatterns`\n // count to 139 with build-noise/test-noise from 2-3 weeks ago.\n const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n const ageMs = now - new Date(info.lastSeen).getTime();\n if (ageMs > THIRTY_DAYS) {\n delete k.errorPatterns[pattern];\n pruned++;\n } else if (ageMs > SEVEN_DAYS && !info.resolution) {\n // Unresolved + dormant ≥7d → stale, not real\n delete k.errorPatterns[pattern];\n pruned++;\n }\n }\n\n if (pruned > 0 || decayed > 0) {\n doSave();\n logger.info(COMPONENT, `Memory staleness check: ${pruned} pruned, ${decayed} strategies decayed`);\n }\n\n return { pruned, decayed };\n}\n\n/**\n * v4.9.0-local.9: one-time cleanup of \"error patterns\" that are actually\n * file content dumps. Retroactively applies the same classifyErrorPattern\n * logic that now runs on record. Collapses dozens of sliced file-content\n * entries into a single rollup per file.\n *\n * Returns { removed, collapsedInto } where `removed` is the number of raw\n * entries deleted and `collapsedInto` is how many rollup entries were\n * produced (usually much smaller).\n */\nexport function pruneFileContentErrorPatterns(): { removed: number; collapsedInto: number } {\n const k = loadKnowledgeBase();\n const rollup = new Map<string, { count: number; lastSeen: string; resolution?: string }>();\n const keep: Record<string, { count: number; lastSeen: string; resolution?: string }> = {};\n let removed = 0;\n\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n const { pattern: canonical, isFileDump } = classifyErrorPattern(pattern);\n if (isFileDump) {\n // Collapse into the rollup bucket\n const existing = rollup.get(canonical);\n if (existing) {\n existing.count += info.count;\n if (info.lastSeen > existing.lastSeen) existing.lastSeen = info.lastSeen;\n if (!existing.resolution && info.resolution) existing.resolution = info.resolution;\n } else {\n rollup.set(canonical, { count: info.count, lastSeen: info.lastSeen, resolution: info.resolution });\n }\n removed++;\n } else {\n keep[pattern] = info;\n }\n }\n // Merge rollups back in (they may collide with real entries — count-sum if so)\n for (const [canonical, info] of rollup) {\n if (keep[canonical]) {\n keep[canonical].count += info.count;\n if (info.lastSeen > keep[canonical].lastSeen) keep[canonical].lastSeen = info.lastSeen;\n } else {\n keep[canonical] = info;\n }\n }\n k.errorPatterns = keep;\n doSave();\n logger.info(COMPONENT, `[PrunePatterns] removed ${removed} file-content entries, collapsed into ${rollup.size} rollup entries`);\n return { removed, collapsedInto: rollup.size };\n}\n\n/** Get learning summary for the system prompt */\nexport function getLearningContext(): string {\n const k = loadKnowledgeBase();\n const parts: string[] = [];\n\n // Recent high-score knowledge\n const topEntries = k.entries\n .filter((e) => e.score > 0.6)\n .sort((a, b) => b.score - a.score)\n .slice(0, 10);\n\n if (topEntries.length > 0) {\n parts.push('Key learned facts:');\n for (const e of topEntries) {\n parts.push(`- [${e.category}] ${e.content}`);\n }\n }\n\n // Tool recommendations\n const toolRecs = getToolRecommendations();\n const bestTools = Object.entries(toolRecs)\n .filter(([_, rate]) => rate > 0.8)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5);\n\n if (bestTools.length > 0) {\n parts.push('\\nMost reliable tools:');\n for (const [tool, rate] of bestTools) {\n parts.push(`- ${tool}: ${Math.round(rate * 100)}% success rate`);\n }\n }\n\n // Common error patterns to avoid\n const frequentErrors = Object.entries(k.errorPatterns)\n .filter(([_, info]) => info.count > 2)\n .sort((a, b) => b[1].count - a[1].count)\n .slice(0, 3);\n\n if (frequentErrors.length > 0) {\n parts.push('\\nCommon errors to avoid:');\n for (const [pattern, info] of frequentErrors) {\n parts.push(`- ${pattern.slice(0, 100)} (seen ${info.count}x)${info.resolution ? ` → Fix: ${info.resolution}` : ''}`);\n }\n }\n\n return parts.join('\\n');\n}\n\n/** Get per-tool reliability warnings/boosts for tool description injection */\nexport function getToolWarnings(): Record<string, string> {\n const k = loadKnowledgeBase();\n const warnings: Record<string, string> = {};\n\n for (const [tool, stats] of Object.entries(k.toolSuccessRates)) {\n if (stats.total < 10) continue; // Need enough data to be meaningful\n const rate = stats.success / stats.total;\n if (rate < 0.3) {\n warnings[tool] = `[LOW RELIABILITY: ${Math.round(rate * 100)}% success rate over ${stats.total} uses]`;\n } else if (rate > 0.9) {\n warnings[tool] = `[HIGHLY RELIABLE: ${Math.round(rate * 100)}% success rate]`;\n }\n }\n\n return warnings;\n}\n\n/** Record when a tool failure is resolved by using a different tool */\nexport function recordErrorResolution(errorPattern: string, resolution: string): void {\n const k = loadKnowledgeBase();\n const pattern = errorPattern.slice(0, 200);\n if (k.errorPatterns[pattern]) {\n k.errorPatterns[pattern].resolution = resolution;\n } else {\n k.errorPatterns[pattern] = { count: 1, lastSeen: new Date().toISOString(), resolution };\n }\n debouncedSave();\n}\n\n/** Get stats about the learning system */\nexport function getLearningStats(): {\n knowledgeEntries: number;\n toolsTracked: number;\n errorPatterns: number;\n /**\n * v4.10.0-local fix: true count of UNRESOLVED patterns, used by the\n * curiosity drive. Prior behavior was to report `errorPatterns` (total)\n * as if it were unresolved — meaning marking patterns as resolved did\n * nothing to the drive signal. Now separate field.\n */\n unresolvedErrorPatterns: number;\n corrections: number;\n insights: number;\n strategies: number;\n taskTypes: number;\n} {\n const k = loadKnowledgeBase();\n const unresolvedErrorPatterns = Object.values(k.errorPatterns).filter(v => !v.resolution).length;\n return {\n knowledgeEntries: k.entries.length,\n toolsTracked: Object.keys(k.toolSuccessRates).length,\n errorPatterns: Object.keys(k.errorPatterns).length,\n unresolvedErrorPatterns,\n corrections: k.userCorrections.length,\n insights: k.conversationInsights.length,\n strategies: k.strategies.length,\n taskTypes: Object.keys(k.toolPreferencesByType).length,\n };\n}\n\n// ── Phase 4: Active Learning ──────────────────────────────────────\n\n/** Classify a message into a task type for preference tracking */\nexport function classifyTaskType(message: string): string {\n const lower = message.toLowerCase();\n if (/\\b(code|function|class|typescript|python|script|debug|compile|build)\\b/.test(lower)) return 'coding';\n if (/\\b(search|research|find|look up|investigate|compare)\\b/.test(lower)) return 'research';\n if (/\\b(analy[sz]e|data|csv|chart|graph|statistics|metrics)\\b/.test(lower)) return 'analysis';\n if (/\\b(write|draft|blog|article|email|message|story)\\b/.test(lower)) return 'writing';\n if (/\\b(deploy|server|docker|kubernetes|ci|cd|infrastructure)\\b/.test(lower)) return 'devops';\n if (/\\b(file|folder|directory|rename|move|copy|delete)\\b/.test(lower)) return 'filesystem';\n if (/\\b(schedule|cron|automat|workflow|remind)\\b/.test(lower)) return 'automation';\n return 'general';\n}\n\n/** Record a tool result against a specific task type */\nexport function recordToolPreference(toolName: string, taskType: string, success: boolean): void {\n const k = loadKnowledgeBase();\n if (!k.toolPreferencesByType[taskType]) {\n k.toolPreferencesByType[taskType] = {};\n }\n if (!k.toolPreferencesByType[taskType][toolName]) {\n k.toolPreferencesByType[taskType][toolName] = { success: 0, total: 0 };\n }\n\n k.toolPreferencesByType[taskType][toolName].total++;\n if (success) k.toolPreferencesByType[taskType][toolName].success++;\n debouncedSave();\n}\n\n/** Get ranked tool preferences for a task type */\nexport function getToolPreferences(taskType: string): ToolPreference[] {\n const k = loadKnowledgeBase();\n const prefs = k.toolPreferencesByType[taskType];\n if (!prefs) return [];\n\n return Object.entries(prefs)\n .filter(([, stats]) => stats.total >= 3) // Minimum sample size\n .map(([tool, stats]) => ({\n tool,\n successRate: stats.success / stats.total,\n totalUses: stats.total,\n }))\n .sort((a, b) => b.successRate - a.successRate);\n}\n\n/** Generate human-readable preference hints from collected tool-preference data */\nexport function getLearnedPreferenceHints(taskType: string): string | null {\n const prefs = getToolPreferences(taskType);\n if (prefs.length < 2) return null;\n\n const lines: string[] = [];\n const FILE_TOOLS = ['read_file', 'write_file', 'edit_file', 'list_dir'];\n const shellPref = prefs.find(p => p.tool === 'shell');\n\n // Find cases where shell has lower success than dedicated tools\n for (const dt of prefs.filter(p => FILE_TOOLS.includes(p.tool))) {\n if (shellPref && dt.successRate > shellPref.successRate && dt.totalUses >= 3) {\n lines.push(\n `prefer ${dt.tool} (${Math.round(dt.successRate * 100)}% success) ` +\n `over shell (${Math.round(shellPref.successRate * 100)}% success)`,\n );\n }\n }\n\n // Surface top tools for this task type if no shell comparison available\n if (lines.length === 0) {\n const top3 = prefs.slice(0, 3);\n if (top3.length >= 2) {\n lines.push(`best tools: ${top3.map(p => `${p.tool} (${Math.round(p.successRate * 100)}%)`).join(', ')}`);\n }\n }\n\n return lines.length > 0 ? `For ${taskType} tasks: ${lines.join('; ')}` : null;\n}\n\n/** Record a successful strategy for future reference */\nexport function recordStrategy(\n message: string,\n toolsUsed: string[],\n roundCount: number,\n success: boolean,\n toolSequence?: string[],\n): void {\n const k = loadKnowledgeBase();\n const taskType = classifyTaskType(message);\n\n // Check if a similar sequence already exists — merge instead of duplicating\n if (success && toolSequence && toolSequence.length > 0) {\n const seqKey = toolSequence.join('→');\n const existing = k.strategies.find(\n s => s.success && s.taskType === taskType && s.toolSequence?.join('→') === seqKey,\n );\n if (existing) {\n existing.successCount = (existing.successCount || 1) + 1;\n existing.timestamp = new Date().toISOString();\n debouncedSave();\n return;\n }\n }\n\n const entry: StrategyEntry = {\n pattern: message.slice(0, 200),\n toolsUsed: [...new Set(toolsUsed)],\n toolSequence: toolSequence?.slice(0, 20), // Cap sequence length\n taskType,\n roundCount,\n success,\n successCount: success ? 1 : 0,\n timestamp: new Date().toISOString(),\n };\n\n k.strategies.push(entry);\n invalidateStrategyCaches();\n\n // Evict old/failed strategies to keep at 200 max\n if (k.strategies.length > 200) {\n // Sort: keep high-success + recent, evict failed + old + low-success\n k.strategies.sort((a, b) => {\n if (a.success !== b.success) return a.success ? -1 : 1;\n const aCount = a.successCount || 1;\n const bCount = b.successCount || 1;\n if (aCount !== bCount) return bCount - aCount;\n return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();\n });\n k.strategies = k.strategies.slice(0, 200);\n }\n\n debouncedSave();\n}\n\n/** Record outcome of an applied strategy (feedback loop) */\nexport function recordStrategyOutcome(\n taskType: string,\n toolSequence: string[],\n succeeded: boolean,\n): void {\n const k = loadKnowledgeBase();\n const seqKey = toolSequence.join('→');\n const match = k.strategies.find(\n s => s.taskType === taskType && s.toolSequence?.join('→') === seqKey,\n );\n if (!match) return;\n\n if (succeeded) {\n match.successCount = (match.successCount || 1) + 1;\n match.lastValidated = new Date().toISOString();\n } else {\n match.failCount = (match.failCount || 0) + 1;\n }\n\n // High-fail strategies get marked as unsuccessful\n const becameUnsuccessful = (match.failCount || 0) > (match.successCount || 1) && match.success;\n if (becameUnsuccessful) {\n match.success = false;\n }\n\n if (becameUnsuccessful) invalidateStrategyCaches();\n debouncedSave();\n}\n\n/** Decay unvalidated strategies — called on every getStrategyHints */\nfunction decayStrategies(strategies: StrategyEntry[]): boolean {\n const now = Date.now();\n let changed = false;\n\n for (const s of strategies) {\n const lastValidated = s.lastValidated\n ? new Date(s.lastValidated).getTime()\n : new Date(s.timestamp).getTime();\n const daysSinceValidated = (now - lastValidated) / 86400000;\n\n // Strategies not validated in 30 days: reduce successCount by 20%\n if (daysSinceValidated > 30 && (s.successCount || 1) > 1) {\n s.successCount = Math.max(1, Math.floor((s.successCount || 1) * 0.8));\n changed = true;\n }\n\n // High-fail strategies get excluded\n if ((s.failCount || 0) > (s.successCount || 1) && s.success) {\n s.success = false;\n changed = true;\n }\n }\n\n return changed;\n}\n\n/** Get strategy hints for a similar task */\nexport function getStrategyHints(message: string): string | null {\n const k = loadKnowledgeBase();\n\n // Apply decay to stale strategies\n const now = Date.now();\n if (now - lastDecayRun > DECAY_INTERVAL_MS) {\n lastDecayRun = now;\n if (decayStrategies(k.strategies)) {\n debouncedSave();\n invalidateStrategyCaches();\n }\n }\n if (k.strategies.length === 0) return null;\n\n const taskType = classifyTaskType(message);\n const words = new Set(message.toLowerCase().split(/\\s+/).filter(w => w.length > 3));\n if (words.size === 0) return null;\n\n const successfulStrategies = successfulStrategiesCache ?? (successfulStrategiesCache = k.strategies.filter(s => s.success));\n if (successfulStrategies.length === 0) return null;\n\n // Score strategies by: task type match + keyword overlap + success count\n let bestMatch: StrategyEntry | null = null;\n let bestScore = 0;\n\n for (const strategy of successfulStrategies) {\n const patternWords = getPatternWords(strategy.pattern);\n let overlap = 0;\n for (const w of words) {\n if (patternWords.has(w)) overlap++;\n }\n const keywordScore = overlap / Math.max(words.size, patternWords.size);\n\n // Task type match bonus (0.2)\n const typeBonus = strategy.taskType === taskType ? 0.2 : 0;\n\n // Success count bonus (normalized, max 0.15)\n const countBonus = Math.min((strategy.successCount || 1) / 10, 0.15);\n\n const totalScore = keywordScore + typeBonus + countBonus;\n if (totalScore > bestScore && keywordScore > 0.15) {\n bestScore = totalScore;\n bestMatch = strategy;\n }\n }\n\n if (!bestMatch) return null;\n\n // Prefer showing the ordered sequence if available\n const toolInfo = bestMatch.toolSequence && bestMatch.toolSequence.length > 0\n ? bestMatch.toolSequence.join(' → ')\n : bestMatch.toolsUsed.join(', ');\n\n const countInfo = (bestMatch.successCount || 1) > 1\n ? ` (succeeded ${bestMatch.successCount}x)`\n : '';\n\n return `For similar ${bestMatch.taskType || 'general'} tasks, a proven tool sequence: ${toolInfo} (${bestMatch.roundCount} rounds)${countInfo}.`;\n}\n\n/** Get error resolution if a known pattern matches */\nexport function getErrorResolution(error: string): string | null {\n const k = loadKnowledgeBase();\n const errorLower = error.toLowerCase();\n\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n if (info.resolution && errorLower.includes(pattern.toLowerCase().slice(0, 50))) {\n return info.resolution;\n }\n }\n return null;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/memory/learning.ts"],"sourcesContent":["/**\n * TITAN — Learning Engine\n * Continuous self-improvement: learns from interactions, tracks patterns,\n * builds a knowledge base, and improves tool selection over time.\n */\nimport { writeFileSync, renameSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { mkdirIfNotExists } from '../utils/helpers.js';\nimport { createMtimeCache } from '../utils/mtimeCache.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Learning';\nconst KNOWLEDGE_FILE = join(TITAN_HOME, 'knowledge.json');\n\nexport interface LearningEntry {\n id: string;\n type: 'skill_usage' | 'error_pattern' | 'user_correction' | 'successful_pattern' | 'fact' | 'preference';\n category: string;\n content: string;\n context?: string;\n score: number; // confidence/usefulness score (0-1)\n accessCount: number; // how often this knowledge was retrieved\n createdAt: string;\n updatedAt: string;\n}\n\n/** Tool preference by task type (Phase 4 — Active Learning) */\ninterface ToolPreference {\n tool: string;\n successRate: number;\n totalUses: number;\n}\n\n/** Strategy memory — records what approaches worked for task patterns */\ninterface StrategyEntry {\n pattern: string; // Task pattern description\n toolsUsed: string[]; // Deduplicated set\n toolSequence?: string[]; // Ordered sequence of tool calls (preserves order + repeats)\n taskType?: string; // Classified task type (coding, research, etc.)\n roundCount: number;\n success: boolean;\n successCount?: number; // How many times this sequence has succeeded\n failCount?: number; // How many times this strategy failed after being applied\n lastValidated?: string; // ISO timestamp of last successful validation\n timestamp: string;\n}\n\ninterface KnowledgeBase {\n entries: LearningEntry[];\n toolSuccessRates: Record<string, { success: number; fail: number; total: number }>;\n /** Tool success rates segmented by task type (coding, research, analysis, etc.) */\n toolPreferencesByType: Record<string, Record<string, { success: number; total: number }>>;\n /** Strategy memory — top 50 strategies, evicted by age + success */\n strategies: StrategyEntry[];\n errorPatterns: Record<string, { count: number; lastSeen: string; resolution?: string }>;\n userCorrections: Array<{ original: string; correction: string; timestamp: string }>;\n conversationInsights: Array<{ topic: string; outcome: string; toolsUsed: string[]; timestamp: string }>;\n}\n\n/**\n * v5.5.11: mtime-validated cache. Previously this was a bare `let kb` that\n * never reloaded from disk, so external writes (admin script, agent file-\n * write tool, autopilot) were invisible to the live gateway until restart.\n * Reproduced 2026-05-07: a one-off node script wrote pruned errorPatterns\n * to knowledge.json; the gateway kept reporting the stale 139-pattern\n * count via the curiosity drive snapshot until manual systemctl restart.\n *\n * mtimeCache stat()s the file on every read; if the mtime advanced since\n * the last load, it reloads from disk. Mutations performed on the returned\n * object are visible to subsequent reads (same Map reference) and persisted\n * by doSave() which then calls cache.set() to update mtime tracking.\n *\n * Note: there is a microsecond-scale race where external write + concurrent\n * local mutation could lose the local mutation. That trade-off is worth it\n * — the previous behaviour silently served stale data indefinitely.\n */\nconst kbCache = createMtimeCache<KnowledgeBase>({\n path: KNOWLEDGE_FILE,\n parse: (raw) => {\n const parsed = JSON.parse(raw) as KnowledgeBase;\n // Backfill: older knowledge.json files may be missing newer fields.\n parsed.entries = parsed.entries || [];\n parsed.toolSuccessRates = parsed.toolSuccessRates || {};\n parsed.toolPreferencesByType = parsed.toolPreferencesByType || {};\n parsed.strategies = parsed.strategies || [];\n parsed.errorPatterns = parsed.errorPatterns || {};\n parsed.userCorrections = parsed.userCorrections || [];\n parsed.conversationInsights = parsed.conversationInsights || [];\n return parsed;\n },\n initial: () => createEmptyKB(),\n component: COMPONENT,\n});\n\nlet dirty = false;\n\n// ── Strategy hint caches ──────────────────────────────────────────\nlet successfulStrategiesCache: StrategyEntry[] | null = null;\nlet lastDecayRun = 0;\nconst DECAY_INTERVAL_MS = 3600_000; // 1 hour\nconst patternWordsCache = new Map<string, Set<string>>();\n\nfunction invalidateStrategyCaches(): void {\n successfulStrategiesCache = null;\n}\n\nfunction getPatternWords(pattern: string): Set<string> {\n let words = patternWordsCache.get(pattern);\n if (!words) {\n words = new Set(pattern.toLowerCase().split(/\\s+/).filter(w => w.length > 3));\n patternWordsCache.set(pattern, words);\n }\n return words;\n}\n\n// v5.5.11: mtime-validated. Synchronous — fast cache hit (just a stat() call)\n// in the common case, full re-parse only when external write detected.\nfunction loadKnowledgeBase(): KnowledgeBase {\n mkdirIfNotExists(TITAN_HOME);\n return kbCache.read();\n}\n\nfunction createEmptyKB(): KnowledgeBase {\n return {\n entries: [],\n toolSuccessRates: {},\n toolPreferencesByType: {},\n strategies: [],\n errorPatterns: {},\n userCorrections: [],\n conversationInsights: [],\n };\n}\n\nfunction doSave(): void {\n const kb = kbCache.read();\n mkdirIfNotExists(TITAN_HOME);\n try {\n const tmpFile = KNOWLEDGE_FILE + '.tmp';\n writeFileSync(tmpFile, JSON.stringify(kb, null, 2), 'utf-8');\n renameSync(tmpFile, KNOWLEDGE_FILE);\n // v5.5.11: bump our recorded mtime so we don't false-positive on our own write next read.\n kbCache.set(kb, 'doSave');\n dirty = false;\n } catch (err) {\n dirty = true;\n logger.error(COMPONENT, `Failed to save knowledge base: ${(err as Error).message}`);\n }\n}\n\nlet saveTimeout: ReturnType<typeof setTimeout> | null = null;\nfunction debouncedSave(): void {\n if (dirty) { doSave(); return; }\n if (saveTimeout) clearTimeout(saveTimeout);\n saveTimeout = setTimeout(doSave, 2000);\n saveTimeout.unref();\n}\n\n/** Initialize the learning engine */\nexport function initLearning(): void {\n const kb = loadKnowledgeBase();\n logger.info(COMPONENT, `Learning engine initialized (${kb.entries.length} knowledge entries)`);\n}\n\n/**\n * v4.9.0-local.9: detect \"error\" strings that are actually file content\n * echoed back as context (common when build tools dump failing source).\n * The pre-.9 behavior was to slice(0, 200) and dedupe on that prefix,\n * which made the same file show up many times and inflated Curiosity's\n * \"unresolved patterns\" count by ~5-10x. Observed on 2026-04-18: 327\n * total patterns, of which >50% were just slices of titan-saas source.\n *\n * Heuristic: if the string looks like source code being printed back,\n * don't record it as a distinct error pattern. Specifically:\n * - starts with \"File: /path/to/...\" (compiler output convention)\n * - contains \"--- 1:\" or \"--- 2:\" etc. (line-numbered code dumps)\n * - is >1200 chars (real error strings are usually short; anything\n * longer is almost always dumped source)\n * - is an entire TypeScript/JS import block (≥3 `import ` lines)\n *\n * When detected, we record a GENERIC rollup pattern (\"build-dumped-source:\n * <basename>\") instead of the raw content, so Curiosity sees one entry per\n * file instead of N copies of the same file's content.\n */\nfunction classifyErrorPattern(error: string): { pattern: string; isFileDump: boolean } {\n const trimmed = error.trim();\n const basename = (m: RegExpMatchArray | null): string => {\n const path = m?.[1] ?? '';\n return path.split('/').pop() ?? 'unknown';\n };\n\n // \"File: /path/to/foo.ts (N lines) --- ...\"\n const fileHeaderMatch = trimmed.match(/^File:\\s+(\\S+)\\s+\\(\\d+\\s+lines\\)/);\n if (fileHeaderMatch) {\n return { pattern: `build-dumped-source:${basename(fileHeaderMatch)}`, isFileDump: true };\n }\n\n // Multi-line numbered code dump (\"--- 1: ... --- 2: ...\")\n if (/---\\s*\\d+:/.test(trimmed) && /---\\s*2:/.test(trimmed)) {\n return { pattern: 'build-dumped-source:numbered-code-block', isFileDump: true };\n }\n\n // Import blocks (TypeScript/JS) — ≥3 import lines → probably source\n const importLines = (trimmed.match(/^import\\s+/gm) || []).length;\n if (importLines >= 3) {\n return { pattern: 'build-dumped-source:import-block', isFileDump: true };\n }\n\n // v5.5.8: Next.js build output — \"▲ Next.js X.Y.Z ... Compiled successfully ...\"\n // Was being recorded as 100s of \"error patterns\" because builds dump to stderr\n // even on success. Roll up to a single canonical entry.\n if (/▲\\s+Next\\.js\\s+[\\d.]+|Creating an optimized production build|Linting and checking validity/.test(trimmed)) {\n return { pattern: 'build-noise:nextjs-output', isFileDump: true };\n }\n\n // v5.5.8: Vitest assertion failures — \"expected X to be Y // Object.is equality\"\n // These are stale test failures from past runs, not recurring runtime errors.\n // Roll up by assertion shape rather than specific values.\n const vitestMatch = trimmed.match(/^expected\\s+.+\\s+to\\s+(be|deeply equal|have|contain|match)\\b/);\n if (vitestMatch) {\n return { pattern: `test-noise:vitest-assertion:${vitestMatch[1]}`, isFileDump: true };\n }\n\n // v5.5.8: bare \"build-dumped-source:...\" lines (already-classified entries\n // that got re-recorded as errors). Treat as same canonical pattern.\n if (trimmed.startsWith('build-dumped-source:')) {\n return { pattern: trimmed.split('\\n')[0].slice(0, 200), isFileDump: true };\n }\n\n // Too long to be a useful signature\n if (trimmed.length > 1200) {\n return { pattern: 'oversized-error:' + trimmed.slice(0, 60).replace(/\\s+/g, ' '), isFileDump: true };\n }\n\n // Normal short error — keep the original 200-char slice behavior\n return { pattern: trimmed.slice(0, 200), isFileDump: false };\n}\n\n/** Record a tool execution result for learning */\nexport function recordToolResult(toolName: string, success: boolean, context?: string, error?: string): void {\n const k = loadKnowledgeBase();\n\n // Update tool success rates\n if (!k.toolSuccessRates[toolName]) {\n k.toolSuccessRates[toolName] = { success: 0, fail: 0, total: 0 };\n }\n k.toolSuccessRates[toolName].total++;\n if (success) {\n k.toolSuccessRates[toolName].success++;\n } else {\n k.toolSuccessRates[toolName].fail++;\n // Track error patterns — with file-content detection (v4.9.0-local.9)\n if (error) {\n const { pattern } = classifyErrorPattern(error);\n if (!k.errorPatterns[pattern]) {\n k.errorPatterns[pattern] = { count: 0, lastSeen: '' };\n }\n k.errorPatterns[pattern].count++;\n k.errorPatterns[pattern].lastSeen = new Date().toISOString();\n }\n }\n\n debouncedSave();\n}\n\n// Exposed for tests + one-time migration tooling\nexport { classifyErrorPattern };\n\n/** Record a successful interaction pattern */\nexport function recordSuccessPattern(pattern: {\n topic: string;\n toolsUsed: string[];\n outcome: string;\n}): void {\n const k = loadKnowledgeBase();\n k.conversationInsights.push({\n ...pattern,\n timestamp: new Date().toISOString(),\n });\n // Keep last 500 insights\n if (k.conversationInsights.length > 500) {\n k.conversationInsights = k.conversationInsights.slice(-500);\n }\n debouncedSave();\n}\n\n/** Record a user correction to learn from mistakes */\nexport function recordUserCorrection(original: string, correction: string): void {\n const k = loadKnowledgeBase();\n k.userCorrections.push({\n original,\n correction,\n timestamp: new Date().toISOString(),\n });\n if (k.userCorrections.length > 200) {\n k.userCorrections = k.userCorrections.slice(-200);\n }\n debouncedSave();\n}\n\n/** Learn a new fact or update an existing one */\nexport function learnFact(category: string, content: string, context?: string): void {\n const k = loadKnowledgeBase();\n const id = `${category}:${content.slice(0, 50)}`;\n const existing = k.entries.findIndex((e) => e.id === id);\n\n if (existing >= 0) {\n k.entries[existing].score = Math.min(k.entries[existing].score + 0.1, 1.0);\n k.entries[existing].accessCount++;\n k.entries[existing].updatedAt = new Date().toISOString();\n } else {\n k.entries.push({\n id,\n type: 'fact',\n category,\n content,\n context,\n score: 0.5,\n accessCount: 0,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n });\n }\n // Keep entries under 2000\n if (k.entries.length > 2000) {\n // Remove lowest-scored entries\n k.entries.sort((a, b) => b.score - a.score);\n k.entries = k.entries.slice(0, 1500);\n }\n debouncedSave();\n}\n\n/** Query the knowledge base for relevant information */\nexport function queryKnowledge(query: string, category?: string, limit: number = 10): LearningEntry[] {\n const k = loadKnowledgeBase();\n const q = query.toLowerCase();\n\n const results = k.entries.filter((e) => {\n const matchesQuery = e.content.toLowerCase().includes(q) ||\n e.category.toLowerCase().includes(q) ||\n (e.context && e.context.toLowerCase().includes(q));\n const matchesCategory = !category || e.category === category;\n return matchesQuery && matchesCategory;\n });\n\n // Sort by score (most useful first), then by recency\n results.sort((a, b) => {\n if (b.score !== a.score) return b.score - a.score;\n return b.updatedAt.localeCompare(a.updatedAt);\n });\n\n // Boost access count for retrieved entries\n for (const entry of results.slice(0, limit)) {\n entry.accessCount++;\n entry.score = Math.min(entry.score + 0.01, 1.0);\n }\n\n debouncedSave();\n return results.slice(0, limit);\n}\n\n/** Get tool recommendations based on historical success rates */\nexport function getToolRecommendations(): Record<string, number> {\n const k = loadKnowledgeBase();\n const recommendations: Record<string, number> = {};\n\n for (const [tool, stats] of Object.entries(k.toolSuccessRates)) {\n if (stats.total > 0) {\n recommendations[tool] = stats.success / stats.total;\n }\n }\n\n return recommendations;\n}\n\n/**\n * TITAN pattern: Memory staleness verification\n * Before acting on learned knowledge, verify it's still current.\n * Strategies unvalidated for 30+ days lose 20% successCount.\n * Knowledge entries older than 60 days get flagged as potentially stale.\n */\nexport function verifyMemoryStaleness(): { pruned: number; decayed: number } {\n const k = loadKnowledgeBase();\n const now = Date.now();\n const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000;\n const SIXTY_DAYS = 60 * 24 * 60 * 60 * 1000;\n let pruned = 0;\n let decayed = 0;\n\n // Decay strategies unvalidated for 30+ days\n for (const strategy of k.strategies) {\n const lastValidated = strategy.lastValidated ? new Date(strategy.lastValidated).getTime() : new Date(strategy.timestamp).getTime();\n if (now - lastValidated > THIRTY_DAYS && (strategy.successCount || 0) > 0) {\n strategy.successCount = Math.floor((strategy.successCount || 0) * 0.8);\n decayed++;\n }\n }\n\n // Remove knowledge entries older than 60 days with low scores\n const before = k.entries.length;\n k.entries = k.entries.filter(e => {\n const age = now - new Date(e.createdAt).getTime();\n return age < SIXTY_DAYS || e.score > 0.7;\n });\n pruned = before - k.entries.length;\n\n // Remove error patterns not seen in 30+ days\n // v5.5.8: ALSO remove unresolved patterns not seen in 7+ days. These have\n // stopped recurring (no count increment for a week) so they're stale signal,\n // not a current problem. Was inflating Curiosity's `unresolvedErrorPatterns`\n // count to 139 with build-noise/test-noise from 2-3 weeks ago.\n const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n const ageMs = now - new Date(info.lastSeen).getTime();\n if (ageMs > THIRTY_DAYS) {\n delete k.errorPatterns[pattern];\n pruned++;\n } else if (ageMs > SEVEN_DAYS && !info.resolution) {\n // Unresolved + dormant ≥7d → stale, not real\n delete k.errorPatterns[pattern];\n pruned++;\n }\n }\n\n if (pruned > 0 || decayed > 0) {\n doSave();\n logger.info(COMPONENT, `Memory staleness check: ${pruned} pruned, ${decayed} strategies decayed`);\n }\n\n return { pruned, decayed };\n}\n\n/**\n * v4.9.0-local.9: one-time cleanup of \"error patterns\" that are actually\n * file content dumps. Retroactively applies the same classifyErrorPattern\n * logic that now runs on record. Collapses dozens of sliced file-content\n * entries into a single rollup per file.\n *\n * Returns { removed, collapsedInto } where `removed` is the number of raw\n * entries deleted and `collapsedInto` is how many rollup entries were\n * produced (usually much smaller).\n */\nexport function pruneFileContentErrorPatterns(): { removed: number; collapsedInto: number } {\n const k = loadKnowledgeBase();\n const rollup = new Map<string, { count: number; lastSeen: string; resolution?: string }>();\n const keep: Record<string, { count: number; lastSeen: string; resolution?: string }> = {};\n let removed = 0;\n\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n const { pattern: canonical, isFileDump } = classifyErrorPattern(pattern);\n if (isFileDump) {\n // Collapse into the rollup bucket\n const existing = rollup.get(canonical);\n if (existing) {\n existing.count += info.count;\n if (info.lastSeen > existing.lastSeen) existing.lastSeen = info.lastSeen;\n if (!existing.resolution && info.resolution) existing.resolution = info.resolution;\n } else {\n rollup.set(canonical, { count: info.count, lastSeen: info.lastSeen, resolution: info.resolution });\n }\n removed++;\n } else {\n keep[pattern] = info;\n }\n }\n // Merge rollups back in (they may collide with real entries — count-sum if so)\n for (const [canonical, info] of rollup) {\n if (keep[canonical]) {\n keep[canonical].count += info.count;\n if (info.lastSeen > keep[canonical].lastSeen) keep[canonical].lastSeen = info.lastSeen;\n } else {\n keep[canonical] = info;\n }\n }\n k.errorPatterns = keep;\n doSave();\n logger.info(COMPONENT, `[PrunePatterns] removed ${removed} file-content entries, collapsed into ${rollup.size} rollup entries`);\n return { removed, collapsedInto: rollup.size };\n}\n\n/** Get learning summary for the system prompt */\nexport function getLearningContext(): string {\n const k = loadKnowledgeBase();\n const parts: string[] = [];\n\n // Recent high-score knowledge\n const topEntries = k.entries\n .filter((e) => e.score > 0.6)\n .sort((a, b) => b.score - a.score)\n .slice(0, 10);\n\n if (topEntries.length > 0) {\n parts.push('Key learned facts:');\n for (const e of topEntries) {\n parts.push(`- [${e.category}] ${e.content}`);\n }\n }\n\n // Tool recommendations\n const toolRecs = getToolRecommendations();\n const bestTools = Object.entries(toolRecs)\n .filter(([_, rate]) => rate > 0.8)\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5);\n\n if (bestTools.length > 0) {\n parts.push('\\nMost reliable tools:');\n for (const [tool, rate] of bestTools) {\n parts.push(`- ${tool}: ${Math.round(rate * 100)}% success rate`);\n }\n }\n\n // Common error patterns to avoid\n const frequentErrors = Object.entries(k.errorPatterns)\n .filter(([_, info]) => info.count > 2)\n .sort((a, b) => b[1].count - a[1].count)\n .slice(0, 3);\n\n if (frequentErrors.length > 0) {\n parts.push('\\nCommon errors to avoid:');\n for (const [pattern, info] of frequentErrors) {\n parts.push(`- ${pattern.slice(0, 100)} (seen ${info.count}x)${info.resolution ? ` → Fix: ${info.resolution}` : ''}`);\n }\n }\n\n return parts.join('\\n');\n}\n\n/** Get per-tool reliability warnings/boosts for tool description injection */\nexport function getToolWarnings(): Record<string, string> {\n const k = loadKnowledgeBase();\n const warnings: Record<string, string> = {};\n\n for (const [tool, stats] of Object.entries(k.toolSuccessRates)) {\n if (stats.total < 10) continue; // Need enough data to be meaningful\n const rate = stats.success / stats.total;\n if (rate < 0.3) {\n warnings[tool] = `[LOW RELIABILITY: ${Math.round(rate * 100)}% success rate over ${stats.total} uses]`;\n } else if (rate > 0.9) {\n warnings[tool] = `[HIGHLY RELIABLE: ${Math.round(rate * 100)}% success rate]`;\n }\n }\n\n return warnings;\n}\n\n/** Record when a tool failure is resolved by using a different tool */\nexport function recordErrorResolution(errorPattern: string, resolution: string): void {\n const k = loadKnowledgeBase();\n const pattern = errorPattern.slice(0, 200);\n if (k.errorPatterns[pattern]) {\n k.errorPatterns[pattern].resolution = resolution;\n } else {\n k.errorPatterns[pattern] = { count: 1, lastSeen: new Date().toISOString(), resolution };\n }\n debouncedSave();\n}\n\n/** Get stats about the learning system */\nexport function getLearningStats(): {\n knowledgeEntries: number;\n toolsTracked: number;\n errorPatterns: number;\n /**\n * v4.10.0-local fix: true count of UNRESOLVED patterns, used by the\n * curiosity drive. Prior behavior was to report `errorPatterns` (total)\n * as if it were unresolved — meaning marking patterns as resolved did\n * nothing to the drive signal. Now separate field.\n */\n unresolvedErrorPatterns: number;\n corrections: number;\n insights: number;\n strategies: number;\n taskTypes: number;\n} {\n const k = loadKnowledgeBase();\n const unresolvedErrorPatterns = Object.values(k.errorPatterns).filter(v => !v.resolution).length;\n return {\n knowledgeEntries: k.entries.length,\n toolsTracked: Object.keys(k.toolSuccessRates).length,\n errorPatterns: Object.keys(k.errorPatterns).length,\n unresolvedErrorPatterns,\n corrections: k.userCorrections.length,\n insights: k.conversationInsights.length,\n strategies: k.strategies.length,\n taskTypes: Object.keys(k.toolPreferencesByType).length,\n };\n}\n\n// ── Phase 4: Active Learning ──────────────────────────────────────\n\n/** Classify a message into a task type for preference tracking */\nexport function classifyTaskType(message: string): string {\n const lower = message.toLowerCase();\n if (/\\b(code|function|class|typescript|python|script|debug|compile|build)\\b/.test(lower)) return 'coding';\n if (/\\b(search|research|find|look up|investigate|compare)\\b/.test(lower)) return 'research';\n if (/\\b(analy[sz]e|data|csv|chart|graph|statistics|metrics)\\b/.test(lower)) return 'analysis';\n if (/\\b(write|draft|blog|article|email|message|story)\\b/.test(lower)) return 'writing';\n if (/\\b(deploy|server|docker|kubernetes|ci|cd|infrastructure)\\b/.test(lower)) return 'devops';\n if (/\\b(file|folder|directory|rename|move|copy|delete)\\b/.test(lower)) return 'filesystem';\n if (/\\b(schedule|cron|automat|workflow|remind)\\b/.test(lower)) return 'automation';\n return 'general';\n}\n\n/** Record a tool result against a specific task type */\nexport function recordToolPreference(toolName: string, taskType: string, success: boolean): void {\n const k = loadKnowledgeBase();\n if (!k.toolPreferencesByType[taskType]) {\n k.toolPreferencesByType[taskType] = {};\n }\n if (!k.toolPreferencesByType[taskType][toolName]) {\n k.toolPreferencesByType[taskType][toolName] = { success: 0, total: 0 };\n }\n\n k.toolPreferencesByType[taskType][toolName].total++;\n if (success) k.toolPreferencesByType[taskType][toolName].success++;\n debouncedSave();\n}\n\n/** Get ranked tool preferences for a task type */\nexport function getToolPreferences(taskType: string): ToolPreference[] {\n const k = loadKnowledgeBase();\n const prefs = k.toolPreferencesByType[taskType];\n if (!prefs) return [];\n\n return Object.entries(prefs)\n .filter(([, stats]) => stats.total >= 3) // Minimum sample size\n .map(([tool, stats]) => ({\n tool,\n successRate: stats.success / stats.total,\n totalUses: stats.total,\n }))\n .sort((a, b) => b.successRate - a.successRate);\n}\n\n/** Generate human-readable preference hints from collected tool-preference data */\nexport function getLearnedPreferenceHints(taskType: string): string | null {\n const prefs = getToolPreferences(taskType);\n if (prefs.length < 2) return null;\n\n const lines: string[] = [];\n const FILE_TOOLS = ['read_file', 'write_file', 'edit_file', 'list_dir'];\n const shellPref = prefs.find(p => p.tool === 'shell');\n\n // Find cases where shell has lower success than dedicated tools\n for (const dt of prefs.filter(p => FILE_TOOLS.includes(p.tool))) {\n if (shellPref && dt.successRate > shellPref.successRate && dt.totalUses >= 3) {\n lines.push(\n `prefer ${dt.tool} (${Math.round(dt.successRate * 100)}% success) ` +\n `over shell (${Math.round(shellPref.successRate * 100)}% success)`,\n );\n }\n }\n\n // Surface top tools for this task type if no shell comparison available\n if (lines.length === 0) {\n const top3 = prefs.slice(0, 3);\n if (top3.length >= 2) {\n lines.push(`best tools: ${top3.map(p => `${p.tool} (${Math.round(p.successRate * 100)}%)`).join(', ')}`);\n }\n }\n\n return lines.length > 0 ? `For ${taskType} tasks: ${lines.join('; ')}` : null;\n}\n\n/** Record a successful strategy for future reference */\nexport function recordStrategy(\n message: string,\n toolsUsed: string[],\n roundCount: number,\n success: boolean,\n toolSequence?: string[],\n): void {\n const k = loadKnowledgeBase();\n const taskType = classifyTaskType(message);\n\n // Check if a similar sequence already exists — merge instead of duplicating\n if (success && toolSequence && toolSequence.length > 0) {\n const seqKey = toolSequence.join('→');\n const existing = k.strategies.find(\n s => s.success && s.taskType === taskType && s.toolSequence?.join('→') === seqKey,\n );\n if (existing) {\n existing.successCount = (existing.successCount || 1) + 1;\n existing.timestamp = new Date().toISOString();\n debouncedSave();\n return;\n }\n }\n\n const entry: StrategyEntry = {\n pattern: message.slice(0, 200),\n toolsUsed: [...new Set(toolsUsed)],\n toolSequence: toolSequence?.slice(0, 20), // Cap sequence length\n taskType,\n roundCount,\n success,\n successCount: success ? 1 : 0,\n timestamp: new Date().toISOString(),\n };\n\n k.strategies.push(entry);\n invalidateStrategyCaches();\n\n // Evict old/failed strategies to keep at 200 max\n if (k.strategies.length > 200) {\n // Sort: keep high-success + recent, evict failed + old + low-success\n k.strategies.sort((a, b) => {\n if (a.success !== b.success) return a.success ? -1 : 1;\n const aCount = a.successCount || 1;\n const bCount = b.successCount || 1;\n if (aCount !== bCount) return bCount - aCount;\n return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();\n });\n k.strategies = k.strategies.slice(0, 200);\n }\n\n debouncedSave();\n}\n\n/** Record outcome of an applied strategy (feedback loop) */\nexport function recordStrategyOutcome(\n taskType: string,\n toolSequence: string[],\n succeeded: boolean,\n): void {\n const k = loadKnowledgeBase();\n const seqKey = toolSequence.join('→');\n const match = k.strategies.find(\n s => s.taskType === taskType && s.toolSequence?.join('→') === seqKey,\n );\n if (!match) return;\n\n if (succeeded) {\n match.successCount = (match.successCount || 1) + 1;\n match.lastValidated = new Date().toISOString();\n } else {\n match.failCount = (match.failCount || 0) + 1;\n }\n\n // High-fail strategies get marked as unsuccessful\n const becameUnsuccessful = (match.failCount || 0) > (match.successCount || 1) && match.success;\n if (becameUnsuccessful) {\n match.success = false;\n }\n\n if (becameUnsuccessful) invalidateStrategyCaches();\n debouncedSave();\n}\n\n/** Decay unvalidated strategies — called on every getStrategyHints */\nfunction decayStrategies(strategies: StrategyEntry[]): boolean {\n const now = Date.now();\n let changed = false;\n\n for (const s of strategies) {\n const lastValidated = s.lastValidated\n ? new Date(s.lastValidated).getTime()\n : new Date(s.timestamp).getTime();\n const daysSinceValidated = (now - lastValidated) / 86400000;\n\n // Strategies not validated in 30 days: reduce successCount by 20%\n if (daysSinceValidated > 30 && (s.successCount || 1) > 1) {\n s.successCount = Math.max(1, Math.floor((s.successCount || 1) * 0.8));\n changed = true;\n }\n\n // High-fail strategies get excluded\n if ((s.failCount || 0) > (s.successCount || 1) && s.success) {\n s.success = false;\n changed = true;\n }\n }\n\n return changed;\n}\n\n/** Get strategy hints for a similar task */\nexport function getStrategyHints(message: string): string | null {\n const k = loadKnowledgeBase();\n\n // Apply decay to stale strategies\n const now = Date.now();\n if (now - lastDecayRun > DECAY_INTERVAL_MS) {\n lastDecayRun = now;\n if (decayStrategies(k.strategies)) {\n debouncedSave();\n invalidateStrategyCaches();\n }\n }\n if (k.strategies.length === 0) return null;\n\n const taskType = classifyTaskType(message);\n const words = new Set(message.toLowerCase().split(/\\s+/).filter(w => w.length > 3));\n if (words.size === 0) return null;\n\n const successfulStrategies = successfulStrategiesCache ?? (successfulStrategiesCache = k.strategies.filter(s => s.success));\n if (successfulStrategies.length === 0) return null;\n\n // Score strategies by: task type match + keyword overlap + success count\n let bestMatch: StrategyEntry | null = null;\n let bestScore = 0;\n\n for (const strategy of successfulStrategies) {\n const patternWords = getPatternWords(strategy.pattern);\n let overlap = 0;\n for (const w of words) {\n if (patternWords.has(w)) overlap++;\n }\n const keywordScore = overlap / Math.max(words.size, patternWords.size);\n\n // Task type match bonus (0.2)\n const typeBonus = strategy.taskType === taskType ? 0.2 : 0;\n\n // Success count bonus (normalized, max 0.15)\n const countBonus = Math.min((strategy.successCount || 1) / 10, 0.15);\n\n const totalScore = keywordScore + typeBonus + countBonus;\n if (totalScore > bestScore && keywordScore > 0.15) {\n bestScore = totalScore;\n bestMatch = strategy;\n }\n }\n\n if (!bestMatch) return null;\n\n // Prefer showing the ordered sequence if available\n const toolInfo = bestMatch.toolSequence && bestMatch.toolSequence.length > 0\n ? bestMatch.toolSequence.join(' → ')\n : bestMatch.toolsUsed.join(', ');\n\n const countInfo = (bestMatch.successCount || 1) > 1\n ? ` (succeeded ${bestMatch.successCount}x)`\n : '';\n\n return `For similar ${bestMatch.taskType || 'general'} tasks, a proven tool sequence: ${toolInfo} (${bestMatch.roundCount} rounds)${countInfo}.`;\n}\n\n/** Get error resolution if a known pattern matches */\nexport function getErrorResolution(error: string): string | null {\n const k = loadKnowledgeBase();\n const errorLower = error.toLowerCase();\n\n for (const [pattern, info] of Object.entries(k.errorPatterns)) {\n if (info.resolution && errorLower.includes(pattern.toLowerCase().slice(0, 50))) {\n return info.resolution;\n }\n }\n return null;\n}\n"],"mappings":";AAKA,SAAS,eAAe,kBAAkB;AAC1C,SAAS,YAAY;AACrB,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AACjC,SAAS,wBAAwB;AACjC,OAAO,YAAY;AAEnB,MAAM,YAAY;AAClB,MAAM,iBAAiB,KAAK,YAAY,gBAAgB;AAgExD,MAAM,UAAU,iBAAgC;AAAA,EAC5C,MAAM;AAAA,EACN,OAAO,CAAC,QAAQ;AACZ,UAAM,SAAS,KAAK,MAAM,GAAG;AAE7B,WAAO,UAAU,OAAO,WAAW,CAAC;AACpC,WAAO,mBAAmB,OAAO,oBAAoB,CAAC;AACtD,WAAO,wBAAwB,OAAO,yBAAyB,CAAC;AAChE,WAAO,aAAa,OAAO,cAAc,CAAC;AAC1C,WAAO,gBAAgB,OAAO,iBAAiB,CAAC;AAChD,WAAO,kBAAkB,OAAO,mBAAmB,CAAC;AACpD,WAAO,uBAAuB,OAAO,wBAAwB,CAAC;AAC9D,WAAO;AAAA,EACX;AAAA,EACA,SAAS,MAAM,cAAc;AAAA,EAC7B,WAAW;AACf,CAAC;AAED,IAAI,QAAQ;AAGZ,IAAI,4BAAoD;AACxD,IAAI,eAAe;AACnB,MAAM,oBAAoB;AAC1B,MAAM,oBAAoB,oBAAI,IAAyB;AAEvD,SAAS,2BAAiC;AACtC,8BAA4B;AAChC;AAEA,SAAS,gBAAgB,SAA8B;AACnD,MAAI,QAAQ,kBAAkB,IAAI,OAAO;AACzC,MAAI,CAAC,OAAO;AACR,YAAQ,IAAI,IAAI,QAAQ,YAAY,EAAE,MAAM,KAAK,EAAE,OAAO,OAAK,EAAE,SAAS,CAAC,CAAC;AAC5E,sBAAkB,IAAI,SAAS,KAAK;AAAA,EACxC;AACA,SAAO;AACX;AAIA,SAAS,oBAAmC;AACxC,mBAAiB,UAAU;AAC3B,SAAO,QAAQ,KAAK;AACxB;AAEA,SAAS,gBAA+B;AACpC,SAAO;AAAA,IACH,SAAS,CAAC;AAAA,IACV,kBAAkB,CAAC;AAAA,IACnB,uBAAuB,CAAC;AAAA,IACxB,YAAY,CAAC;AAAA,IACb,eAAe,CAAC;AAAA,IAChB,iBAAiB,CAAC;AAAA,IAClB,sBAAsB,CAAC;AAAA,EAC3B;AACJ;AAEA,SAAS,SAAe;AACpB,QAAM,KAAK,QAAQ,KAAK;AACxB,mBAAiB,UAAU;AAC3B,MAAI;AACA,UAAM,UAAU,iBAAiB;AACjC,kBAAc,SAAS,KAAK,UAAU,IAAI,MAAM,CAAC,GAAG,OAAO;AAC3D,eAAW,SAAS,cAAc;AAElC,YAAQ,IAAI,IAAI,QAAQ;AACxB,YAAQ;AAAA,EACZ,SAAS,KAAK;AACV,YAAQ;AACR,WAAO,MAAM,WAAW,kCAAmC,IAAc,OAAO,EAAE;AAAA,EACtF;AACJ;AAEA,IAAI,cAAoD;AACxD,SAAS,gBAAsB;AAC3B,MAAI,OAAO;AAAE,WAAO;AAAG;AAAA,EAAQ;AAC/B,MAAI,YAAa,cAAa,WAAW;AACzC,gBAAc,WAAW,QAAQ,GAAI;AACrC,cAAY,MAAM;AACtB;AAGO,SAAS,eAAqB;AACjC,QAAM,KAAK,kBAAkB;AAC7B,SAAO,KAAK,WAAW,gCAAgC,GAAG,QAAQ,MAAM,qBAAqB;AACjG;AAsBA,SAAS,qBAAqB,OAAyD;AACnF,QAAM,UAAU,MAAM,KAAK;AAC3B,QAAM,WAAW,CAAC,MAAuC;AACrD,UAAM,OAAO,IAAI,CAAC,KAAK;AACvB,WAAO,KAAK,MAAM,GAAG,EAAE,IAAI,KAAK;AAAA,EACpC;AAGA,QAAM,kBAAkB,QAAQ,MAAM,kCAAkC;AACxE,MAAI,iBAAiB;AACjB,WAAO,EAAE,SAAS,uBAAuB,SAAS,eAAe,CAAC,IAAI,YAAY,KAAK;AAAA,EAC3F;AAGA,MAAI,aAAa,KAAK,OAAO,KAAK,WAAW,KAAK,OAAO,GAAG;AACxD,WAAO,EAAE,SAAS,2CAA2C,YAAY,KAAK;AAAA,EAClF;AAGA,QAAM,eAAe,QAAQ,MAAM,cAAc,KAAK,CAAC,GAAG;AAC1D,MAAI,eAAe,GAAG;AAClB,WAAO,EAAE,SAAS,oCAAoC,YAAY,KAAK;AAAA,EAC3E;AAKA,MAAI,6FAA6F,KAAK,OAAO,GAAG;AAC5G,WAAO,EAAE,SAAS,6BAA6B,YAAY,KAAK;AAAA,EACpE;AAKA,QAAM,cAAc,QAAQ,MAAM,8DAA8D;AAChG,MAAI,aAAa;AACb,WAAO,EAAE,SAAS,+BAA+B,YAAY,CAAC,CAAC,IAAI,YAAY,KAAK;AAAA,EACxF;AAIA,MAAI,QAAQ,WAAW,sBAAsB,GAAG;AAC5C,WAAO,EAAE,SAAS,QAAQ,MAAM,IAAI,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK;AAAA,EAC7E;AAGA,MAAI,QAAQ,SAAS,MAAM;AACvB,WAAO,EAAE,SAAS,qBAAqB,QAAQ,MAAM,GAAG,EAAE,EAAE,QAAQ,QAAQ,GAAG,GAAG,YAAY,KAAK;AAAA,EACvG;AAGA,SAAO,EAAE,SAAS,QAAQ,MAAM,GAAG,GAAG,GAAG,YAAY,MAAM;AAC/D;AAGO,SAAS,iBAAiB,UAAkB,SAAkB,SAAkB,OAAsB;AACzG,QAAM,IAAI,kBAAkB;AAG5B,MAAI,CAAC,EAAE,iBAAiB,QAAQ,GAAG;AAC/B,MAAE,iBAAiB,QAAQ,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,EAAE;AAAA,EACnE;AACA,IAAE,iBAAiB,QAAQ,EAAE;AAC7B,MAAI,SAAS;AACT,MAAE,iBAAiB,QAAQ,EAAE;AAAA,EACjC,OAAO;AACH,MAAE,iBAAiB,QAAQ,EAAE;AAE7B,QAAI,OAAO;AACP,YAAM,EAAE,QAAQ,IAAI,qBAAqB,KAAK;AAC9C,UAAI,CAAC,EAAE,cAAc,OAAO,GAAG;AAC3B,UAAE,cAAc,OAAO,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG;AAAA,MACxD;AACA,QAAE,cAAc,OAAO,EAAE;AACzB,QAAE,cAAc,OAAO,EAAE,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAC/D;AAAA,EACJ;AAEA,gBAAc;AAClB;AAMO,SAAS,qBAAqB,SAI5B;AACL,QAAM,IAAI,kBAAkB;AAC5B,IAAE,qBAAqB,KAAK;AAAA,IACxB,GAAG;AAAA,IACH,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,EACtC,CAAC;AAED,MAAI,EAAE,qBAAqB,SAAS,KAAK;AACrC,MAAE,uBAAuB,EAAE,qBAAqB,MAAM,IAAI;AAAA,EAC9D;AACA,gBAAc;AAClB;AAGO,SAAS,qBAAqB,UAAkB,YAA0B;AAC7E,QAAM,IAAI,kBAAkB;AAC5B,IAAE,gBAAgB,KAAK;AAAA,IACnB;AAAA,IACA;AAAA,IACA,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,EACtC,CAAC;AACD,MAAI,EAAE,gBAAgB,SAAS,KAAK;AAChC,MAAE,kBAAkB,EAAE,gBAAgB,MAAM,IAAI;AAAA,EACpD;AACA,gBAAc;AAClB;AAGO,SAAS,UAAU,UAAkB,SAAiB,SAAwB;AACjF,QAAM,IAAI,kBAAkB;AAC5B,QAAM,KAAK,GAAG,QAAQ,IAAI,QAAQ,MAAM,GAAG,EAAE,CAAC;AAC9C,QAAM,WAAW,EAAE,QAAQ,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE;AAEvD,MAAI,YAAY,GAAG;AACf,MAAE,QAAQ,QAAQ,EAAE,QAAQ,KAAK,IAAI,EAAE,QAAQ,QAAQ,EAAE,QAAQ,KAAK,CAAG;AACzE,MAAE,QAAQ,QAAQ,EAAE;AACpB,MAAE,QAAQ,QAAQ,EAAE,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,EAC3D,OAAO;AACH,MAAE,QAAQ,KAAK;AAAA,MACX;AAAA,MACA,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO;AAAA,MACP,aAAa;AAAA,MACb,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACtC,CAAC;AAAA,EACL;AAEA,MAAI,EAAE,QAAQ,SAAS,KAAM;AAEzB,MAAE,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAC1C,MAAE,UAAU,EAAE,QAAQ,MAAM,GAAG,IAAI;AAAA,EACvC;AACA,gBAAc;AAClB;AAGO,SAAS,eAAe,OAAe,UAAmB,QAAgB,IAAqB;AAClG,QAAM,IAAI,kBAAkB;AAC5B,QAAM,IAAI,MAAM,YAAY;AAE5B,QAAM,UAAU,EAAE,QAAQ,OAAO,CAAC,MAAM;AACpC,UAAM,eAAe,EAAE,QAAQ,YAAY,EAAE,SAAS,CAAC,KACnD,EAAE,SAAS,YAAY,EAAE,SAAS,CAAC,KAClC,EAAE,WAAW,EAAE,QAAQ,YAAY,EAAE,SAAS,CAAC;AACpD,UAAM,kBAAkB,CAAC,YAAY,EAAE,aAAa;AACpD,WAAO,gBAAgB;AAAA,EAC3B,CAAC;AAGD,UAAQ,KAAK,CAAC,GAAG,MAAM;AACnB,QAAI,EAAE,UAAU,EAAE,MAAO,QAAO,EAAE,QAAQ,EAAE;AAC5C,WAAO,EAAE,UAAU,cAAc,EAAE,SAAS;AAAA,EAChD,CAAC;AAGD,aAAW,SAAS,QAAQ,MAAM,GAAG,KAAK,GAAG;AACzC,UAAM;AACN,UAAM,QAAQ,KAAK,IAAI,MAAM,QAAQ,MAAM,CAAG;AAAA,EAClD;AAEA,gBAAc;AACd,SAAO,QAAQ,MAAM,GAAG,KAAK;AACjC;AAGO,SAAS,yBAAiD;AAC7D,QAAM,IAAI,kBAAkB;AAC5B,QAAM,kBAA0C,CAAC;AAEjD,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,EAAE,gBAAgB,GAAG;AAC5D,QAAI,MAAM,QAAQ,GAAG;AACjB,sBAAgB,IAAI,IAAI,MAAM,UAAU,MAAM;AAAA,IAClD;AAAA,EACJ;AAEA,SAAO;AACX;AAQO,SAAS,wBAA6D;AACzE,QAAM,IAAI,kBAAkB;AAC5B,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,cAAc,KAAK,KAAK,KAAK,KAAK;AACxC,QAAM,aAAa,KAAK,KAAK,KAAK,KAAK;AACvC,MAAI,SAAS;AACb,MAAI,UAAU;AAGd,aAAW,YAAY,EAAE,YAAY;AACjC,UAAM,gBAAgB,SAAS,gBAAgB,IAAI,KAAK,SAAS,aAAa,EAAE,QAAQ,IAAI,IAAI,KAAK,SAAS,SAAS,EAAE,QAAQ;AACjI,QAAI,MAAM,gBAAgB,gBAAgB,SAAS,gBAAgB,KAAK,GAAG;AACvE,eAAS,eAAe,KAAK,OAAO,SAAS,gBAAgB,KAAK,GAAG;AACrE;AAAA,IACJ;AAAA,EACJ;AAGA,QAAM,SAAS,EAAE,QAAQ;AACzB,IAAE,UAAU,EAAE,QAAQ,OAAO,OAAK;AAC9B,UAAM,MAAM,MAAM,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ;AAChD,WAAO,MAAM,cAAc,EAAE,QAAQ;AAAA,EACzC,CAAC;AACD,WAAS,SAAS,EAAE,QAAQ;AAO5B,QAAM,aAAa,IAAI,KAAK,KAAK,KAAK;AACtC,aAAW,CAAC,SAAS,IAAI,KAAK,OAAO,QAAQ,EAAE,aAAa,GAAG;AAC3D,UAAM,QAAQ,MAAM,IAAI,KAAK,KAAK,QAAQ,EAAE,QAAQ;AACpD,QAAI,QAAQ,aAAa;AACrB,aAAO,EAAE,cAAc,OAAO;AAC9B;AAAA,IACJ,WAAW,QAAQ,cAAc,CAAC,KAAK,YAAY;AAE/C,aAAO,EAAE,cAAc,OAAO;AAC9B;AAAA,IACJ;AAAA,EACJ;AAEA,MAAI,SAAS,KAAK,UAAU,GAAG;AAC3B,WAAO;AACP,WAAO,KAAK,WAAW,2BAA2B,MAAM,YAAY,OAAO,qBAAqB;AAAA,EACpG;AAEA,SAAO,EAAE,QAAQ,QAAQ;AAC7B;AAYO,SAAS,gCAA4E;AACxF,QAAM,IAAI,kBAAkB;AAC5B,QAAM,SAAS,oBAAI,IAAsE;AACzF,QAAM,OAAiF,CAAC;AACxF,MAAI,UAAU;AAEd,aAAW,CAAC,SAAS,IAAI,KAAK,OAAO,QAAQ,EAAE,aAAa,GAAG;AAC3D,UAAM,EAAE,SAAS,WAAW,WAAW,IAAI,qBAAqB,OAAO;AACvE,QAAI,YAAY;AAEZ,YAAM,WAAW,OAAO,IAAI,SAAS;AACrC,UAAI,UAAU;AACV,iBAAS,SAAS,KAAK;AACvB,YAAI,KAAK,WAAW,SAAS,SAAU,UAAS,WAAW,KAAK;AAChE,YAAI,CAAC,SAAS,cAAc,KAAK,WAAY,UAAS,aAAa,KAAK;AAAA,MAC5E,OAAO;AACH,eAAO,IAAI,WAAW,EAAE,OAAO,KAAK,OAAO,UAAU,KAAK,UAAU,YAAY,KAAK,WAAW,CAAC;AAAA,MACrG;AACA;AAAA,IACJ,OAAO;AACH,WAAK,OAAO,IAAI;AAAA,IACpB;AAAA,EACJ;AAEA,aAAW,CAAC,WAAW,IAAI,KAAK,QAAQ;AACpC,QAAI,KAAK,SAAS,GAAG;AACjB,WAAK,SAAS,EAAE,SAAS,KAAK;AAC9B,UAAI,KAAK,WAAW,KAAK,SAAS,EAAE,SAAU,MAAK,SAAS,EAAE,WAAW,KAAK;AAAA,IAClF,OAAO;AACH,WAAK,SAAS,IAAI;AAAA,IACtB;AAAA,EACJ;AACA,IAAE,gBAAgB;AAClB,SAAO;AACP,SAAO,KAAK,WAAW,2BAA2B,OAAO,yCAAyC,OAAO,IAAI,iBAAiB;AAC9H,SAAO,EAAE,SAAS,eAAe,OAAO,KAAK;AACjD;AAGO,SAAS,qBAA6B;AACzC,QAAM,IAAI,kBAAkB;AAC5B,QAAM,QAAkB,CAAC;AAGzB,QAAM,aAAa,EAAE,QAChB,OAAO,CAAC,MAAM,EAAE,QAAQ,GAAG,EAC3B,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,EAChC,MAAM,GAAG,EAAE;AAEhB,MAAI,WAAW,SAAS,GAAG;AACvB,UAAM,KAAK,oBAAoB;AAC/B,eAAW,KAAK,YAAY;AACxB,YAAM,KAAK,MAAM,EAAE,QAAQ,KAAK,EAAE,OAAO,EAAE;AAAA,IAC/C;AAAA,EACJ;AAGA,QAAM,WAAW,uBAAuB;AACxC,QAAM,YAAY,OAAO,QAAQ,QAAQ,EACpC,OAAO,CAAC,CAAC,GAAG,IAAI,MAAM,OAAO,GAAG,EAChC,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,EAC1B,MAAM,GAAG,CAAC;AAEf,MAAI,UAAU,SAAS,GAAG;AACtB,UAAM,KAAK,wBAAwB;AACnC,eAAW,CAAC,MAAM,IAAI,KAAK,WAAW;AAClC,YAAM,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,OAAO,GAAG,CAAC,gBAAgB;AAAA,IACnE;AAAA,EACJ;AAGA,QAAM,iBAAiB,OAAO,QAAQ,EAAE,aAAa,EAChD,OAAO,CAAC,CAAC,GAAG,IAAI,MAAM,KAAK,QAAQ,CAAC,EACpC,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EACtC,MAAM,GAAG,CAAC;AAEf,MAAI,eAAe,SAAS,GAAG;AAC3B,UAAM,KAAK,2BAA2B;AACtC,eAAW,CAAC,SAAS,IAAI,KAAK,gBAAgB;AAC1C,YAAM,KAAK,KAAK,QAAQ,MAAM,GAAG,GAAG,CAAC,UAAU,KAAK,KAAK,KAAK,KAAK,aAAa,gBAAW,KAAK,UAAU,KAAK,EAAE,EAAE;AAAA,IACvH;AAAA,EACJ;AAEA,SAAO,MAAM,KAAK,IAAI;AAC1B;AAGO,SAAS,kBAA0C;AACtD,QAAM,IAAI,kBAAkB;AAC5B,QAAM,WAAmC,CAAC;AAE1C,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,EAAE,gBAAgB,GAAG;AAC5D,QAAI,MAAM,QAAQ,GAAI;AACtB,UAAM,OAAO,MAAM,UAAU,MAAM;AACnC,QAAI,OAAO,KAAK;AACZ,eAAS,IAAI,IAAI,qBAAqB,KAAK,MAAM,OAAO,GAAG,CAAC,uBAAuB,MAAM,KAAK;AAAA,IAClG,WAAW,OAAO,KAAK;AACnB,eAAS,IAAI,IAAI,qBAAqB,KAAK,MAAM,OAAO,GAAG,CAAC;AAAA,IAChE;AAAA,EACJ;AAEA,SAAO;AACX;AAGO,SAAS,sBAAsB,cAAsB,YAA0B;AAClF,QAAM,IAAI,kBAAkB;AAC5B,QAAM,UAAU,aAAa,MAAM,GAAG,GAAG;AACzC,MAAI,EAAE,cAAc,OAAO,GAAG;AAC1B,MAAE,cAAc,OAAO,EAAE,aAAa;AAAA,EAC1C,OAAO;AACH,MAAE,cAAc,OAAO,IAAI,EAAE,OAAO,GAAG,WAAU,oBAAI,KAAK,GAAE,YAAY,GAAG,WAAW;AAAA,EAC1F;AACA,gBAAc;AAClB;AAGO,SAAS,mBAed;AACE,QAAM,IAAI,kBAAkB;AAC5B,QAAM,0BAA0B,OAAO,OAAO,EAAE,aAAa,EAAE,OAAO,OAAK,CAAC,EAAE,UAAU,EAAE;AAC1F,SAAO;AAAA,IACH,kBAAkB,EAAE,QAAQ;AAAA,IAC5B,cAAc,OAAO,KAAK,EAAE,gBAAgB,EAAE;AAAA,IAC9C,eAAe,OAAO,KAAK,EAAE,aAAa,EAAE;AAAA,IAC5C;AAAA,IACA,aAAa,EAAE,gBAAgB;AAAA,IAC/B,UAAU,EAAE,qBAAqB;AAAA,IACjC,YAAY,EAAE,WAAW;AAAA,IACzB,WAAW,OAAO,KAAK,EAAE,qBAAqB,EAAE;AAAA,EACpD;AACJ;AAKO,SAAS,iBAAiB,SAAyB;AACtD,QAAM,QAAQ,QAAQ,YAAY;AAClC,MAAI,yEAAyE,KAAK,KAAK,EAAG,QAAO;AACjG,MAAI,yDAAyD,KAAK,KAAK,EAAG,QAAO;AACjF,MAAI,2DAA2D,KAAK,KAAK,EAAG,QAAO;AACnF,MAAI,qDAAqD,KAAK,KAAK,EAAG,QAAO;AAC7E,MAAI,6DAA6D,KAAK,KAAK,EAAG,QAAO;AACrF,MAAI,sDAAsD,KAAK,KAAK,EAAG,QAAO;AAC9E,MAAI,8CAA8C,KAAK,KAAK,EAAG,QAAO;AACtE,SAAO;AACX;AAGO,SAAS,qBAAqB,UAAkB,UAAkB,SAAwB;AAC7F,QAAM,IAAI,kBAAkB;AAC5B,MAAI,CAAC,EAAE,sBAAsB,QAAQ,GAAG;AACpC,MAAE,sBAAsB,QAAQ,IAAI,CAAC;AAAA,EACzC;AACA,MAAI,CAAC,EAAE,sBAAsB,QAAQ,EAAE,QAAQ,GAAG;AAC9C,MAAE,sBAAsB,QAAQ,EAAE,QAAQ,IAAI,EAAE,SAAS,GAAG,OAAO,EAAE;AAAA,EACzE;AAEA,IAAE,sBAAsB,QAAQ,EAAE,QAAQ,EAAE;AAC5C,MAAI,QAAS,GAAE,sBAAsB,QAAQ,EAAE,QAAQ,EAAE;AACzD,gBAAc;AAClB;AAGO,SAAS,mBAAmB,UAAoC;AACnE,QAAM,IAAI,kBAAkB;AAC5B,QAAM,QAAQ,EAAE,sBAAsB,QAAQ;AAC9C,MAAI,CAAC,MAAO,QAAO,CAAC;AAEpB,SAAO,OAAO,QAAQ,KAAK,EACtB,OAAO,CAAC,CAAC,EAAE,KAAK,MAAM,MAAM,SAAS,CAAC,EACtC,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO;AAAA,IACrB;AAAA,IACA,aAAa,MAAM,UAAU,MAAM;AAAA,IACnC,WAAW,MAAM;AAAA,EACrB,EAAE,EACD,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,EAAE,WAAW;AACrD;AAGO,SAAS,0BAA0B,UAAiC;AACvE,QAAM,QAAQ,mBAAmB,QAAQ;AACzC,MAAI,MAAM,SAAS,EAAG,QAAO;AAE7B,QAAM,QAAkB,CAAC;AACzB,QAAM,aAAa,CAAC,aAAa,cAAc,aAAa,UAAU;AACtE,QAAM,YAAY,MAAM,KAAK,OAAK,EAAE,SAAS,OAAO;AAGpD,aAAW,MAAM,MAAM,OAAO,OAAK,WAAW,SAAS,EAAE,IAAI,CAAC,GAAG;AAC7D,QAAI,aAAa,GAAG,cAAc,UAAU,eAAe,GAAG,aAAa,GAAG;AAC1E,YAAM;AAAA,QACF,UAAU,GAAG,IAAI,KAAK,KAAK,MAAM,GAAG,cAAc,GAAG,CAAC,0BACvC,KAAK,MAAM,UAAU,cAAc,GAAG,CAAC;AAAA,MAC1D;AAAA,IACJ;AAAA,EACJ;AAGA,MAAI,MAAM,WAAW,GAAG;AACpB,UAAM,OAAO,MAAM,MAAM,GAAG,CAAC;AAC7B,QAAI,KAAK,UAAU,GAAG;AAClB,YAAM,KAAK,eAAe,KAAK,IAAI,OAAK,GAAG,EAAE,IAAI,KAAK,KAAK,MAAM,EAAE,cAAc,GAAG,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,EAAE;AAAA,IAC3G;AAAA,EACJ;AAEA,SAAO,MAAM,SAAS,IAAI,OAAO,QAAQ,WAAW,MAAM,KAAK,IAAI,CAAC,KAAK;AAC7E;AAGO,SAAS,eACZ,SACA,WACA,YACA,SACA,cACI;AACJ,QAAM,IAAI,kBAAkB;AAC5B,QAAM,WAAW,iBAAiB,OAAO;AAGzC,MAAI,WAAW,gBAAgB,aAAa,SAAS,GAAG;AACpD,UAAM,SAAS,aAAa,KAAK,QAAG;AACpC,UAAM,WAAW,EAAE,WAAW;AAAA,MAC1B,OAAK,EAAE,WAAW,EAAE,aAAa,YAAY,EAAE,cAAc,KAAK,QAAG,MAAM;AAAA,IAC/E;AACA,QAAI,UAAU;AACV,eAAS,gBAAgB,SAAS,gBAAgB,KAAK;AACvD,eAAS,aAAY,oBAAI,KAAK,GAAE,YAAY;AAC5C,oBAAc;AACd;AAAA,IACJ;AAAA,EACJ;AAEA,QAAM,QAAuB;AAAA,IACzB,SAAS,QAAQ,MAAM,GAAG,GAAG;AAAA,IAC7B,WAAW,CAAC,GAAG,IAAI,IAAI,SAAS,CAAC;AAAA,IACjC,cAAc,cAAc,MAAM,GAAG,EAAE;AAAA;AAAA,IACvC;AAAA,IACA;AAAA,IACA;AAAA,IACA,cAAc,UAAU,IAAI;AAAA,IAC5B,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,EACtC;AAEA,IAAE,WAAW,KAAK,KAAK;AACvB,2BAAyB;AAGzB,MAAI,EAAE,WAAW,SAAS,KAAK;AAE3B,MAAE,WAAW,KAAK,CAAC,GAAG,MAAM;AACxB,UAAI,EAAE,YAAY,EAAE,QAAS,QAAO,EAAE,UAAU,KAAK;AACrD,YAAM,SAAS,EAAE,gBAAgB;AACjC,YAAM,SAAS,EAAE,gBAAgB;AACjC,UAAI,WAAW,OAAQ,QAAO,SAAS;AACvC,aAAO,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ;AAAA,IAC3E,CAAC;AACD,MAAE,aAAa,EAAE,WAAW,MAAM,GAAG,GAAG;AAAA,EAC5C;AAEA,gBAAc;AAClB;AAGO,SAAS,sBACZ,UACA,cACA,WACI;AACJ,QAAM,IAAI,kBAAkB;AAC5B,QAAM,SAAS,aAAa,KAAK,QAAG;AACpC,QAAM,QAAQ,EAAE,WAAW;AAAA,IACvB,OAAK,EAAE,aAAa,YAAY,EAAE,cAAc,KAAK,QAAG,MAAM;AAAA,EAClE;AACA,MAAI,CAAC,MAAO;AAEZ,MAAI,WAAW;AACX,UAAM,gBAAgB,MAAM,gBAAgB,KAAK;AACjD,UAAM,iBAAgB,oBAAI,KAAK,GAAE,YAAY;AAAA,EACjD,OAAO;AACH,UAAM,aAAa,MAAM,aAAa,KAAK;AAAA,EAC/C;AAGA,QAAM,sBAAsB,MAAM,aAAa,MAAM,MAAM,gBAAgB,MAAM,MAAM;AACvF,MAAI,oBAAoB;AACpB,UAAM,UAAU;AAAA,EACpB;AAEA,MAAI,mBAAoB,0BAAyB;AACjD,gBAAc;AAClB;AAGA,SAAS,gBAAgB,YAAsC;AAC3D,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,UAAU;AAEd,aAAW,KAAK,YAAY;AACxB,UAAM,gBAAgB,EAAE,gBAClB,IAAI,KAAK,EAAE,aAAa,EAAE,QAAQ,IAClC,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ;AACpC,UAAM,sBAAsB,MAAM,iBAAiB;AAGnD,QAAI,qBAAqB,OAAO,EAAE,gBAAgB,KAAK,GAAG;AACtD,QAAE,eAAe,KAAK,IAAI,GAAG,KAAK,OAAO,EAAE,gBAAgB,KAAK,GAAG,CAAC;AACpE,gBAAU;AAAA,IACd;AAGA,SAAK,EAAE,aAAa,MAAM,EAAE,gBAAgB,MAAM,EAAE,SAAS;AACzD,QAAE,UAAU;AACZ,gBAAU;AAAA,IACd;AAAA,EACJ;AAEA,SAAO;AACX;AAGO,SAAS,iBAAiB,SAAgC;AAC7D,QAAM,IAAI,kBAAkB;AAG5B,QAAM,MAAM,KAAK,IAAI;AACrB,MAAI,MAAM,eAAe,mBAAmB;AACxC,mBAAe;AACf,QAAI,gBAAgB,EAAE,UAAU,GAAG;AAC/B,oBAAc;AACd,+BAAyB;AAAA,IAC7B;AAAA,EACJ;AACA,MAAI,EAAE,WAAW,WAAW,EAAG,QAAO;AAEtC,QAAM,WAAW,iBAAiB,OAAO;AACzC,QAAM,QAAQ,IAAI,IAAI,QAAQ,YAAY,EAAE,MAAM,KAAK,EAAE,OAAO,OAAK,EAAE,SAAS,CAAC,CAAC;AAClF,MAAI,MAAM,SAAS,EAAG,QAAO;AAE7B,QAAM,uBAAuB,8BAA8B,4BAA4B,EAAE,WAAW,OAAO,OAAK,EAAE,OAAO;AACzH,MAAI,qBAAqB,WAAW,EAAG,QAAO;AAG9C,MAAI,YAAkC;AACtC,MAAI,YAAY;AAEhB,aAAW,YAAY,sBAAsB;AACzC,UAAM,eAAe,gBAAgB,SAAS,OAAO;AACrD,QAAI,UAAU;AACd,eAAW,KAAK,OAAO;AACnB,UAAI,aAAa,IAAI,CAAC,EAAG;AAAA,IAC7B;AACA,UAAM,eAAe,UAAU,KAAK,IAAI,MAAM,MAAM,aAAa,IAAI;AAGrE,UAAM,YAAY,SAAS,aAAa,WAAW,MAAM;AAGzD,UAAM,aAAa,KAAK,KAAK,SAAS,gBAAgB,KAAK,IAAI,IAAI;AAEnE,UAAM,aAAa,eAAe,YAAY;AAC9C,QAAI,aAAa,aAAa,eAAe,MAAM;AAC/C,kBAAY;AACZ,kBAAY;AAAA,IAChB;AAAA,EACJ;AAEA,MAAI,CAAC,UAAW,QAAO;AAGvB,QAAM,WAAW,UAAU,gBAAgB,UAAU,aAAa,SAAS,IACrE,UAAU,aAAa,KAAK,UAAK,IACjC,UAAU,UAAU,KAAK,IAAI;AAEnC,QAAM,aAAa,UAAU,gBAAgB,KAAK,IAC5C,eAAe,UAAU,YAAY,OACrC;AAEN,SAAO,eAAe,UAAU,YAAY,SAAS,mCAAmC,QAAQ,KAAK,UAAU,UAAU,WAAW,SAAS;AACjJ;AAGO,SAAS,mBAAmB,OAA8B;AAC7D,QAAM,IAAI,kBAAkB;AAC5B,QAAM,aAAa,MAAM,YAAY;AAErC,aAAW,CAAC,SAAS,IAAI,KAAK,OAAO,QAAQ,EAAE,aAAa,GAAG;AAC3D,QAAI,KAAK,cAAc,WAAW,SAAS,QAAQ,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG;AAC5E,aAAO,KAAK;AAAA,IAChB;AAAA,EACJ;AACA,SAAO;AACX;","names":[]}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { homedir } from "os";
3
3
  import { join } from "path";
4
- const TITAN_VERSION = "5.5.10";
4
+ const TITAN_VERSION = "5.5.11";
5
5
  const TITAN_CODENAME = "Spacewalk";
6
6
  const TITAN_NAME = "TITAN";
7
7
  const TITAN_FULL_NAME = "The Intelligent Task Automation Network";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '5.5.10';\nexport const TITAN_CODENAME = 'Spacewalk';\nexport const TITAN_NAME = 'TITAN';\nexport const TITAN_FULL_NAME = 'The Intelligent Task Automation Network';\nexport const TITAN_ASCII_LOGO = `\n╔══════════════════════════════════════════════════════╗\n║ ║\n║ ████████╗██╗████████╗ █████╗ ███╗ ██╗ ║\n║ ██║ ██║ ██║ ██╔══██╗████╗ ██║ ║\n║ ██║ ██║ ██║ ███████║██╔██╗ ██║ ║\n║ ██║ ██║ ██║ ██╔══██║██║╚██╗██║ ║\n║ ██║ ██║ ██║ ██║ ██║██║ ╚████║ ║\n║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ║\n║ ║\n║ The Intelligent Task Automation Network ║\n║ v${TITAN_VERSION} • by Tony Elliott ║\n╚══════════════════════════════════════════════════════╝`;\n\n// Paths\n// Hunt Finding #03 (2026-04-14): honor TITAN_HOME env var if set.\n// Previously this was hardcoded to `~/.titan`, which meant:\n// - Docker containers couldn't override the config path\n// - Shared machines couldn't isolate per-user state\n// - Test fixtures couldn't run against an isolated home\n// - The systemd unit's `Environment=TITAN_HOME=...` was silently ignored\n// The env var is read once at module load (constants are resolved at import time).\n// If TITAN_HOME starts with `~/`, expand it to the user's home dir.\nfunction resolveTitanHome(): string {\n const envHome = process.env.TITAN_HOME;\n if (envHome && envHome.trim().length > 0) {\n const trimmed = envHome.trim();\n if (trimmed.startsWith('~/')) {\n return join(homedir(), trimmed.slice(2));\n }\n if (trimmed === '~') {\n return homedir();\n }\n return trimmed;\n }\n return join(homedir(), '.titan');\n}\nexport const TITAN_HOME = resolveTitanHome();\nexport const TITAN_CONFIG_PATH = join(TITAN_HOME, 'titan.json');\nexport const TITAN_DB_PATH = join(TITAN_HOME, 'titan.db');\nexport const TITAN_WORKSPACE = join(TITAN_HOME, 'workspace');\nexport const TITAN_SKILLS_DIR = join(TITAN_WORKSPACE, 'skills');\nexport const TITAN_LOGS_DIR = join(TITAN_HOME, 'logs');\nexport const TITAN_MEMORY_DIR = join(TITAN_HOME, 'memory');\n\n// Workspace prompt files (injected into agent context)\nexport const AGENTS_MD = join(TITAN_WORKSPACE, 'AGENTS.md');\nexport const SOUL_MD = join(TITAN_WORKSPACE, 'SOUL.md');\nexport const TOOLS_MD = join(TITAN_WORKSPACE, 'TOOLS.md');\nexport const TITAN_MD_FILENAME = 'TITAN.md';\nexport const AUTOPILOT_MD = join(TITAN_HOME, 'AUTOPILOT.md');\nexport const AUTOPILOT_RUNS_PATH = join(TITAN_HOME, 'autopilot-runs.jsonl');\nexport const TITAN_CREDENTIALS_DIR = join(TITAN_HOME, 'credentials');\n\n// Income & lead tracking\nexport const INCOME_LEDGER_PATH = join(TITAN_HOME, 'income-ledger.jsonl');\nexport const FREELANCE_LEADS_PATH = join(TITAN_HOME, 'freelance-leads.jsonl');\nexport const FREELANCE_PROFILE_PATH = join(TITAN_HOME, 'freelance-profile.json');\nexport const LEADS_PATH = join(TITAN_HOME, 'leads.jsonl');\nexport const TELEMETRY_EVENTS_PATH = join(TITAN_HOME, 'telemetry-events.jsonl');\nexport const SOMADRIVE_STATE_PATH = join(TITAN_HOME, 'soma-drive-state.json');\nexport const ACTIVITY_LOG_PATH = join(TITAN_HOME, 'activity-log.jsonl');\n\n// Gateway defaults\nexport const DEFAULT_GATEWAY_HOST = '0.0.0.0';\nexport const DEFAULT_GATEWAY_PORT = 48420;\nexport const DEFAULT_WEB_PORT = 48421;\n\n// Agent defaults\nexport const DEFAULT_MODEL = 'anthropic/claude-sonnet-4-20250514';\n/** v5.4.1: User-preference ceiling. Providers clamp per-model via\n * clampMaxTokens() so this can be high without causing 400s on\n * capped endpoints (e.g. Claude Sonnet 4 8K, Cohere 4K). */\nexport const DEFAULT_MAX_TOKENS = 200000;\nexport const DEFAULT_TEMPERATURE = 0.7;\nexport const MAX_CONTEXT_MESSAGES = 50;\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes\n\n// Security\nexport const DEFAULT_SANDBOX_MODE = 'host';\n/** Default allowed tools. Empty = allow ALL registered tools.\n * Use security.deniedTools to block specific tools instead. */\nexport const ALLOWED_TOOLS_DEFAULT: string[] = [];\nexport const DENIED_TOOLS_DEFAULT: string[] = [];\n"],"mappings":";AAGA,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAW1B,aAAa;AAAA;AAYnB,SAAS,mBAA2B;AAChC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACtC,UAAM,UAAU,QAAQ,KAAK;AAC7B,QAAI,QAAQ,WAAW,IAAI,GAAG;AAC1B,aAAO,KAAK,QAAQ,GAAG,QAAQ,MAAM,CAAC,CAAC;AAAA,IAC3C;AACA,QAAI,YAAY,KAAK;AACjB,aAAO,QAAQ;AAAA,IACnB;AACA,WAAO;AAAA,EACX;AACA,SAAO,KAAK,QAAQ,GAAG,QAAQ;AACnC;AACO,MAAM,aAAa,iBAAiB;AACpC,MAAM,oBAAoB,KAAK,YAAY,YAAY;AACvD,MAAM,gBAAgB,KAAK,YAAY,UAAU;AACjD,MAAM,kBAAkB,KAAK,YAAY,WAAW;AACpD,MAAM,mBAAmB,KAAK,iBAAiB,QAAQ;AACvD,MAAM,iBAAiB,KAAK,YAAY,MAAM;AAC9C,MAAM,mBAAmB,KAAK,YAAY,QAAQ;AAGlD,MAAM,YAAY,KAAK,iBAAiB,WAAW;AACnD,MAAM,UAAU,KAAK,iBAAiB,SAAS;AAC/C,MAAM,WAAW,KAAK,iBAAiB,UAAU;AACjD,MAAM,oBAAoB;AAC1B,MAAM,eAAe,KAAK,YAAY,cAAc;AACpD,MAAM,sBAAsB,KAAK,YAAY,sBAAsB;AACnE,MAAM,wBAAwB,KAAK,YAAY,aAAa;AAG5D,MAAM,qBAAqB,KAAK,YAAY,qBAAqB;AACjE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,yBAAyB,KAAK,YAAY,wBAAwB;AACxE,MAAM,aAAa,KAAK,YAAY,aAAa;AACjD,MAAM,wBAAwB,KAAK,YAAY,wBAAwB;AACvE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,oBAAoB,KAAK,YAAY,oBAAoB;AAG/D,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,mBAAmB;AAGzB,MAAM,gBAAgB;AAItB,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,qBAAqB,KAAK,KAAK;AAGrC,MAAM,uBAAuB;AAG7B,MAAM,wBAAkC,CAAC;AACzC,MAAM,uBAAiC,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '5.5.11';\nexport const TITAN_CODENAME = 'Spacewalk';\nexport const TITAN_NAME = 'TITAN';\nexport const TITAN_FULL_NAME = 'The Intelligent Task Automation Network';\nexport const TITAN_ASCII_LOGO = `\n╔══════════════════════════════════════════════════════╗\n║ ║\n║ ████████╗██╗████████╗ █████╗ ███╗ ██╗ ║\n║ ██║ ██║ ██║ ██╔══██╗████╗ ██║ ║\n║ ██║ ██║ ██║ ███████║██╔██╗ ██║ ║\n║ ██║ ██║ ██║ ██╔══██║██║╚██╗██║ ║\n║ ██║ ██║ ██║ ██║ ██║██║ ╚████║ ║\n║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ║\n║ ║\n║ The Intelligent Task Automation Network ║\n║ v${TITAN_VERSION} • by Tony Elliott ║\n╚══════════════════════════════════════════════════════╝`;\n\n// Paths\n// Hunt Finding #03 (2026-04-14): honor TITAN_HOME env var if set.\n// Previously this was hardcoded to `~/.titan`, which meant:\n// - Docker containers couldn't override the config path\n// - Shared machines couldn't isolate per-user state\n// - Test fixtures couldn't run against an isolated home\n// - The systemd unit's `Environment=TITAN_HOME=...` was silently ignored\n// The env var is read once at module load (constants are resolved at import time).\n// If TITAN_HOME starts with `~/`, expand it to the user's home dir.\nfunction resolveTitanHome(): string {\n const envHome = process.env.TITAN_HOME;\n if (envHome && envHome.trim().length > 0) {\n const trimmed = envHome.trim();\n if (trimmed.startsWith('~/')) {\n return join(homedir(), trimmed.slice(2));\n }\n if (trimmed === '~') {\n return homedir();\n }\n return trimmed;\n }\n return join(homedir(), '.titan');\n}\nexport const TITAN_HOME = resolveTitanHome();\nexport const TITAN_CONFIG_PATH = join(TITAN_HOME, 'titan.json');\nexport const TITAN_DB_PATH = join(TITAN_HOME, 'titan.db');\nexport const TITAN_WORKSPACE = join(TITAN_HOME, 'workspace');\nexport const TITAN_SKILLS_DIR = join(TITAN_WORKSPACE, 'skills');\nexport const TITAN_LOGS_DIR = join(TITAN_HOME, 'logs');\nexport const TITAN_MEMORY_DIR = join(TITAN_HOME, 'memory');\n\n// Workspace prompt files (injected into agent context)\nexport const AGENTS_MD = join(TITAN_WORKSPACE, 'AGENTS.md');\nexport const SOUL_MD = join(TITAN_WORKSPACE, 'SOUL.md');\nexport const TOOLS_MD = join(TITAN_WORKSPACE, 'TOOLS.md');\nexport const TITAN_MD_FILENAME = 'TITAN.md';\nexport const AUTOPILOT_MD = join(TITAN_HOME, 'AUTOPILOT.md');\nexport const AUTOPILOT_RUNS_PATH = join(TITAN_HOME, 'autopilot-runs.jsonl');\nexport const TITAN_CREDENTIALS_DIR = join(TITAN_HOME, 'credentials');\n\n// Income & lead tracking\nexport const INCOME_LEDGER_PATH = join(TITAN_HOME, 'income-ledger.jsonl');\nexport const FREELANCE_LEADS_PATH = join(TITAN_HOME, 'freelance-leads.jsonl');\nexport const FREELANCE_PROFILE_PATH = join(TITAN_HOME, 'freelance-profile.json');\nexport const LEADS_PATH = join(TITAN_HOME, 'leads.jsonl');\nexport const TELEMETRY_EVENTS_PATH = join(TITAN_HOME, 'telemetry-events.jsonl');\nexport const SOMADRIVE_STATE_PATH = join(TITAN_HOME, 'soma-drive-state.json');\nexport const ACTIVITY_LOG_PATH = join(TITAN_HOME, 'activity-log.jsonl');\n\n// Gateway defaults\nexport const DEFAULT_GATEWAY_HOST = '0.0.0.0';\nexport const DEFAULT_GATEWAY_PORT = 48420;\nexport const DEFAULT_WEB_PORT = 48421;\n\n// Agent defaults\nexport const DEFAULT_MODEL = 'anthropic/claude-sonnet-4-20250514';\n/** v5.4.1: User-preference ceiling. Providers clamp per-model via\n * clampMaxTokens() so this can be high without causing 400s on\n * capped endpoints (e.g. Claude Sonnet 4 8K, Cohere 4K). */\nexport const DEFAULT_MAX_TOKENS = 200000;\nexport const DEFAULT_TEMPERATURE = 0.7;\nexport const MAX_CONTEXT_MESSAGES = 50;\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes\n\n// Security\nexport const DEFAULT_SANDBOX_MODE = 'host';\n/** Default allowed tools. Empty = allow ALL registered tools.\n * Use security.deniedTools to block specific tools instead. */\nexport const ALLOWED_TOOLS_DEFAULT: string[] = [];\nexport const DENIED_TOOLS_DEFAULT: string[] = [];\n"],"mappings":";AAGA,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAW1B,aAAa;AAAA;AAYnB,SAAS,mBAA2B;AAChC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACtC,UAAM,UAAU,QAAQ,KAAK;AAC7B,QAAI,QAAQ,WAAW,IAAI,GAAG;AAC1B,aAAO,KAAK,QAAQ,GAAG,QAAQ,MAAM,CAAC,CAAC;AAAA,IAC3C;AACA,QAAI,YAAY,KAAK;AACjB,aAAO,QAAQ;AAAA,IACnB;AACA,WAAO;AAAA,EACX;AACA,SAAO,KAAK,QAAQ,GAAG,QAAQ;AACnC;AACO,MAAM,aAAa,iBAAiB;AACpC,MAAM,oBAAoB,KAAK,YAAY,YAAY;AACvD,MAAM,gBAAgB,KAAK,YAAY,UAAU;AACjD,MAAM,kBAAkB,KAAK,YAAY,WAAW;AACpD,MAAM,mBAAmB,KAAK,iBAAiB,QAAQ;AACvD,MAAM,iBAAiB,KAAK,YAAY,MAAM;AAC9C,MAAM,mBAAmB,KAAK,YAAY,QAAQ;AAGlD,MAAM,YAAY,KAAK,iBAAiB,WAAW;AACnD,MAAM,UAAU,KAAK,iBAAiB,SAAS;AAC/C,MAAM,WAAW,KAAK,iBAAiB,UAAU;AACjD,MAAM,oBAAoB;AAC1B,MAAM,eAAe,KAAK,YAAY,cAAc;AACpD,MAAM,sBAAsB,KAAK,YAAY,sBAAsB;AACnE,MAAM,wBAAwB,KAAK,YAAY,aAAa;AAG5D,MAAM,qBAAqB,KAAK,YAAY,qBAAqB;AACjE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,yBAAyB,KAAK,YAAY,wBAAwB;AACxE,MAAM,aAAa,KAAK,YAAY,aAAa;AACjD,MAAM,wBAAwB,KAAK,YAAY,wBAAwB;AACvE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,oBAAoB,KAAK,YAAY,oBAAoB;AAG/D,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,mBAAmB;AAGzB,MAAM,gBAAgB;AAItB,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,qBAAqB,KAAK,KAAK;AAGrC,MAAM,uBAAuB;AAG7B,MAAM,wBAAkC,CAAC;AACzC,MAAM,uBAAiC,CAAC;","names":[]}
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, statSync, readFileSync } from "fs";
3
+ import logger from "./logger.js";
4
+ function createMtimeCache(opts) {
5
+ let value = null;
6
+ let lastLoadedMtimeMs = 0;
7
+ function load() {
8
+ if (!existsSync(opts.path)) {
9
+ value = opts.initial();
10
+ lastLoadedMtimeMs = 0;
11
+ return value;
12
+ }
13
+ try {
14
+ const stat = statSync(opts.path);
15
+ const raw = readFileSync(opts.path, "utf-8");
16
+ value = opts.parse(raw);
17
+ lastLoadedMtimeMs = stat.mtimeMs;
18
+ return value;
19
+ } catch (err) {
20
+ logger.warn(opts.component, `mtime-cache load failed for ${opts.path}: ${err.message}`);
21
+ value = opts.initial();
22
+ lastLoadedMtimeMs = 0;
23
+ return value;
24
+ }
25
+ }
26
+ return {
27
+ read() {
28
+ if (value === null) return load();
29
+ try {
30
+ if (existsSync(opts.path)) {
31
+ const stat = statSync(opts.path);
32
+ if (stat.mtimeMs > lastLoadedMtimeMs) {
33
+ return load();
34
+ }
35
+ } else if (lastLoadedMtimeMs !== 0) {
36
+ return load();
37
+ }
38
+ } catch {
39
+ }
40
+ return value;
41
+ },
42
+ invalidate() {
43
+ value = null;
44
+ lastLoadedMtimeMs = 0;
45
+ },
46
+ set(newValue, _reason) {
47
+ value = newValue;
48
+ try {
49
+ if (existsSync(opts.path)) {
50
+ lastLoadedMtimeMs = statSync(opts.path).mtimeMs;
51
+ } else {
52
+ lastLoadedMtimeMs = Date.now();
53
+ }
54
+ } catch {
55
+ lastLoadedMtimeMs = Date.now();
56
+ }
57
+ },
58
+ debug() {
59
+ return { hasValue: value !== null, lastLoadedMtimeMs, path: opts.path };
60
+ }
61
+ };
62
+ }
63
+ export {
64
+ createMtimeCache
65
+ };
66
+ //# sourceMappingURL=mtimeCache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/mtimeCache.ts"],"sourcesContent":["/**\n * TITAN — mtime-validated file cache helper\n *\n * The problem: many modules in TITAN cache parsed JSON from a file in module-\n * scope state. If the file is then modified externally (admin script, another\n * Node process, manual edit, agent file-write tool, autopilot), the cache\n * goes stale and the module lies about disk state. This was reproduced in\n * the 2026-05-07 stabilization session: a one-off node script wrote to\n * goals.json on disk; the live gateway's `getApproval/listGoals` API kept\n * returning the pre-write state until restart.\n *\n * The fix: on every cache read, stat the file and compare mtime. If the\n * file is newer than the cached load, reload from disk before returning.\n * Cost is one `fs.statSync` per cache hit — typically <0.1ms — in exchange\n * for correctness.\n *\n * Usage:\n * const goalsCache = createMtimeCache<Goal[]>({\n * path: GOALS_PATH,\n * parse: (raw) => (JSON.parse(raw).goals || []),\n * initial: () => [],\n * component: 'Goals',\n * });\n * const goals = goalsCache.read(); // returns current goals\n * goalsCache.invalidate(); // force reload on next read\n * goalsCache.set(updatedGoals, 'after createGoal'); // update both cache + disk-aware mtime\n */\nimport { existsSync, statSync, readFileSync } from 'fs';\nimport logger from './logger.js';\n\nexport interface MtimeCacheOpts<T> {\n /** Absolute filesystem path to the cached file */\n path: string;\n /** Convert raw file contents to typed value. Throws → caller falls back to `initial`. */\n parse: (raw: string) => T;\n /** Returns the value to use when the file does not exist or is unreadable. */\n initial: () => T;\n /** Logger component label. */\n component: string;\n}\n\nexport interface MtimeCache<T> {\n /** Read current value, reloading from disk if the file mtime advanced since last load. */\n read(): T;\n /** Force the next `read()` to bypass the cache (e.g. after writing to the file via this same module). */\n invalidate(): void;\n /**\n * Update the in-memory cache without re-reading the file. Use this AFTER you write\n * to disk, so the next `read()` doesn't false-positive on its own mtime advance.\n */\n set(value: T, _reason?: string): void;\n /** Diagnostics — for tests + status endpoints. */\n debug(): { hasValue: boolean; lastLoadedMtimeMs: number; path: string };\n}\n\nexport function createMtimeCache<T>(opts: MtimeCacheOpts<T>): MtimeCache<T> {\n let value: T | null = null;\n let lastLoadedMtimeMs = 0;\n\n function load(): T {\n if (!existsSync(opts.path)) {\n value = opts.initial();\n lastLoadedMtimeMs = 0;\n return value;\n }\n try {\n const stat = statSync(opts.path);\n const raw = readFileSync(opts.path, 'utf-8');\n value = opts.parse(raw);\n lastLoadedMtimeMs = stat.mtimeMs;\n return value;\n } catch (err) {\n logger.warn(opts.component, `mtime-cache load failed for ${opts.path}: ${(err as Error).message}`);\n value = opts.initial();\n lastLoadedMtimeMs = 0;\n return value;\n }\n }\n\n return {\n read(): T {\n if (value === null) return load();\n // Cheap mtime check on cache hit. If the file got newer than our\n // last load (external write), reload from disk.\n try {\n if (existsSync(opts.path)) {\n const stat = statSync(opts.path);\n if (stat.mtimeMs > lastLoadedMtimeMs) {\n return load();\n }\n } else if (lastLoadedMtimeMs !== 0) {\n // File existed when we cached, now it doesn't — fall back to initial.\n return load();\n }\n } catch {\n // stat failure is non-fatal; serve the cached value.\n }\n return value;\n },\n invalidate(): void {\n value = null;\n lastLoadedMtimeMs = 0;\n },\n set(newValue: T, _reason?: string): void {\n value = newValue;\n // Bump our recorded mtime to \"now\" so a same-second external write\n // is still detected next read.\n try {\n if (existsSync(opts.path)) {\n lastLoadedMtimeMs = statSync(opts.path).mtimeMs;\n } else {\n lastLoadedMtimeMs = Date.now();\n }\n } catch {\n lastLoadedMtimeMs = Date.now();\n }\n },\n debug(): { hasValue: boolean; lastLoadedMtimeMs: number; path: string } {\n return { hasValue: value !== null, lastLoadedMtimeMs, path: opts.path };\n },\n };\n}\n"],"mappings":";AA2BA,SAAS,YAAY,UAAU,oBAAoB;AACnD,OAAO,YAAY;AA2BZ,SAAS,iBAAoB,MAAwC;AACxE,MAAI,QAAkB;AACtB,MAAI,oBAAoB;AAExB,WAAS,OAAU;AACf,QAAI,CAAC,WAAW,KAAK,IAAI,GAAG;AACxB,cAAQ,KAAK,QAAQ;AACrB,0BAAoB;AACpB,aAAO;AAAA,IACX;AACA,QAAI;AACA,YAAM,OAAO,SAAS,KAAK,IAAI;AAC/B,YAAM,MAAM,aAAa,KAAK,MAAM,OAAO;AAC3C,cAAQ,KAAK,MAAM,GAAG;AACtB,0BAAoB,KAAK;AACzB,aAAO;AAAA,IACX,SAAS,KAAK;AACV,aAAO,KAAK,KAAK,WAAW,+BAA+B,KAAK,IAAI,KAAM,IAAc,OAAO,EAAE;AACjG,cAAQ,KAAK,QAAQ;AACrB,0BAAoB;AACpB,aAAO;AAAA,IACX;AAAA,EACJ;AAEA,SAAO;AAAA,IACH,OAAU;AACN,UAAI,UAAU,KAAM,QAAO,KAAK;AAGhC,UAAI;AACA,YAAI,WAAW,KAAK,IAAI,GAAG;AACvB,gBAAM,OAAO,SAAS,KAAK,IAAI;AAC/B,cAAI,KAAK,UAAU,mBAAmB;AAClC,mBAAO,KAAK;AAAA,UAChB;AAAA,QACJ,WAAW,sBAAsB,GAAG;AAEhC,iBAAO,KAAK;AAAA,QAChB;AAAA,MACJ,QAAQ;AAAA,MAER;AACA,aAAO;AAAA,IACX;AAAA,IACA,aAAmB;AACf,cAAQ;AACR,0BAAoB;AAAA,IACxB;AAAA,IACA,IAAI,UAAa,SAAwB;AACrC,cAAQ;AAGR,UAAI;AACA,YAAI,WAAW,KAAK,IAAI,GAAG;AACvB,8BAAoB,SAAS,KAAK,IAAI,EAAE;AAAA,QAC5C,OAAO;AACH,8BAAoB,KAAK,IAAI;AAAA,QACjC;AAAA,MACJ,QAAQ;AACJ,4BAAoB,KAAK,IAAI;AAAA,MACjC;AAAA,IACJ;AAAA,IACA,QAAwE;AACpE,aAAO,EAAE,UAAU,UAAU,MAAM,mBAAmB,MAAM,KAAK,KAAK;AAAA,IAC1E;AAAA,EACJ;AACJ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "titan-agent",
3
- "version": "5.5.10",
3
+ "version": "5.5.11",
4
4
  "description": "TITAN — Autonomous AI agent framework with self-improvement, multi-agent orchestration, 36 LLM providers, 16 channel adapters, GPU VRAM management, mesh networking, LiveKit voice, TITAN-Soma homeostatic drives, and a React Mission Control dashboard. Open-source, TypeScript, MIT licensed.",
5
5
  "author": "Tony Elliott (https://github.com/Djtony707)",
6
6
  "repository": {