titan-agent 5.5.5 → 5.5.6

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.
@@ -3,7 +3,7 @@ import logger from "../utils/logger.js";
3
3
  import { structuredSpawn } from "./structuredSpawn.js";
4
4
  const COMPONENT = "PeerAdvise";
5
5
  const DEFAULT_ADVISOR = "sage";
6
- const DEFAULT_TIMEOUT_MS = 2e4;
6
+ const DEFAULT_TIMEOUT_MS = 3e4;
7
7
  async function peerAdvise(opts) {
8
8
  const advisor = opts.advisor ?? DEFAULT_ADVISOR;
9
9
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/agent/peerAdvise.ts"],"sourcesContent":["/**\n * TITAN — Peer Advisor (v4.13+)\n *\n * Before an autonomous producer (canary-eval, self-repair daemon, auto-heal)\n * escalates a concern to Tony via an approval, it consults a peer specialist\n * and asks: should I really bother the human with this, or can the org figure\n * it out without escalating?\n *\n * The advisor is a single structured sub-agent call with a small schema. It\n * returns one of:\n * - escalate → file the approval as planned (human decision needed)\n * - dismiss → the concern is known/expected/already-fixed; drop it\n * - investigate → the concern is real but the org can act on it first;\n * caller should try its remediation path instead of\n * immediately bothering the human\n *\n * Fail-open: if the advisor call errors, returns null → caller escalates as\n * usual. Better to bother Tony than silently swallow a real problem.\n */\nimport logger from '../utils/logger.js';\nimport { structuredSpawn } from './structuredSpawn.js';\n\nconst COMPONENT = 'PeerAdvise';\n\nexport type PeerVerdict = 'escalate' | 'dismiss' | 'investigate';\n\nexport interface PeerAdvice {\n verdict: PeerVerdict;\n reason: string;\n confidence: number;\n advisorSpecialist: string;\n}\n\nexport interface PeerAdviseOpts {\n /** One-line description of what's triggering the potential escalation. */\n concern: string;\n /** Classification — canary_regression | self_repair | auto_heal | etc. */\n kind: string;\n /** Additional facts the advisor should consider. */\n context?: string;\n /**\n * Which specialist to ask. Default: 'sage' (critic/reviewer role).\n * Route by concern kind when it's obvious (code-failure → sage,\n * research-gap → scout, etc.). Unknown kinds get sage.\n */\n advisor?: 'sage' | 'analyst' | 'scout' | 'builder' | 'writer' | 'default';\n /** Max wait. Default 20000 (20s) — advisor should be quick or fail open. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_ADVISOR = 'sage';\nconst DEFAULT_TIMEOUT_MS = 20_000;\n\n/**\n * Consult a peer specialist about whether a concern warrants escalation.\n * Returns null on failure so the caller can fall back to filing the\n * approval unchanged.\n */\nexport async function peerAdvise(opts: PeerAdviseOpts): Promise<PeerAdvice | null> {\n const advisor = opts.advisor ?? DEFAULT_ADVISOR;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const task = [\n `Another component of TITAN wants to bother Tony with an approval request. Before they do, they want your read as the peer advisor.`,\n '',\n `Concern kind: ${opts.kind}`,\n `Concern: ${opts.concern}`,\n opts.context ? `Context:\\n${opts.context}` : '',\n '',\n 'Decide ONE of three verdicts:',\n ' - escalate → human attention is genuinely needed right now',\n ' - dismiss → this is expected behaviour / already resolved / noise',\n ' - investigate → the org should try something automatic first (log and keep an eye on it; do not file approval)',\n '',\n 'Return a JSON object with fields: status (\"done\"), artifacts ([]), questions ([]), confidence (0-1), reasoning (1-2 sentences explaining your verdict), plus an extra field \"verdict\" containing exactly one of: escalate | dismiss | investigate.',\n 'Be a tough gatekeeper: escalate only when a human must look. When in doubt, lean dismiss or investigate.',\n ].filter(Boolean).join('\\n');\n\n const startedAt = Date.now();\n try {\n // Use Promise.race to enforce the timeout independent of subagent internals.\n const result = await Promise.race([\n structuredSpawn({\n specialistId: advisor,\n task,\n maxRounds: 2,\n }),\n new Promise<null>((resolve) => setTimeout(() => resolve(null), timeoutMs)),\n ]);\n if (!result) {\n logger.warn(COMPONENT, `${advisor} advisor timed out after ${timeoutMs}ms — failing open (escalate)`);\n return null;\n }\n const raw = result.rawResponse || '';\n // Pull 'verdict' out of the raw JSON if present\n let verdict: PeerVerdict = 'escalate';\n const m = raw.match(/\"verdict\"\\s*:\\s*\"(escalate|dismiss|investigate)\"/i);\n if (m) verdict = m[1].toLowerCase() as PeerVerdict;\n const advice: PeerAdvice = {\n verdict,\n reason: result.reasoning || 'no reason provided',\n confidence: result.confidence,\n advisorSpecialist: advisor,\n };\n logger.info(COMPONENT, `${advisor} verdict=${advice.verdict} confidence=${advice.confidence.toFixed(2)} reason=\"${advice.reason.slice(0, 100)}\" durationMs=${Date.now() - startedAt}`);\n return advice;\n } catch (err) {\n logger.warn(COMPONENT, `peer advise threw: ${(err as Error).message} — failing open (escalate)`);\n return null;\n }\n}\n"],"mappings":";AAmBA,OAAO,YAAY;AACnB,SAAS,uBAAuB;AAEhC,MAAM,YAAY;AA4BlB,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;AAO3B,eAAsB,WAAW,MAAkD;AAC/E,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,YAAY,KAAK,aAAa;AAEpC,QAAM,OAAO;AAAA,IACT;AAAA,IACA;AAAA,IACA,iBAAiB,KAAK,IAAI;AAAA,IAC1B,YAAY,KAAK,OAAO;AAAA,IACxB,KAAK,UAAU;AAAA,EAAa,KAAK,OAAO,KAAK;AAAA,IAC7C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACJ,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AAE3B,QAAM,YAAY,KAAK,IAAI;AAC3B,MAAI;AAEA,UAAM,SAAS,MAAM,QAAQ,KAAK;AAAA,MAC9B,gBAAgB;AAAA,QACZ,cAAc;AAAA,QACd;AAAA,QACA,WAAW;AAAA,MACf,CAAC;AAAA,MACD,IAAI,QAAc,CAAC,YAAY,WAAW,MAAM,QAAQ,IAAI,GAAG,SAAS,CAAC;AAAA,IAC7E,CAAC;AACD,QAAI,CAAC,QAAQ;AACT,aAAO,KAAK,WAAW,GAAG,OAAO,4BAA4B,SAAS,mCAA8B;AACpG,aAAO;AAAA,IACX;AACA,UAAM,MAAM,OAAO,eAAe;AAElC,QAAI,UAAuB;AAC3B,UAAM,IAAI,IAAI,MAAM,mDAAmD;AACvE,QAAI,EAAG,WAAU,EAAE,CAAC,EAAE,YAAY;AAClC,UAAM,SAAqB;AAAA,MACvB;AAAA,MACA,QAAQ,OAAO,aAAa;AAAA,MAC5B,YAAY,OAAO;AAAA,MACnB,mBAAmB;AAAA,IACvB;AACA,WAAO,KAAK,WAAW,GAAG,OAAO,YAAY,OAAO,OAAO,eAAe,OAAO,WAAW,QAAQ,CAAC,CAAC,YAAY,OAAO,OAAO,MAAM,GAAG,GAAG,CAAC,gBAAgB,KAAK,IAAI,IAAI,SAAS,EAAE;AACrL,WAAO;AAAA,EACX,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,sBAAuB,IAAc,OAAO,iCAA4B;AAC/F,WAAO;AAAA,EACX;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/agent/peerAdvise.ts"],"sourcesContent":["/**\n * TITAN — Peer Advisor (v4.13+)\n *\n * Before an autonomous producer (canary-eval, self-repair daemon, auto-heal)\n * escalates a concern to Tony via an approval, it consults a peer specialist\n * and asks: should I really bother the human with this, or can the org figure\n * it out without escalating?\n *\n * The advisor is a single structured sub-agent call with a small schema. It\n * returns one of:\n * - escalate → file the approval as planned (human decision needed)\n * - dismiss → the concern is known/expected/already-fixed; drop it\n * - investigate → the concern is real but the org can act on it first;\n * caller should try its remediation path instead of\n * immediately bothering the human\n *\n * Fail-open: if the advisor call errors, returns null → caller escalates as\n * usual. Better to bother Tony than silently swallow a real problem.\n */\nimport logger from '../utils/logger.js';\nimport { structuredSpawn } from './structuredSpawn.js';\n\nconst COMPONENT = 'PeerAdvise';\n\nexport type PeerVerdict = 'escalate' | 'dismiss' | 'investigate';\n\nexport interface PeerAdvice {\n verdict: PeerVerdict;\n reason: string;\n confidence: number;\n advisorSpecialist: string;\n}\n\nexport interface PeerAdviseOpts {\n /** One-line description of what's triggering the potential escalation. */\n concern: string;\n /** Classification — canary_regression | self_repair | auto_heal | etc. */\n kind: string;\n /** Additional facts the advisor should consider. */\n context?: string;\n /**\n * Which specialist to ask. Default: 'sage' (critic/reviewer role).\n * Route by concern kind when it's obvious (code-failure → sage,\n * research-gap → scout, etc.). Unknown kinds get sage.\n */\n advisor?: 'sage' | 'analyst' | 'scout' | 'builder' | 'writer' | 'default';\n /** Max wait. Default 30000 (30s) — advisor should be quick or fail open. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_ADVISOR = 'sage';\n// v5.5.6: bumped from 20s. Observed sage runs often take 13-25s (one\n// round + thinking-fallback + tool turn), so 20s caused frequent\n// time-outs that fell open as 'escalate'. 30s gives normal runs\n// headroom while still bounding latency.\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Consult a peer specialist about whether a concern warrants escalation.\n * Returns null on failure so the caller can fall back to filing the\n * approval unchanged.\n */\nexport async function peerAdvise(opts: PeerAdviseOpts): Promise<PeerAdvice | null> {\n const advisor = opts.advisor ?? DEFAULT_ADVISOR;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n const task = [\n `Another component of TITAN wants to bother Tony with an approval request. Before they do, they want your read as the peer advisor.`,\n '',\n `Concern kind: ${opts.kind}`,\n `Concern: ${opts.concern}`,\n opts.context ? `Context:\\n${opts.context}` : '',\n '',\n 'Decide ONE of three verdicts:',\n ' - escalate → human attention is genuinely needed right now',\n ' - dismiss → this is expected behaviour / already resolved / noise',\n ' - investigate → the org should try something automatic first (log and keep an eye on it; do not file approval)',\n '',\n 'Return a JSON object with fields: status (\"done\"), artifacts ([]), questions ([]), confidence (0-1), reasoning (1-2 sentences explaining your verdict), plus an extra field \"verdict\" containing exactly one of: escalate | dismiss | investigate.',\n 'Be a tough gatekeeper: escalate only when a human must look. When in doubt, lean dismiss or investigate.',\n ].filter(Boolean).join('\\n');\n\n const startedAt = Date.now();\n try {\n // Use Promise.race to enforce the timeout independent of subagent internals.\n const result = await Promise.race([\n structuredSpawn({\n specialistId: advisor,\n task,\n maxRounds: 2,\n }),\n new Promise<null>((resolve) => setTimeout(() => resolve(null), timeoutMs)),\n ]);\n if (!result) {\n logger.warn(COMPONENT, `${advisor} advisor timed out after ${timeoutMs}ms — failing open (escalate)`);\n return null;\n }\n const raw = result.rawResponse || '';\n // Pull 'verdict' out of the raw JSON if present\n let verdict: PeerVerdict = 'escalate';\n const m = raw.match(/\"verdict\"\\s*:\\s*\"(escalate|dismiss|investigate)\"/i);\n if (m) verdict = m[1].toLowerCase() as PeerVerdict;\n const advice: PeerAdvice = {\n verdict,\n reason: result.reasoning || 'no reason provided',\n confidence: result.confidence,\n advisorSpecialist: advisor,\n };\n logger.info(COMPONENT, `${advisor} verdict=${advice.verdict} confidence=${advice.confidence.toFixed(2)} reason=\"${advice.reason.slice(0, 100)}\" durationMs=${Date.now() - startedAt}`);\n return advice;\n } catch (err) {\n logger.warn(COMPONENT, `peer advise threw: ${(err as Error).message} — failing open (escalate)`);\n return null;\n }\n}\n"],"mappings":";AAmBA,OAAO,YAAY;AACnB,SAAS,uBAAuB;AAEhC,MAAM,YAAY;AA4BlB,MAAM,kBAAkB;AAKxB,MAAM,qBAAqB;AAO3B,eAAsB,WAAW,MAAkD;AAC/E,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,YAAY,KAAK,aAAa;AAEpC,QAAM,OAAO;AAAA,IACT;AAAA,IACA;AAAA,IACA,iBAAiB,KAAK,IAAI;AAAA,IAC1B,YAAY,KAAK,OAAO;AAAA,IACxB,KAAK,UAAU;AAAA,EAAa,KAAK,OAAO,KAAK;AAAA,IAC7C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACJ,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AAE3B,QAAM,YAAY,KAAK,IAAI;AAC3B,MAAI;AAEA,UAAM,SAAS,MAAM,QAAQ,KAAK;AAAA,MAC9B,gBAAgB;AAAA,QACZ,cAAc;AAAA,QACd;AAAA,QACA,WAAW;AAAA,MACf,CAAC;AAAA,MACD,IAAI,QAAc,CAAC,YAAY,WAAW,MAAM,QAAQ,IAAI,GAAG,SAAS,CAAC;AAAA,IAC7E,CAAC;AACD,QAAI,CAAC,QAAQ;AACT,aAAO,KAAK,WAAW,GAAG,OAAO,4BAA4B,SAAS,mCAA8B;AACpG,aAAO;AAAA,IACX;AACA,UAAM,MAAM,OAAO,eAAe;AAElC,QAAI,UAAuB;AAC3B,UAAM,IAAI,IAAI,MAAM,mDAAmD;AACvE,QAAI,EAAG,WAAU,EAAE,CAAC,EAAE,YAAY;AAClC,UAAM,SAAqB;AAAA,MACvB;AAAA,MACA,QAAQ,OAAO,aAAa;AAAA,MAC5B,YAAY,OAAO;AAAA,MACnB,mBAAmB;AAAA,IACvB;AACA,WAAO,KAAK,WAAW,GAAG,OAAO,YAAY,OAAO,OAAO,eAAe,OAAO,WAAW,QAAQ,CAAC,CAAC,YAAY,OAAO,OAAO,MAAM,GAAG,GAAG,CAAC,gBAAgB,KAAK,IAAI,IAAI,SAAS,EAAE;AACrL,WAAO;AAAA,EACX,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,sBAAuB,IAAc,OAAO,iCAA4B;AAC/F,WAAO;AAAA,EACX;AACJ;","names":[]}
@@ -63,9 +63,24 @@ function normalizeTarget(kind, raw) {
63
63
  return t;
64
64
  }
65
65
  }
66
+ const TRANSIENT_FILE_PATTERNS = [
67
+ /^\/tmp\//,
68
+ /^\/var\/tmp\//,
69
+ /^\/private\/tmp\//,
70
+ /^\/run\/user\//,
71
+ /\.tmp(\.|\b)/,
72
+ /~$/
73
+ ];
74
+ function isTransientPath(kind, normalized) {
75
+ if (kind !== "file") return false;
76
+ return TRANSIENT_FILE_PATTERNS.some((re) => re.test(normalized));
77
+ }
66
78
  function recordFixEvent(opts) {
67
79
  const now = /* @__PURE__ */ new Date();
68
80
  const normalized = normalizeTarget(opts.kind, opts.target);
81
+ if (isTransientPath(opts.kind, normalized)) {
82
+ return { oscillation: false, priorCount: 0 };
83
+ }
69
84
  const events = loadRecentEvents();
70
85
  const cutoff = now.getTime() - OSCILLATION_WINDOW_MS;
71
86
  const priors = events.filter(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/safety/fixOscillation.ts"],"sourcesContent":["/**\n * TITAN — Fix Oscillation Detector (v4.9.0+, local hard-takeoff)\n *\n * \"The fix that made it worse\" detector.\n *\n * Every time TITAN modifies a target (file, goal, drive setpoint,\n * prompt), we record a `fixEvent`. If the SAME target gets fixed\n * TWICE within 24h, it's an oscillation — likely either:\n * - The first fix didn't actually work, TITAN is redoing it\n * - The first fix broke something adjacent, TITAN is now patching\n * the breakage\n * - The fix itself keeps oscillating because two contradictory\n * proposals disagree\n *\n * Each oscillation report goes to the kill switch, which fires when\n * ≥3 oscillations hit in a 24h window.\n *\n * This module is intentionally agent-agnostic: it records events\n * describing WHAT was changed, not WHO changed it. Target strings\n * are normalized so different call sites for the same fix collapse\n * correctly.\n *\n * Storage: <TITAN_HOME>/fix-events.jsonl (append-only, bounded).\n */\nimport { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync } from 'fs';\nimport { dirname, join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\nimport { recordFixOscillation } from './killSwitch.js';\n\nconst COMPONENT = 'FixOscillation';\nconst EVENTS_PATH = join(TITAN_HOME, 'fix-events.jsonl');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type FixTargetKind =\n | 'file'\n | 'goal'\n | 'drive'\n | 'prompt'\n | 'config'\n | 'skill'\n | 'approval'\n | 'other';\n\nexport interface FixEvent {\n /** Normalized target identifier. Same target = same string. */\n target: string;\n kind: FixTargetKind;\n /** Short description of what was changed — shows up in audit. */\n detail: string;\n /** Session / agent that made the change. Empty string for system ops. */\n by: string;\n at: string;\n}\n\n// ── Storage ──────────────────────────────────────────────────────\n\nconst OSCILLATION_WINDOW_MS = 24 * 60 * 60 * 1000; // 24h\nconst EVENTS_FILE_MAX_LINES = 5000;\n\nfunction ensureDir(): void {\n try { mkdirSync(dirname(EVENTS_PATH), { recursive: true }); } catch { /* ok */ }\n}\n\n/**\n * Load recent events. We keep the file bounded — beyond MAX_LINES we\n * rewrite with a trailing tail. Cheap O(lines) per boot; acceptable\n * since bounded.\n */\nfunction loadRecentEvents(): FixEvent[] {\n if (!existsSync(EVENTS_PATH)) return [];\n try {\n const raw = readFileSync(EVENTS_PATH, 'utf-8');\n const lines = raw.split('\\n').filter(Boolean);\n const events: FixEvent[] = [];\n for (const line of lines) {\n try {\n const ev = JSON.parse(line) as FixEvent;\n if (ev.target && ev.at) events.push(ev);\n } catch { /* malformed line, skip */ }\n }\n return events;\n } catch (err) {\n logger.warn(COMPONENT, `fix-events.jsonl parse failed: ${(err as Error).message}`);\n return [];\n }\n}\n\nfunction append(event: FixEvent): void {\n ensureDir();\n appendFileSync(EVENTS_PATH, JSON.stringify(event) + '\\n', 'utf-8');\n // Bounded file: rewrite with trailing tail if exceeded.\n try {\n const lines = readFileSync(EVENTS_PATH, 'utf-8').split('\\n');\n if (lines.length > EVENTS_FILE_MAX_LINES) {\n writeFileSync(EVENTS_PATH, lines.slice(-EVENTS_FILE_MAX_LINES).join('\\n'), 'utf-8');\n }\n } catch { /* best-effort */ }\n}\n\n// ── Normalization ────────────────────────────────────────────────\n\n/**\n * Collapse variations of the same target into one identifier. E.g. a\n * file path with a trailing slash, a goal id in different case, a\n * drive with/without the \"soma:\" prefix — all normalize to one key.\n */\nexport function normalizeTarget(kind: FixTargetKind, raw: string): string {\n const t = String(raw ?? '').trim();\n switch (kind) {\n case 'file':\n // Resolve symlinks we can't from here, but collapse to absolute\n // path + strip trailing slash + lowercase on mac.\n return t.replace(/\\/+$/, '').toLowerCase();\n case 'goal':\n return t.toLowerCase();\n case 'drive':\n return t.replace(/^soma:/i, '').toLowerCase();\n case 'prompt':\n case 'config':\n case 'skill':\n return t.toLowerCase();\n case 'approval':\n return t;\n default:\n return t;\n }\n}\n\n// ── Public API ───────────────────────────────────────────────────\n\n/**\n * Record a fix event. If this is the SECOND (or later) fix on the same\n * target within the oscillation window, the kill switch is notified via\n * `recordFixOscillation`. The kill switch itself decides when enough\n * oscillation events have accumulated to fire.\n *\n * Returns `{ oscillation: boolean, priorCount }`:\n * - oscillation=true when this event caused an oscillation to be\n * reported\n * - priorCount = number of prior fix events on the same target\n * within the window\n */\nexport function recordFixEvent(opts: {\n target: string;\n kind: FixTargetKind;\n detail: string;\n by?: string;\n}): { oscillation: boolean; priorCount: number } {\n const now = new Date();\n const normalized = normalizeTarget(opts.kind, opts.target);\n const events = loadRecentEvents();\n const cutoff = now.getTime() - OSCILLATION_WINDOW_MS;\n const priors = events.filter(e =>\n e.kind === opts.kind &&\n normalizeTarget(e.kind, e.target) === normalized &&\n new Date(e.at).getTime() >= cutoff,\n );\n\n const event: FixEvent = {\n target: normalized,\n kind: opts.kind,\n detail: opts.detail.slice(0, 400),\n by: opts.by ?? '',\n at: now.toISOString(),\n };\n append(event);\n\n if (priors.length >= 1) {\n // Same target fixed at least once in the window — this is an\n // oscillation. Notify kill switch, which counts toward its\n // 3-in-24h threshold.\n logger.warn(COMPONENT, `Oscillation on ${opts.kind} \"${normalized.slice(0, 80)}\" (${priors.length + 1}× in 24h): ${opts.detail.slice(0, 100)}`);\n recordFixOscillation(`${opts.kind}:${normalized}`);\n return { oscillation: true, priorCount: priors.length };\n }\n return { oscillation: false, priorCount: 0 };\n}\n\n/**\n * Read-side helper: get all fix events on a target within the window.\n * Useful for the UI's self-repair panel + self-repair daemon.\n */\nexport function getRecentEventsOn(\n kind: FixTargetKind,\n target: string,\n windowMs: number = OSCILLATION_WINDOW_MS,\n): FixEvent[] {\n const normalized = normalizeTarget(kind, target);\n const cutoff = Date.now() - windowMs;\n return loadRecentEvents().filter(e =>\n e.kind === kind\n && normalizeTarget(e.kind, e.target) === normalized\n && new Date(e.at).getTime() >= cutoff,\n );\n}\n\n/**\n * All recent events, newest first. Used by the self-repair daemon to\n * spot patterns we didn't anticipate (e.g. same drive tuned 5× across\n * 5 different targets — not oscillation per-se but noteworthy).\n */\nexport function getAllRecentEvents(windowMs: number = OSCILLATION_WINDOW_MS): FixEvent[] {\n const cutoff = Date.now() - windowMs;\n return loadRecentEvents()\n .filter(e => new Date(e.at).getTime() >= cutoff)\n .sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime());\n}\n\n/** Test-only: wipe the events file. */\nexport function _resetFixEventsForTests(): void {\n try {\n if (existsSync(EVENTS_PATH)) writeFileSync(EVENTS_PATH, '', 'utf-8');\n } catch { /* ok */ }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/safety/fixOscillation.ts"],"sourcesContent":["/**\n * TITAN — Fix Oscillation Detector (v4.9.0+, local hard-takeoff)\n *\n * \"The fix that made it worse\" detector.\n *\n * Every time TITAN modifies a target (file, goal, drive setpoint,\n * prompt), we record a `fixEvent`. If the SAME target gets fixed\n * TWICE within 24h, it's an oscillation — likely either:\n * - The first fix didn't actually work, TITAN is redoing it\n * - The first fix broke something adjacent, TITAN is now patching\n * the breakage\n * - The fix itself keeps oscillating because two contradictory\n * proposals disagree\n *\n * Each oscillation report goes to the kill switch, which fires when\n * ≥3 oscillations hit in a 24h window.\n *\n * This module is intentionally agent-agnostic: it records events\n * describing WHAT was changed, not WHO changed it. Target strings\n * are normalized so different call sites for the same fix collapse\n * correctly.\n *\n * Storage: <TITAN_HOME>/fix-events.jsonl (append-only, bounded).\n */\nimport { existsSync, readFileSync, appendFileSync, mkdirSync, writeFileSync } from 'fs';\nimport { dirname, join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\nimport { recordFixOscillation } from './killSwitch.js';\n\nconst COMPONENT = 'FixOscillation';\nconst EVENTS_PATH = join(TITAN_HOME, 'fix-events.jsonl');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type FixTargetKind =\n | 'file'\n | 'goal'\n | 'drive'\n | 'prompt'\n | 'config'\n | 'skill'\n | 'approval'\n | 'other';\n\nexport interface FixEvent {\n /** Normalized target identifier. Same target = same string. */\n target: string;\n kind: FixTargetKind;\n /** Short description of what was changed — shows up in audit. */\n detail: string;\n /** Session / agent that made the change. Empty string for system ops. */\n by: string;\n at: string;\n}\n\n// ── Storage ──────────────────────────────────────────────────────\n\nconst OSCILLATION_WINDOW_MS = 24 * 60 * 60 * 1000; // 24h\nconst EVENTS_FILE_MAX_LINES = 5000;\n\nfunction ensureDir(): void {\n try { mkdirSync(dirname(EVENTS_PATH), { recursive: true }); } catch { /* ok */ }\n}\n\n/**\n * Load recent events. We keep the file bounded — beyond MAX_LINES we\n * rewrite with a trailing tail. Cheap O(lines) per boot; acceptable\n * since bounded.\n */\nfunction loadRecentEvents(): FixEvent[] {\n if (!existsSync(EVENTS_PATH)) return [];\n try {\n const raw = readFileSync(EVENTS_PATH, 'utf-8');\n const lines = raw.split('\\n').filter(Boolean);\n const events: FixEvent[] = [];\n for (const line of lines) {\n try {\n const ev = JSON.parse(line) as FixEvent;\n if (ev.target && ev.at) events.push(ev);\n } catch { /* malformed line, skip */ }\n }\n return events;\n } catch (err) {\n logger.warn(COMPONENT, `fix-events.jsonl parse failed: ${(err as Error).message}`);\n return [];\n }\n}\n\nfunction append(event: FixEvent): void {\n ensureDir();\n appendFileSync(EVENTS_PATH, JSON.stringify(event) + '\\n', 'utf-8');\n // Bounded file: rewrite with trailing tail if exceeded.\n try {\n const lines = readFileSync(EVENTS_PATH, 'utf-8').split('\\n');\n if (lines.length > EVENTS_FILE_MAX_LINES) {\n writeFileSync(EVENTS_PATH, lines.slice(-EVENTS_FILE_MAX_LINES).join('\\n'), 'utf-8');\n }\n } catch { /* best-effort */ }\n}\n\n// ── Normalization ────────────────────────────────────────────────\n\n/**\n * Collapse variations of the same target into one identifier. E.g. a\n * file path with a trailing slash, a goal id in different case, a\n * drive with/without the \"soma:\" prefix — all normalize to one key.\n */\nexport function normalizeTarget(kind: FixTargetKind, raw: string): string {\n const t = String(raw ?? '').trim();\n switch (kind) {\n case 'file':\n // Resolve symlinks we can't from here, but collapse to absolute\n // path + strip trailing slash + lowercase on mac.\n return t.replace(/\\/+$/, '').toLowerCase();\n case 'goal':\n return t.toLowerCase();\n case 'drive':\n return t.replace(/^soma:/i, '').toLowerCase();\n case 'prompt':\n case 'config':\n case 'skill':\n return t.toLowerCase();\n case 'approval':\n return t;\n default:\n return t;\n }\n}\n\n// ── Public API ───────────────────────────────────────────────────\n\n/**\n * Record a fix event. If this is the SECOND (or later) fix on the same\n * target within the oscillation window, the kill switch is notified via\n * `recordFixOscillation`. The kill switch itself decides when enough\n * oscillation events have accumulated to fire.\n *\n * Returns `{ oscillation: boolean, priorCount }`:\n * - oscillation=true when this event caused an oscillation to be\n * reported\n * - priorCount = number of prior fix events on the same target\n * within the window\n */\n/**\n * v5.5.6: Transient file paths where repeated writes are by design — skip\n * oscillation tracking. These are tmpfs locations and don't represent state\n * the kill-switch should care about. Examples: LLM-generated `/tmp/verdict.json`\n * artefacts that get re-emitted on every sage spawn.\n */\nconst TRANSIENT_FILE_PATTERNS: RegExp[] = [\n /^\\/tmp\\//,\n /^\\/var\\/tmp\\//,\n /^\\/private\\/tmp\\//,\n /^\\/run\\/user\\//,\n /\\.tmp(\\.|\\b)/,\n /~$/,\n];\n\nfunction isTransientPath(kind: FixTargetKind, normalized: string): boolean {\n if (kind !== 'file') return false;\n return TRANSIENT_FILE_PATTERNS.some((re) => re.test(normalized));\n}\n\nexport function recordFixEvent(opts: {\n target: string;\n kind: FixTargetKind;\n detail: string;\n by?: string;\n}): { oscillation: boolean; priorCount: number } {\n const now = new Date();\n const normalized = normalizeTarget(opts.kind, opts.target);\n\n // v5.5.6: skip transient/tmp paths — repeated writes there are by design\n if (isTransientPath(opts.kind, normalized)) {\n return { oscillation: false, priorCount: 0 };\n }\n const events = loadRecentEvents();\n const cutoff = now.getTime() - OSCILLATION_WINDOW_MS;\n const priors = events.filter(e =>\n e.kind === opts.kind &&\n normalizeTarget(e.kind, e.target) === normalized &&\n new Date(e.at).getTime() >= cutoff,\n );\n\n const event: FixEvent = {\n target: normalized,\n kind: opts.kind,\n detail: opts.detail.slice(0, 400),\n by: opts.by ?? '',\n at: now.toISOString(),\n };\n append(event);\n\n if (priors.length >= 1) {\n // Same target fixed at least once in the window — this is an\n // oscillation. Notify kill switch, which counts toward its\n // 3-in-24h threshold.\n logger.warn(COMPONENT, `Oscillation on ${opts.kind} \"${normalized.slice(0, 80)}\" (${priors.length + 1}× in 24h): ${opts.detail.slice(0, 100)}`);\n recordFixOscillation(`${opts.kind}:${normalized}`);\n return { oscillation: true, priorCount: priors.length };\n }\n return { oscillation: false, priorCount: 0 };\n}\n\n/**\n * Read-side helper: get all fix events on a target within the window.\n * Useful for the UI's self-repair panel + self-repair daemon.\n */\nexport function getRecentEventsOn(\n kind: FixTargetKind,\n target: string,\n windowMs: number = OSCILLATION_WINDOW_MS,\n): FixEvent[] {\n const normalized = normalizeTarget(kind, target);\n const cutoff = Date.now() - windowMs;\n return loadRecentEvents().filter(e =>\n e.kind === kind\n && normalizeTarget(e.kind, e.target) === normalized\n && new Date(e.at).getTime() >= cutoff,\n );\n}\n\n/**\n * All recent events, newest first. Used by the self-repair daemon to\n * spot patterns we didn't anticipate (e.g. same drive tuned 5× across\n * 5 different targets — not oscillation per-se but noteworthy).\n */\nexport function getAllRecentEvents(windowMs: number = OSCILLATION_WINDOW_MS): FixEvent[] {\n const cutoff = Date.now() - windowMs;\n return loadRecentEvents()\n .filter(e => new Date(e.at).getTime() >= cutoff)\n .sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime());\n}\n\n/** Test-only: wipe the events file. */\nexport function _resetFixEventsForTests(): void {\n try {\n if (existsSync(EVENTS_PATH)) writeFileSync(EVENTS_PATH, '', 'utf-8');\n } catch { /* ok */ }\n}\n"],"mappings":"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","names":[]}
@@ -4,6 +4,7 @@ import { checkAutoHealOpportunities } from "./autoHealRunner.js";
4
4
  const COMPONENT = "SelfRepair";
5
5
  const findingsByKey = /* @__PURE__ */ new Map();
6
6
  function findingKey(f) {
7
+ if (f.dedupeKey) return f.dedupeKey;
7
8
  return `${f.kind}:${JSON.stringify(f.evidence)}`;
8
9
  }
9
10
  async function runSelfRepairSweep() {
@@ -66,7 +67,8 @@ async function checkDrivesStuckHigh(out) {
66
67
  evidence: { driveId, avgSatisfaction: Math.round(sats.reduce((a, b) => a + b, 0) / sats.length * 100) / 100, sampleCount: sats.length },
67
68
  suggestedAction: `Temporarily dampen ${driveId} drive (lower its weight to 0.5\xD7 or disable for 24h) and investigate why satisfaction can't recover.`,
68
69
  firstSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
69
- severity: driveId === "safety" ? "high" : "medium"
70
+ severity: driveId === "safety" ? "high" : "medium",
71
+ dedupeKey: `drive_stuck_high:${driveId}`
70
72
  });
71
73
  }
72
74
  } catch {
@@ -89,7 +91,8 @@ async function checkGoalsStuckActive(out) {
89
91
  evidence: { goalId: g.id, title: g.title, subtaskCount: subs.length, ageHours: Math.round((Date.now() - startedAt) / 36e5) },
90
92
  suggestedAction: `Split this goal into smaller concrete subtasks OR close it as infeasible.`,
91
93
  firstSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
92
- severity: "medium"
94
+ severity: "medium",
95
+ dedupeKey: `goal_stuck_active:${g.id}`
93
96
  });
94
97
  }
95
98
  } catch {
@@ -107,7 +110,8 @@ async function checkEpisodicAnomaly(out) {
107
110
  evidence: { count: failed, byKind: s.byKind },
108
111
  suggestedAction: `Review recent goals \u2014 either the proposal quality dropped or an underlying subsystem is failing. Consider pausing autopilot until root cause identified.`,
109
112
  firstSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
110
- severity: "high"
113
+ severity: "high",
114
+ dedupeKey: "episodic_anomaly:goal_failed_24h"
111
115
  });
112
116
  }
113
117
  } catch {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/safety/selfRepair.ts"],"sourcesContent":["/**\n * TITAN — Self-Repair Daemon (v4.9.0+, local hard-takeoff)\n *\n * The meta-watcher that orchestrates the safety + memory systems.\n * Runs every 5 minutes (configurable). Each tick it checks the state\n * of the organism across multiple dimensions and, when something's\n * stuck, files a self-repair proposal to the approval queue.\n *\n * The daemon does NOT auto-fix — it proposes. Human-in-the-loop is\n * core: TITAN can detect \"I'm stuck,\" but the decision of what to do\n * about it stays with Tony.\n *\n * Checks:\n * 1. Drive stuck high for > 6h — propose damping / goal reset for\n * that drive\n * 2. Same goal active > 24h with 0 subtask progress — propose split\n * or close\n * 3. Memory file shape drift — auto-repair from backup (and log)\n * 4. Episodic anomaly: >10 goal_failed events in 24h — propose\n * safety investigation\n * 5. Integrity ratio below 0.5 — propose metric-gaming audit\n * 6. Working memory has > 5 open-question sessions > 6h old —\n * propose review\n * 7. Auto-heal: evaluate 6 repair strategies (MissingPackage,\n * BrokenImport, VersionMismatch, OrphanModule, ConfigError,\n * BuildFailure) against findings + fix-oscillation events.\n * Self-modifying repairs file a self_mod_pr approval; safe\n * repairs execute directly (or log in dry-run mode).\n *\n * Each proposal carries a {type:'self_repair', reason, evidence,\n * suggestedAction} payload. Approvals approved by Tony fire the\n * suggested action; rejected ones get archived.\n */\nimport logger from '../utils/logger.js';\nimport { checkAutoHealOpportunities } from './autoHealRunner.js';\n\nconst COMPONENT = 'SelfRepair';\n\n// ── Check result types ──────────────────────────────────────────\n\nexport interface SelfRepairFinding {\n kind:\n | 'drive_stuck_high'\n | 'goal_stuck_active'\n | 'memory_shape_drift'\n | 'episodic_anomaly'\n | 'integrity_low'\n | 'working_memory_stale';\n reason: string;\n evidence: Record<string, unknown>;\n suggestedAction: string;\n /** When this finding first showed up — deduped across ticks. */\n firstSeenAt: string;\n /** Severity drives proposal priority. */\n severity: 'low' | 'medium' | 'high';\n}\n\n// ── Cached findings (dedupe across ticks) ────────────────────────\n\nconst findingsByKey = new Map<string, SelfRepairFinding>();\n\nfunction findingKey(f: Pick<SelfRepairFinding, 'kind' | 'evidence'>): string {\n return `${f.kind}:${JSON.stringify(f.evidence)}`;\n}\n\n// ── The watcher ──────────────────────────────────────────────────\n\n/** Runs a full self-repair sweep. Called by the daemon on its interval. */\nexport async function runSelfRepairSweep(): Promise<SelfRepairFinding[]> {\n const findings: SelfRepairFinding[] = [];\n\n await Promise.all([\n checkDrivesStuckHigh(findings),\n checkGoalsStuckActive(findings),\n checkEpisodicAnomaly(findings),\n checkIntegrityRatio(findings),\n checkWorkingMemoryStale(findings),\n checkTestHealth(findings),\n ]);\n\n // v4.10.0: evaluate auto-heal strategies against findings.\n // Runs in dry-run mode by default — only logs what it would do.\n // Strategies that need to modify /opt/TITAN source files file a\n // self_mod_pr approval instead of executing directly.\n try {\n const healResult = await checkAutoHealOpportunities(findings, true);\n if (healResult.opportunities.length > 0) {\n logger.info(COMPONENT, `Auto-heal: ${healResult.opportunities.length} opportunity(ies), ${healResult.selfModPRsFiled} self_mod_pr(s) filed, ${healResult.executed.length} executed, ${healResult.dryRunSkipped} dry-run skipped`);\n }\n } catch (err) {\n logger.warn(COMPONENT, `Auto-heal check failed: ${(err as Error).message}`);\n }\n\n // Dedupe against prior ticks — only surface new findings.\n const newFindings: SelfRepairFinding[] = [];\n for (const f of findings) {\n const k = findingKey(f);\n if (!findingsByKey.has(k)) {\n findingsByKey.set(k, f);\n newFindings.push(f);\n }\n }\n\n // File approvals for new findings.\n for (const f of newFindings) {\n await fileRepairApproval(f);\n }\n\n // Prune stale findings (kind+evidence combo not seen in 24h)\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const [k, f] of findingsByKey) {\n if (new Date(f.firstSeenAt).getTime() < cutoff) findingsByKey.delete(k);\n }\n\n if (newFindings.length > 0) {\n logger.warn(COMPONENT, `Sweep: ${newFindings.length} new finding(s): ${newFindings.map(f => f.kind).join(', ')}`);\n }\n return findings;\n}\n\n// ── Individual checks ────────────────────────────────────────────\n\nasync function checkDrivesStuckHigh(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { loadDriveHistory } = await import('../organism/drives.js');\n const hist = loadDriveHistory();\n if (!hist || !hist.history || hist.history.length < 30) return;\n // v4.9.0-local.5: filter by TIMESTAMP, not by count. A restart\n // that preserves the history file would otherwise make the check\n // re-read hours-old ticks as if they were recent. Use last 6h\n // by wall clock — tick cadence is 60s so that's ~360 ticks ideally,\n // but we accept any that fall in the window. Require ≥ 30\n // samples in the window to avoid false-positives right after\n // a restart where new ticks haven't accumulated yet.\n const windowStart = Date.now() - 6 * 60 * 60 * 1000;\n const recent = hist.history.filter(h => {\n const t = h.timestamp ? new Date(h.timestamp).getTime() : 0;\n return t >= windowStart;\n });\n if (recent.length < 30) return;\n for (const driveId of ['curiosity', 'hunger', 'purpose', 'safety', 'social'] as const) {\n const sats = recent\n .map(h => (h.satisfactions as Record<string, number>)[driveId])\n .filter((s): s is number => typeof s === 'number');\n if (sats.length < 30) continue;\n // Under 0.3 consistently = stuck high pressure\n const stuck = sats.every(s => s < 0.3);\n if (!stuck) continue;\n out.push({\n kind: 'drive_stuck_high',\n reason: `${driveId} drive satisfaction < 0.3 across all ${sats.length} ticks in the last 6h`,\n evidence: { driveId, avgSatisfaction: Math.round((sats.reduce((a, b) => a + b, 0) / sats.length) * 100) / 100, sampleCount: sats.length },\n suggestedAction: `Temporarily dampen ${driveId} drive (lower its weight to 0.5× or disable for 24h) and investigate why satisfaction can't recover.`,\n firstSeenAt: new Date().toISOString(),\n severity: driveId === 'safety' ? 'high' : 'medium',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkGoalsStuckActive(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { listGoals } = await import('../agent/goals.js');\n const goals = listGoals('active');\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const g of goals) {\n const startedAt = new Date(g.createdAt).getTime();\n if (startedAt > cutoff) continue; // younger than 24h\n const subs = g.subtasks || [];\n const done = subs.filter(s => s.status === 'done').length;\n if (done > 0) continue; // some progress\n out.push({\n kind: 'goal_stuck_active',\n reason: `Goal \"${g.title}\" has been active > 24h with 0 completed subtasks`,\n evidence: { goalId: g.id, title: g.title, subtaskCount: subs.length, ageHours: Math.round((Date.now() - startedAt) / 3_600_000) },\n suggestedAction: `Split this goal into smaller concrete subtasks OR close it as infeasible.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'medium',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkEpisodicAnomaly(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { getEpisodicStats } = await import('../memory/episodic.js');\n const s = getEpisodicStats(24);\n const failed = s.byKind.goal_failed ?? 0;\n if (failed >= 10) {\n out.push({\n kind: 'episodic_anomaly',\n reason: `${failed} goal_failed episodes in the last 24h`,\n evidence: { count: failed, byKind: s.byKind },\n suggestedAction: `Review recent goals — either the proposal quality dropped or an underlying subsystem is failing. Consider pausing autopilot until root cause identified.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'high',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkIntegrityRatio(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { getIntegrityRatio, getMetricGuardStats } = await import('./metricGuard.js');\n const ratio = getIntegrityRatio();\n const stats = getMetricGuardStats();\n // Only meaningful with ≥20 events\n if (stats.verified24h + stats.unverified24h < 20) return;\n if (ratio < 0.5) {\n out.push({\n kind: 'integrity_low',\n reason: `Satisfaction-event integrity ratio ${(ratio * 100).toFixed(1)}% (many unverified self-credits)`,\n evidence: { verified: stats.verified24h, unverified: stats.unverified24h, ratio },\n suggestedAction: `Audit the last 24h of drive-satisfaction events for Goodhart patterns — specifically look for repeated verifier failures from the same source.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'high',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkWorkingMemoryStale(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { listActiveSessions } = await import('../memory/workingMemory.js');\n const active = listActiveSessions();\n const cutoff = Date.now() - 6 * 60 * 60 * 1000;\n const stale = active.filter(r => r.openQuestions.length > 0 && new Date(r.lastActiveAt).getTime() < cutoff);\n if (stale.length >= 5) {\n out.push({\n kind: 'working_memory_stale',\n reason: `${stale.length} sessions have open questions and are >6h idle`,\n evidence: { count: stale.length, sessionIds: stale.map(s => s.sessionId.slice(0, 8)) },\n suggestedAction: `Review these sessions — resolve their open questions, close as abandoned, or revive with fresh attention.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'low',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkTestHealth(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { checkTestHealth: checkTests } = await import('../testing/selfRepairIntegration.js');\n const findings = await checkTests();\n out.push(...findings);\n } catch { /* ok */ }\n}\n\n// ── File the approval ────────────────────────────────────────────\n\nasync function fileRepairApproval(finding: SelfRepairFinding): Promise<void> {\n try {\n const cp = await import('../agent/commandPost.js');\n\n // v4.9.0-local.5: cross-restart dedupe. The in-memory findingsByKey\n // Map resets on restart, so a finding that already exists as a\n // pending approval would get re-filed. Before creating a new\n // approval, scan the approval queue for an existing pending\n // self_repair approval with the same finding kind + evidence.\n // v4.10.0-local: tighter dedupe. Earlier we deep-compared `evidence`,\n // but evidence contains `sampleCount` which ticks upward (360 → 364 →\n // 366) — so the \"same\" finding generated a fresh approval every sweep.\n // Now we match on stable identity fields only: finding.kind + driveId\n // (for drive-stuck findings) OR finding.kind + goalId (for goal-stuck)\n // OR just finding.kind otherwise.\n try {\n const approvals = cp.listApprovals?.() ?? [];\n const ourEvidence = finding.evidence as Record<string, unknown>;\n const stableKey =\n (ourEvidence.driveId as string | undefined) ??\n (ourEvidence.goalId as string | undefined) ??\n '';\n const duplicate = approvals.find((a: { status?: string; type?: string; payload?: Record<string, unknown>; createdAt?: string }) => {\n if (a.status !== 'pending' || a.type !== 'custom') return false;\n if (a.payload?.kind !== 'self_repair') return false;\n if (a.payload?.finding !== finding.kind) return false;\n const ev = (a.payload?.evidence as Record<string, unknown>) || {};\n const theirKey = (ev.driveId as string | undefined) ?? (ev.goalId as string | undefined) ?? '';\n return theirKey === stableKey;\n });\n if (duplicate) {\n logger.debug(COMPONENT, `Skipping duplicate self_repair approval for ${finding.kind}/${stableKey} (existing approval ${(duplicate as { id?: string }).id ?? 'unknown'} pending)`);\n return;\n }\n } catch { /* fall through — if listApprovals fails, still try to create */ }\n\n // v4.13: consult sage (critic) before escalating. If the advisor\n // says dismiss/investigate, log and move on instead of filing\n // an approval Tony has to triage.\n try {\n const { peerAdvise } = await import('../agent/peerAdvise.js');\n const advice = await peerAdvise({\n kind: 'self_repair',\n concern: `Self-repair daemon finding (${finding.severity}): ${finding.reason}`,\n context: `Suggested action: ${finding.suggestedAction}\nEvidence: ${JSON.stringify(finding.evidence).slice(0, 500)}`,\n advisor: 'sage',\n timeoutMs: 20000,\n });\n if (advice && advice.verdict !== 'escalate') {\n logger.info(COMPONENT, `self_repair ${advice.verdict} by sage: ${advice.reason.slice(0, 120)}`);\n return;\n }\n } catch (peerErr) {\n logger.debug(COMPONENT, `peerAdvise failed: ${(peerErr as Error).message} — escalating`);\n }\n\n cp.createApproval({\n type: 'custom',\n requestedBy: 'self-repair-daemon',\n payload: {\n kind: 'self_repair',\n finding: finding.kind,\n reason: finding.reason,\n evidence: finding.evidence,\n suggestedAction: finding.suggestedAction,\n severity: finding.severity,\n },\n linkedIssueIds: [],\n });\n // Record as an episode so the pattern is recallable.\n const { recordEpisode } = await import('../memory/episodic.js');\n recordEpisode({\n kind: 'significant_learning',\n summary: `Self-repair flagged: ${finding.reason}`,\n detail: `Suggested action: ${finding.suggestedAction}`,\n tags: ['self-repair', finding.kind, finding.severity],\n });\n } catch (err) {\n logger.warn(COMPONENT, `file approval failed: ${(err as Error).message}`);\n }\n}\n\nexport function getSelfRepairFindings(): SelfRepairFinding[] {\n return Array.from(findingsByKey.values());\n}\n\n/** Test-only: clear dedupe cache. */\nexport function _resetSelfRepairForTests(): void {\n findingsByKey.clear();\n}\n"],"mappings":";AAiCA,OAAO,YAAY;AACnB,SAAS,kCAAkC;AAE3C,MAAM,YAAY;AAuBlB,MAAM,gBAAgB,oBAAI,IAA+B;AAEzD,SAAS,WAAW,GAAyD;AACzE,SAAO,GAAG,EAAE,IAAI,IAAI,KAAK,UAAU,EAAE,QAAQ,CAAC;AAClD;AAKA,eAAsB,qBAAmD;AACrE,QAAM,WAAgC,CAAC;AAEvC,QAAM,QAAQ,IAAI;AAAA,IACd,qBAAqB,QAAQ;AAAA,IAC7B,sBAAsB,QAAQ;AAAA,IAC9B,qBAAqB,QAAQ;AAAA,IAC7B,oBAAoB,QAAQ;AAAA,IAC5B,wBAAwB,QAAQ;AAAA,IAChC,gBAAgB,QAAQ;AAAA,EAC5B,CAAC;AAMD,MAAI;AACA,UAAM,aAAa,MAAM,2BAA2B,UAAU,IAAI;AAClE,QAAI,WAAW,cAAc,SAAS,GAAG;AACrC,aAAO,KAAK,WAAW,cAAc,WAAW,cAAc,MAAM,sBAAsB,WAAW,eAAe,0BAA0B,WAAW,SAAS,MAAM,cAAc,WAAW,aAAa,kBAAkB;AAAA,IACpO;AAAA,EACJ,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,2BAA4B,IAAc,OAAO,EAAE;AAAA,EAC9E;AAGA,QAAM,cAAmC,CAAC;AAC1C,aAAW,KAAK,UAAU;AACtB,UAAM,IAAI,WAAW,CAAC;AACtB,QAAI,CAAC,cAAc,IAAI,CAAC,GAAG;AACvB,oBAAc,IAAI,GAAG,CAAC;AACtB,kBAAY,KAAK,CAAC;AAAA,IACtB;AAAA,EACJ;AAGA,aAAW,KAAK,aAAa;AACzB,UAAM,mBAAmB,CAAC;AAAA,EAC9B;AAGA,QAAM,SAAS,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK;AAC3C,aAAW,CAAC,GAAG,CAAC,KAAK,eAAe;AAChC,QAAI,IAAI,KAAK,EAAE,WAAW,EAAE,QAAQ,IAAI,OAAQ,eAAc,OAAO,CAAC;AAAA,EAC1E;AAEA,MAAI,YAAY,SAAS,GAAG;AACxB,WAAO,KAAK,WAAW,UAAU,YAAY,MAAM,oBAAoB,YAAY,IAAI,OAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,EAAE;AAAA,EACpH;AACA,SAAO;AACX;AAIA,eAAe,qBAAqB,KAAyC;AACzE,MAAI;AACA,UAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,uBAAuB;AACjE,UAAM,OAAO,iBAAiB;AAC9B,QAAI,CAAC,QAAQ,CAAC,KAAK,WAAW,KAAK,QAAQ,SAAS,GAAI;AAQxD,UAAM,cAAc,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK;AAC/C,UAAM,SAAS,KAAK,QAAQ,OAAO,OAAK;AACpC,YAAM,IAAI,EAAE,YAAY,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI;AAC1D,aAAO,KAAK;AAAA,IAChB,CAAC;AACD,QAAI,OAAO,SAAS,GAAI;AACxB,eAAW,WAAW,CAAC,aAAa,UAAU,WAAW,UAAU,QAAQ,GAAY;AACnF,YAAM,OAAO,OACR,IAAI,OAAM,EAAE,cAAyC,OAAO,CAAC,EAC7D,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ;AACrD,UAAI,KAAK,SAAS,GAAI;AAEtB,YAAM,QAAQ,KAAK,MAAM,OAAK,IAAI,GAAG;AACrC,UAAI,CAAC,MAAO;AACZ,UAAI,KAAK;AAAA,QACL,MAAM;AAAA,QACN,QAAQ,GAAG,OAAO,wCAAwC,KAAK,MAAM;AAAA,QACrE,UAAU,EAAE,SAAS,iBAAiB,KAAK,MAAO,KAAK,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI,KAAK,SAAU,GAAG,IAAI,KAAK,aAAa,KAAK,OAAO;AAAA,QACxI,iBAAiB,sBAAsB,OAAO;AAAA,QAC9C,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,UAAU,YAAY,WAAW,SAAS;AAAA,MAC9C,CAAC;AAAA,IACL;AAAA,EACJ,QAAQ;AAAA,EAAW;AACvB;AAEA,eAAe,sBAAsB,KAAyC;AAC1E,MAAI;AACA,UAAM,EAAE,UAAU,IAAI,MAAM,OAAO,mBAAmB;AACtD,UAAM,QAAQ,UAAU,QAAQ;AAChC,UAAM,SAAS,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK;AAC3C,eAAW,KAAK,OAAO;AACnB,YAAM,YAAY,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ;AAChD,UAAI,YAAY,OAAQ;AACxB,YAAM,OAAO,EAAE,YAAY,CAAC;AAC5B,YAAM,OAAO,KAAK,OAAO,OAAK,EAAE,WAAW,MAAM,EAAE;AACnD,UAAI,OAAO,EAAG;AACd,UAAI,KAAK;AAAA,QACL,MAAM;AAAA,QACN,QAAQ,SAAS,EAAE,KAAK;AAAA,QACxB,UAAU,EAAE,QAAQ,EAAE,IAAI,OAAO,EAAE,OAAO,cAAc,KAAK,QAAQ,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,aAAa,IAAS,EAAE;AAAA,QAChI,iBAAiB;AAAA,QACjB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,UAAU;AAAA,MACd,CAAC;AAAA,IACL;AAAA,EACJ,QAAQ;AAAA,EAAW;AACvB;AAEA,eAAe,qBAAqB,KAAyC;AACzE,MAAI;AACA,UAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,uBAAuB;AACjE,UAAM,IAAI,iBAAiB,EAAE;AAC7B,UAAM,SAAS,EAAE,OAAO,eAAe;AACvC,QAAI,UAAU,IAAI;AACd,UAAI,KAAK;AAAA,QACL,MAAM;AAAA,QACN,QAAQ,GAAG,MAAM;AAAA,QACjB,UAAU,EAAE,OAAO,QAAQ,QAAQ,EAAE,OAAO;AAAA,QAC5C,iBAAiB;AAAA,QACjB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,UAAU;AAAA,MACd,CAAC;AAAA,IACL;AAAA,EACJ,QAAQ;AAAA,EAAW;AACvB;AAEA,eAAe,oBAAoB,KAAyC;AACxE,MAAI;AACA,UAAM,EAAE,mBAAmB,oBAAoB,IAAI,MAAM,OAAO,kBAAkB;AAClF,UAAM,QAAQ,kBAAkB;AAChC,UAAM,QAAQ,oBAAoB;AAElC,QAAI,MAAM,cAAc,MAAM,gBAAgB,GAAI;AAClD,QAAI,QAAQ,KAAK;AACb,UAAI,KAAK;AAAA,QACL,MAAM;AAAA,QACN,QAAQ,uCAAuC,QAAQ,KAAK,QAAQ,CAAC,CAAC;AAAA,QACtE,UAAU,EAAE,UAAU,MAAM,aAAa,YAAY,MAAM,eAAe,MAAM;AAAA,QAChF,iBAAiB;AAAA,QACjB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,UAAU;AAAA,MACd,CAAC;AAAA,IACL;AAAA,EACJ,QAAQ;AAAA,EAAW;AACvB;AAEA,eAAe,wBAAwB,KAAyC;AAC5E,MAAI;AACA,UAAM,EAAE,mBAAmB,IAAI,MAAM,OAAO,4BAA4B;AACxE,UAAM,SAAS,mBAAmB;AAClC,UAAM,SAAS,KAAK,IAAI,IAAI,IAAI,KAAK,KAAK;AAC1C,UAAM,QAAQ,OAAO,OAAO,OAAK,EAAE,cAAc,SAAS,KAAK,IAAI,KAAK,EAAE,YAAY,EAAE,QAAQ,IAAI,MAAM;AAC1G,QAAI,MAAM,UAAU,GAAG;AACnB,UAAI,KAAK;AAAA,QACL,MAAM;AAAA,QACN,QAAQ,GAAG,MAAM,MAAM;AAAA,QACvB,UAAU,EAAE,OAAO,MAAM,QAAQ,YAAY,MAAM,IAAI,OAAK,EAAE,UAAU,MAAM,GAAG,CAAC,CAAC,EAAE;AAAA,QACrF,iBAAiB;AAAA,QACjB,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,QACpC,UAAU;AAAA,MACd,CAAC;AAAA,IACL;AAAA,EACJ,QAAQ;AAAA,EAAW;AACvB;AAEA,eAAe,gBAAgB,KAAyC;AACpE,MAAI;AACA,UAAM,EAAE,iBAAiB,WAAW,IAAI,MAAM,OAAO,qCAAqC;AAC1F,UAAM,WAAW,MAAM,WAAW;AAClC,QAAI,KAAK,GAAG,QAAQ;AAAA,EACxB,QAAQ;AAAA,EAAW;AACvB;AAIA,eAAe,mBAAmB,SAA2C;AACzE,MAAI;AACA,UAAM,KAAK,MAAM,OAAO,yBAAyB;AAajD,QAAI;AACA,YAAM,YAAY,GAAG,gBAAgB,KAAK,CAAC;AAC3C,YAAM,cAAc,QAAQ;AAC5B,YAAM,YACD,YAAY,WACZ,YAAY,UACb;AACJ,YAAM,YAAY,UAAU,KAAK,CAAC,MAAiG;AAC/H,YAAI,EAAE,WAAW,aAAa,EAAE,SAAS,SAAU,QAAO;AAC1D,YAAI,EAAE,SAAS,SAAS,cAAe,QAAO;AAC9C,YAAI,EAAE,SAAS,YAAY,QAAQ,KAAM,QAAO;AAChD,cAAM,KAAM,EAAE,SAAS,YAAwC,CAAC;AAChE,cAAM,WAAY,GAAG,WAAmC,GAAG,UAAiC;AAC5F,eAAO,aAAa;AAAA,MACxB,CAAC;AACD,UAAI,WAAW;AACX,eAAO,MAAM,WAAW,+CAA+C,QAAQ,IAAI,IAAI,SAAS,uBAAwB,UAA8B,MAAM,SAAS,WAAW;AAChL;AAAA,MACJ;AAAA,IACJ,QAAQ;AAAA,IAAmE;AAK3E,QAAI;AACA,YAAM,EAAE,WAAW,IAAI,MAAM,OAAO,wBAAwB;AAC5D,YAAM,SAAS,MAAM,WAAW;AAAA,QAC5B,MAAM;AAAA,QACN,SAAS,+BAA+B,QAAQ,QAAQ,MAAM,QAAQ,MAAM;AAAA,QAC5E,SAAS,qBAAqB,QAAQ,eAAe;AAAA,YACzD,KAAK,UAAU,QAAQ,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC;AAAA,QAC1C,SAAS;AAAA,QACT,WAAW;AAAA,MACf,CAAC;AACD,UAAI,UAAU,OAAO,YAAY,YAAY;AACzC,eAAO,KAAK,WAAW,eAAe,OAAO,OAAO,aAAa,OAAO,OAAO,MAAM,GAAG,GAAG,CAAC,EAAE;AAC9F;AAAA,MACJ;AAAA,IACJ,SAAS,SAAS;AACd,aAAO,MAAM,WAAW,sBAAuB,QAAkB,OAAO,oBAAe;AAAA,IAC3F;AAEA,OAAG,eAAe;AAAA,MACd,MAAM;AAAA,MACN,aAAa;AAAA,MACb,SAAS;AAAA,QACL,MAAM;AAAA,QACN,SAAS,QAAQ;AAAA,QACjB,QAAQ,QAAQ;AAAA,QAChB,UAAU,QAAQ;AAAA,QAClB,iBAAiB,QAAQ;AAAA,QACzB,UAAU,QAAQ;AAAA,MACtB;AAAA,MACA,gBAAgB,CAAC;AAAA,IACrB,CAAC;AAED,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,uBAAuB;AAC9D,kBAAc;AAAA,MACV,MAAM;AAAA,MACN,SAAS,wBAAwB,QAAQ,MAAM;AAAA,MAC/C,QAAQ,qBAAqB,QAAQ,eAAe;AAAA,MACpD,MAAM,CAAC,eAAe,QAAQ,MAAM,QAAQ,QAAQ;AAAA,IACxD,CAAC;AAAA,EACL,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,yBAA0B,IAAc,OAAO,EAAE;AAAA,EAC5E;AACJ;AAEO,SAAS,wBAA6C;AACzD,SAAO,MAAM,KAAK,cAAc,OAAO,CAAC;AAC5C;AAGO,SAAS,2BAAiC;AAC7C,gBAAc,MAAM;AACxB;","names":[]}
1
+ {"version":3,"sources":["../../src/safety/selfRepair.ts"],"sourcesContent":["/**\n * TITAN — Self-Repair Daemon (v4.9.0+, local hard-takeoff)\n *\n * The meta-watcher that orchestrates the safety + memory systems.\n * Runs every 5 minutes (configurable). Each tick it checks the state\n * of the organism across multiple dimensions and, when something's\n * stuck, files a self-repair proposal to the approval queue.\n *\n * The daemon does NOT auto-fix — it proposes. Human-in-the-loop is\n * core: TITAN can detect \"I'm stuck,\" but the decision of what to do\n * about it stays with Tony.\n *\n * Checks:\n * 1. Drive stuck high for > 6h — propose damping / goal reset for\n * that drive\n * 2. Same goal active > 24h with 0 subtask progress — propose split\n * or close\n * 3. Memory file shape drift — auto-repair from backup (and log)\n * 4. Episodic anomaly: >10 goal_failed events in 24h — propose\n * safety investigation\n * 5. Integrity ratio below 0.5 — propose metric-gaming audit\n * 6. Working memory has > 5 open-question sessions > 6h old —\n * propose review\n * 7. Auto-heal: evaluate 6 repair strategies (MissingPackage,\n * BrokenImport, VersionMismatch, OrphanModule, ConfigError,\n * BuildFailure) against findings + fix-oscillation events.\n * Self-modifying repairs file a self_mod_pr approval; safe\n * repairs execute directly (or log in dry-run mode).\n *\n * Each proposal carries a {type:'self_repair', reason, evidence,\n * suggestedAction} payload. Approvals approved by Tony fire the\n * suggested action; rejected ones get archived.\n */\nimport logger from '../utils/logger.js';\nimport { checkAutoHealOpportunities } from './autoHealRunner.js';\n\nconst COMPONENT = 'SelfRepair';\n\n// ── Check result types ──────────────────────────────────────────\n\nexport interface SelfRepairFinding {\n kind:\n | 'drive_stuck_high'\n | 'goal_stuck_active'\n | 'memory_shape_drift'\n | 'episodic_anomaly'\n | 'integrity_low'\n | 'working_memory_stale';\n reason: string;\n evidence: Record<string, unknown>;\n suggestedAction: string;\n /** When this finding first showed up — deduped across ticks. */\n firstSeenAt: string;\n /** Severity drives proposal priority. */\n severity: 'low' | 'medium' | 'high';\n /**\n * Stable identity for cross-tick deduplication. When set, used in place of\n * JSON.stringify(evidence) so rolling stats (sample counts, age) inside\n * evidence don't break dedupe. v5.5.6: emit per (kind,target) only.\n */\n dedupeKey?: string;\n}\n\n// ── Cached findings (dedupe across ticks) ────────────────────────\n\nconst findingsByKey = new Map<string, SelfRepairFinding>();\n\nfunction findingKey(f: Pick<SelfRepairFinding, 'kind' | 'evidence' | 'dedupeKey'>): string {\n if (f.dedupeKey) return f.dedupeKey;\n return `${f.kind}:${JSON.stringify(f.evidence)}`;\n}\n\n// ── The watcher ──────────────────────────────────────────────────\n\n/** Runs a full self-repair sweep. Called by the daemon on its interval. */\nexport async function runSelfRepairSweep(): Promise<SelfRepairFinding[]> {\n const findings: SelfRepairFinding[] = [];\n\n await Promise.all([\n checkDrivesStuckHigh(findings),\n checkGoalsStuckActive(findings),\n checkEpisodicAnomaly(findings),\n checkIntegrityRatio(findings),\n checkWorkingMemoryStale(findings),\n checkTestHealth(findings),\n ]);\n\n // v4.10.0: evaluate auto-heal strategies against findings.\n // Runs in dry-run mode by default — only logs what it would do.\n // Strategies that need to modify /opt/TITAN source files file a\n // self_mod_pr approval instead of executing directly.\n try {\n const healResult = await checkAutoHealOpportunities(findings, true);\n if (healResult.opportunities.length > 0) {\n logger.info(COMPONENT, `Auto-heal: ${healResult.opportunities.length} opportunity(ies), ${healResult.selfModPRsFiled} self_mod_pr(s) filed, ${healResult.executed.length} executed, ${healResult.dryRunSkipped} dry-run skipped`);\n }\n } catch (err) {\n logger.warn(COMPONENT, `Auto-heal check failed: ${(err as Error).message}`);\n }\n\n // Dedupe against prior ticks — only surface new findings.\n const newFindings: SelfRepairFinding[] = [];\n for (const f of findings) {\n const k = findingKey(f);\n if (!findingsByKey.has(k)) {\n findingsByKey.set(k, f);\n newFindings.push(f);\n }\n }\n\n // File approvals for new findings.\n for (const f of newFindings) {\n await fileRepairApproval(f);\n }\n\n // Prune stale findings (kind+evidence combo not seen in 24h)\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const [k, f] of findingsByKey) {\n if (new Date(f.firstSeenAt).getTime() < cutoff) findingsByKey.delete(k);\n }\n\n if (newFindings.length > 0) {\n logger.warn(COMPONENT, `Sweep: ${newFindings.length} new finding(s): ${newFindings.map(f => f.kind).join(', ')}`);\n }\n return findings;\n}\n\n// ── Individual checks ────────────────────────────────────────────\n\nasync function checkDrivesStuckHigh(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { loadDriveHistory } = await import('../organism/drives.js');\n const hist = loadDriveHistory();\n if (!hist || !hist.history || hist.history.length < 30) return;\n // v4.9.0-local.5: filter by TIMESTAMP, not by count. A restart\n // that preserves the history file would otherwise make the check\n // re-read hours-old ticks as if they were recent. Use last 6h\n // by wall clock — tick cadence is 60s so that's ~360 ticks ideally,\n // but we accept any that fall in the window. Require ≥ 30\n // samples in the window to avoid false-positives right after\n // a restart where new ticks haven't accumulated yet.\n const windowStart = Date.now() - 6 * 60 * 60 * 1000;\n const recent = hist.history.filter(h => {\n const t = h.timestamp ? new Date(h.timestamp).getTime() : 0;\n return t >= windowStart;\n });\n if (recent.length < 30) return;\n for (const driveId of ['curiosity', 'hunger', 'purpose', 'safety', 'social'] as const) {\n const sats = recent\n .map(h => (h.satisfactions as Record<string, number>)[driveId])\n .filter((s): s is number => typeof s === 'number');\n if (sats.length < 30) continue;\n // Under 0.3 consistently = stuck high pressure\n const stuck = sats.every(s => s < 0.3);\n if (!stuck) continue;\n out.push({\n kind: 'drive_stuck_high',\n reason: `${driveId} drive satisfaction < 0.3 across all ${sats.length} ticks in the last 6h`,\n evidence: { driveId, avgSatisfaction: Math.round((sats.reduce((a, b) => a + b, 0) / sats.length) * 100) / 100, sampleCount: sats.length },\n suggestedAction: `Temporarily dampen ${driveId} drive (lower its weight to 0.5× or disable for 24h) and investigate why satisfaction can't recover.`,\n firstSeenAt: new Date().toISOString(),\n severity: driveId === 'safety' ? 'high' : 'medium',\n dedupeKey: `drive_stuck_high:${driveId}`,\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkGoalsStuckActive(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { listGoals } = await import('../agent/goals.js');\n const goals = listGoals('active');\n const cutoff = Date.now() - 24 * 60 * 60 * 1000;\n for (const g of goals) {\n const startedAt = new Date(g.createdAt).getTime();\n if (startedAt > cutoff) continue; // younger than 24h\n const subs = g.subtasks || [];\n const done = subs.filter(s => s.status === 'done').length;\n if (done > 0) continue; // some progress\n out.push({\n kind: 'goal_stuck_active',\n reason: `Goal \"${g.title}\" has been active > 24h with 0 completed subtasks`,\n evidence: { goalId: g.id, title: g.title, subtaskCount: subs.length, ageHours: Math.round((Date.now() - startedAt) / 3_600_000) },\n suggestedAction: `Split this goal into smaller concrete subtasks OR close it as infeasible.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'medium',\n dedupeKey: `goal_stuck_active:${g.id}`,\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkEpisodicAnomaly(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { getEpisodicStats } = await import('../memory/episodic.js');\n const s = getEpisodicStats(24);\n const failed = s.byKind.goal_failed ?? 0;\n if (failed >= 10) {\n out.push({\n kind: 'episodic_anomaly',\n reason: `${failed} goal_failed episodes in the last 24h`,\n evidence: { count: failed, byKind: s.byKind },\n suggestedAction: `Review recent goals — either the proposal quality dropped or an underlying subsystem is failing. Consider pausing autopilot until root cause identified.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'high',\n dedupeKey: 'episodic_anomaly:goal_failed_24h',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkIntegrityRatio(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { getIntegrityRatio, getMetricGuardStats } = await import('./metricGuard.js');\n const ratio = getIntegrityRatio();\n const stats = getMetricGuardStats();\n // Only meaningful with ≥20 events\n if (stats.verified24h + stats.unverified24h < 20) return;\n if (ratio < 0.5) {\n out.push({\n kind: 'integrity_low',\n reason: `Satisfaction-event integrity ratio ${(ratio * 100).toFixed(1)}% (many unverified self-credits)`,\n evidence: { verified: stats.verified24h, unverified: stats.unverified24h, ratio },\n suggestedAction: `Audit the last 24h of drive-satisfaction events for Goodhart patterns — specifically look for repeated verifier failures from the same source.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'high',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkWorkingMemoryStale(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { listActiveSessions } = await import('../memory/workingMemory.js');\n const active = listActiveSessions();\n const cutoff = Date.now() - 6 * 60 * 60 * 1000;\n const stale = active.filter(r => r.openQuestions.length > 0 && new Date(r.lastActiveAt).getTime() < cutoff);\n if (stale.length >= 5) {\n out.push({\n kind: 'working_memory_stale',\n reason: `${stale.length} sessions have open questions and are >6h idle`,\n evidence: { count: stale.length, sessionIds: stale.map(s => s.sessionId.slice(0, 8)) },\n suggestedAction: `Review these sessions — resolve their open questions, close as abandoned, or revive with fresh attention.`,\n firstSeenAt: new Date().toISOString(),\n severity: 'low',\n });\n }\n } catch { /* ok */ }\n}\n\nasync function checkTestHealth(out: SelfRepairFinding[]): Promise<void> {\n try {\n const { checkTestHealth: checkTests } = await import('../testing/selfRepairIntegration.js');\n const findings = await checkTests();\n out.push(...findings);\n } catch { /* ok */ }\n}\n\n// ── File the approval ────────────────────────────────────────────\n\nasync function fileRepairApproval(finding: SelfRepairFinding): Promise<void> {\n try {\n const cp = await import('../agent/commandPost.js');\n\n // v4.9.0-local.5: cross-restart dedupe. The in-memory findingsByKey\n // Map resets on restart, so a finding that already exists as a\n // pending approval would get re-filed. Before creating a new\n // approval, scan the approval queue for an existing pending\n // self_repair approval with the same finding kind + evidence.\n // v4.10.0-local: tighter dedupe. Earlier we deep-compared `evidence`,\n // but evidence contains `sampleCount` which ticks upward (360 → 364 →\n // 366) — so the \"same\" finding generated a fresh approval every sweep.\n // Now we match on stable identity fields only: finding.kind + driveId\n // (for drive-stuck findings) OR finding.kind + goalId (for goal-stuck)\n // OR just finding.kind otherwise.\n try {\n const approvals = cp.listApprovals?.() ?? [];\n const ourEvidence = finding.evidence as Record<string, unknown>;\n const stableKey =\n (ourEvidence.driveId as string | undefined) ??\n (ourEvidence.goalId as string | undefined) ??\n '';\n const duplicate = approvals.find((a: { status?: string; type?: string; payload?: Record<string, unknown>; createdAt?: string }) => {\n if (a.status !== 'pending' || a.type !== 'custom') return false;\n if (a.payload?.kind !== 'self_repair') return false;\n if (a.payload?.finding !== finding.kind) return false;\n const ev = (a.payload?.evidence as Record<string, unknown>) || {};\n const theirKey = (ev.driveId as string | undefined) ?? (ev.goalId as string | undefined) ?? '';\n return theirKey === stableKey;\n });\n if (duplicate) {\n logger.debug(COMPONENT, `Skipping duplicate self_repair approval for ${finding.kind}/${stableKey} (existing approval ${(duplicate as { id?: string }).id ?? 'unknown'} pending)`);\n return;\n }\n } catch { /* fall through — if listApprovals fails, still try to create */ }\n\n // v4.13: consult sage (critic) before escalating. If the advisor\n // says dismiss/investigate, log and move on instead of filing\n // an approval Tony has to triage.\n try {\n const { peerAdvise } = await import('../agent/peerAdvise.js');\n const advice = await peerAdvise({\n kind: 'self_repair',\n concern: `Self-repair daemon finding (${finding.severity}): ${finding.reason}`,\n context: `Suggested action: ${finding.suggestedAction}\nEvidence: ${JSON.stringify(finding.evidence).slice(0, 500)}`,\n advisor: 'sage',\n timeoutMs: 20000,\n });\n if (advice && advice.verdict !== 'escalate') {\n logger.info(COMPONENT, `self_repair ${advice.verdict} by sage: ${advice.reason.slice(0, 120)}`);\n return;\n }\n } catch (peerErr) {\n logger.debug(COMPONENT, `peerAdvise failed: ${(peerErr as Error).message} — escalating`);\n }\n\n cp.createApproval({\n type: 'custom',\n requestedBy: 'self-repair-daemon',\n payload: {\n kind: 'self_repair',\n finding: finding.kind,\n reason: finding.reason,\n evidence: finding.evidence,\n suggestedAction: finding.suggestedAction,\n severity: finding.severity,\n },\n linkedIssueIds: [],\n });\n // Record as an episode so the pattern is recallable.\n const { recordEpisode } = await import('../memory/episodic.js');\n recordEpisode({\n kind: 'significant_learning',\n summary: `Self-repair flagged: ${finding.reason}`,\n detail: `Suggested action: ${finding.suggestedAction}`,\n tags: ['self-repair', finding.kind, finding.severity],\n });\n } catch (err) {\n logger.warn(COMPONENT, `file approval failed: ${(err as Error).message}`);\n }\n}\n\nexport function getSelfRepairFindings(): SelfRepairFinding[] {\n return Array.from(findingsByKey.values());\n}\n\n/** Test-only: clear dedupe cache. */\nexport function _resetSelfRepairForTests(): void {\n findingsByKey.clear();\n}\n"],"mappings":"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","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.5";
4
+ const TITAN_VERSION = "5.5.6";
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.5';\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.6';\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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "titan-agent",
3
- "version": "5.5.5",
3
+ "version": "5.5.6",
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": {