titan-agent 6.0.1 → 6.0.2
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.
- package/dist/agent/somaInitiative.js +69 -10
- package/dist/agent/somaInitiative.js.map +1 -1
- package/dist/gateway/routes/agents.js +7 -2
- package/dist/gateway/routes/agents.js.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
- package/ui/dist/sw.js +1 -1
|
@@ -50,23 +50,82 @@ function advisoriesPath(userId) {
|
|
|
50
50
|
const home = env ? env.startsWith("~/") ? join(homedir(), env.slice(2)) : env : join(homedir(), ".titan");
|
|
51
51
|
return join(home, "users", userId, "soma-advisories.jsonl");
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
const DEFAULT_DEDUP_WINDOW_MS = 12 * 60 * 60 * 1e3;
|
|
54
|
+
const DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
55
|
+
function getDedupWindowMs() {
|
|
56
|
+
const raw = process.env.TITAN_SOMA_DEDUP_WINDOW_MS;
|
|
57
|
+
const n = raw ? Number(raw) : NaN;
|
|
58
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_DEDUP_WINDOW_MS;
|
|
59
|
+
}
|
|
60
|
+
function getRetentionMs() {
|
|
61
|
+
const raw = process.env.TITAN_SOMA_ADVISORY_RETENTION_MS;
|
|
62
|
+
const n = raw ? Number(raw) : NaN;
|
|
63
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_RETENTION_MS;
|
|
64
|
+
}
|
|
65
|
+
function parseAdvisoryFile(raw) {
|
|
66
|
+
if (!raw.trim()) return [];
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const line of raw.split("\n")) {
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
if (!trimmed) continue;
|
|
71
|
+
try {
|
|
72
|
+
const rec = JSON.parse(trimmed);
|
|
73
|
+
if (rec && typeof rec.at === "string" && typeof rec.action === "string") out.push(rec);
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function dedupKey(action, rationale) {
|
|
80
|
+
const r = rationale.toLowerCase().replace(/\s+/g, " ").replace(/[.!?]+\s*$/, "").trim();
|
|
81
|
+
return `${action}::${r}`;
|
|
82
|
+
}
|
|
83
|
+
function enqueueAdvisory(userId, decision, now = Date.now()) {
|
|
54
84
|
if (decision.action === "nothing") return;
|
|
55
85
|
const path = advisoriesPath(userId);
|
|
56
86
|
try {
|
|
57
87
|
mkdirSync(join(path, ".."), { recursive: true });
|
|
58
88
|
} catch {
|
|
59
89
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
payload: decision.payload
|
|
66
|
-
});
|
|
90
|
+
const rationale = (decision.rationale ?? "").trim() || decision.action;
|
|
91
|
+
const confidence = typeof decision.confidence === "number" && Number.isFinite(decision.confidence) ? decision.confidence : 0.5;
|
|
92
|
+
const dedupWindow = getDedupWindowMs();
|
|
93
|
+
const retention = getRetentionMs();
|
|
94
|
+
const incomingKey = dedupKey(decision.action, rationale);
|
|
67
95
|
try {
|
|
68
|
-
const
|
|
69
|
-
|
|
96
|
+
const existing = existsSync(path) ? parseAdvisoryFile(readFileSync(path, "utf-8")) : [];
|
|
97
|
+
const retentionCutoff = now - retention;
|
|
98
|
+
const retained = existing.filter((r) => {
|
|
99
|
+
const t = new Date(r.at).getTime();
|
|
100
|
+
return Number.isFinite(t) && t >= retentionCutoff;
|
|
101
|
+
});
|
|
102
|
+
const dedupCutoff = now - dedupWindow;
|
|
103
|
+
const isDup = retained.some((r) => {
|
|
104
|
+
const t = new Date(r.at).getTime();
|
|
105
|
+
if (!Number.isFinite(t) || t < dedupCutoff) return false;
|
|
106
|
+
return dedupKey(r.action, r.rationale) === incomingKey;
|
|
107
|
+
});
|
|
108
|
+
if (isDup) {
|
|
109
|
+
logger.debug(COMPONENT, `Skipping duplicate advisory within ${Math.round(dedupWindow / 36e5)}h window: ${decision.action} \u2014 ${rationale.slice(0, 60)}`);
|
|
110
|
+
if (retained.length !== existing.length) {
|
|
111
|
+
const body2 = retained.map((r) => JSON.stringify(r)).join("\n") + (retained.length > 0 ? "\n" : "");
|
|
112
|
+
writeFileSync(path, body2);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const fresh = {
|
|
117
|
+
at: new Date(now).toISOString(),
|
|
118
|
+
action: decision.action,
|
|
119
|
+
rationale,
|
|
120
|
+
confidence,
|
|
121
|
+
payload: decision.payload
|
|
122
|
+
};
|
|
123
|
+
const final = [...retained, fresh];
|
|
124
|
+
const body = final.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
125
|
+
writeFileSync(path, body);
|
|
126
|
+
if (retained.length !== existing.length) {
|
|
127
|
+
logger.info(COMPONENT, `Pruned ${existing.length - retained.length} stale advisory entries (>${Math.round(retention / 864e5)}d old)`);
|
|
128
|
+
}
|
|
70
129
|
} catch (err) {
|
|
71
130
|
logger.warn(COMPONENT, `Failed to enqueue advisory: ${err.message}`);
|
|
72
131
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/agent/somaInitiative.ts"],"sourcesContent":["/**\n * TITAN — somaInitiative (v6.0 step 11)\n *\n * Why this exists\n * ---------------\n * The Presence thesis: \"TITAN acts without being asked.\" This module is the\n * concrete engine. While the user is idle, a cron-driven scout fires every\n * N minutes:\n *\n * 1. Read the user's pattern aggregates (step 12)\n * 2. Read Soma drive state (step 14 + organism layer)\n * 3. Read time-of-day, recent activity, pending todos\n * 4. Decide if there's a useful surface to surface\n * 5. If yes — fire a side-channel widget event (existing widgetEmitter)\n * labelled \"Soma noticed…\" so the user sees it appeared\n * autonomously\n *\n * Most pulses end in \"nothing to do.\" That's by design — the loop is\n * conservative. Better to miss a chance to help than spam the user with\n * unwanted widgets.\n *\n * Reference:\n * - ~/.claude/projects/-Users-michaelelliott/memory/titan-v6-living-canvas.md\n * (v6.0 step 11)\n * - src/storage/patterns.ts — input signals\n * - src/storage/somaProfile.ts — drive baselines\n * - src/agent/widgetEmitter.ts — output channel\n */\nimport logger from '../utils/logger.js';\nimport { deriveSuggestions, recordSignal, aggregatePatterns } from '../storage/patterns.js';\nimport { readSomaProfile } from '../storage/somaProfile.js';\nimport { getActiveSpace } from '../storage/spaces.js';\n\nconst COMPONENT = 'SomaInitiative';\n\n/** How often the loop fires when active. 5 minutes by Tony's spec. */\nconst PULSE_INTERVAL_MS = 5 * 60 * 1000;\n\n/** Time-of-day buckets the heuristics check against (24h, local). */\nfunction timeBucket(now = new Date()): string {\n const hour = now.getHours();\n const day = now.getDay();\n const dayKind = day === 0 || day === 6 ? 'weekend' : 'weekday';\n return `${dayKind}-${hour.toString().padStart(2, '0')}`;\n}\n\n/** Single pulse — pure function of inputs, no side effects yet. */\nexport interface PulseDecision {\n /** What the loop decided to do this pulse. */\n action: 'nothing' | 'pin-widget' | 'create-space' | 'add-cron' | 'note';\n /** Human-readable explanation (used in logs + the `Soma noticed…` chip). */\n rationale?: string;\n /** Confidence the suggestion is welcome (0..1). */\n confidence?: number;\n /** Optional payload for the action — varies by kind. */\n payload?: Record<string, unknown>;\n}\n\n/**\n * Decide what (if anything) Soma should do this pulse. Pure function — no\n * side effects. The actual emission happens in `executePulse`.\n *\n * Inputs:\n * - userId — whose patterns/profile to read\n * - idleMs — milliseconds since last user activity\n * - opts.minIdleMs — only act when truly idle (default 2 min)\n */\nexport function decidePulse(\n userId = 'default-user',\n idleMs = Infinity,\n opts?: { minIdleMs?: number },\n): PulseDecision {\n const minIdle = opts?.minIdleMs ?? 2 * 60 * 1000;\n if (idleMs < minIdle) {\n return { action: 'nothing', rationale: 'user is active' };\n }\n\n // Record the time bucket as a pattern signal so the loop's own activity\n // becomes part of the user's pattern history. This is harmless and\n // helps timing-based suggestions emerge over time.\n recordSignal(userId, `time:${timeBucket()}`);\n\n const suggestions = deriveSuggestions(userId, { windowDays: 14 });\n if (suggestions.length === 0) {\n return { action: 'nothing', rationale: 'no patterns above confidence threshold' };\n }\n\n // Pick the single highest-confidence suggestion. Future iterations could\n // surface multiple, but conservative-first is the right default.\n const best = suggestions.sort((a, b) => b.confidence - a.confidence)[0];\n\n // Don't suggest the same thing repeatedly — dedup on signal in pattern\n // history. We rely on the pattern aggregation cooldown (only suggests\n // when count >= 3 in window) as the primary throttle; here we layer a\n // small extra check using the active Space's frustration baseline.\n const profile = readSomaProfile(userId);\n if (profile.baseline.frustration > 0.7) {\n // User has been frustrated. Don't pile on with proactive suggestions\n // — they'll feel like noise. Wait for the frustration to subside.\n return { action: 'nothing', rationale: 'soma frustration high — holding suggestions' };\n }\n\n return {\n action: best.kind === 'pin-widget' ? 'pin-widget'\n : best.kind === 'create-space' ? 'create-space'\n : best.kind === 'add-cron' ? 'add-cron'\n : 'note',\n rationale: best.rationale,\n confidence: best.confidence,\n payload: { signal: best.signal, activeSpaceId: getActiveSpace()?.id },\n };\n}\n\n/**\n * Fire one pulse + execute its decision. Used both by the cron driver and\n * by tests to step the loop deterministically.\n *\n * Returns the decision for telemetry / test assertions.\n */\nexport function executePulse(userId = 'default-user', idleMs = Infinity): PulseDecision {\n const d = decidePulse(userId, idleMs);\n if (d.action === 'nothing') {\n // Don't log every \"nothing happened\" tick — too noisy.\n return d;\n }\n logger.info(COMPONENT, `[Pulse] action=${d.action} confidence=${d.confidence?.toFixed(2)} rationale=\"${d.rationale}\"`);\n\n // For now the loop emits a logged advisory rather than auto-creating\n // widgets. Auto-creation requires:\n // - the gateway SSE stream to be open (otherwise the widget event\n // fires into the void)\n // - the agent to author the actual widget code, which it can't do\n // from a non-LLM context\n //\n // The right next step (v6.0+) is to enqueue these advisories so the\n // user sees them in a \"Soma noticed…\" panel; the agent's NEXT chat\n // turn can then pick them up and act. That keeps the loop's\n // side-effects observable + reversible.\n enqueueAdvisory(userId, d);\n return d;\n}\n\n// ─── Advisory queue ─────────────────────────────────────────────────\n\nimport { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nfunction advisoriesPath(userId: string): string {\n const env = process.env.TITAN_HOME;\n const home = env\n ? (env.startsWith('~/') ? join(homedir(), env.slice(2)) : env)\n : join(homedir(), '.titan');\n return join(home, 'users', userId, 'soma-advisories.jsonl');\n}\n\nexport function enqueueAdvisory(userId: string, decision: PulseDecision): void {\n if (decision.action === 'nothing') return;\n const path = advisoriesPath(userId);\n try { mkdirSync(join(path, '..'), { recursive: true }); } catch { /* exists */ }\n const line = JSON.stringify({\n at: new Date().toISOString(),\n action: decision.action,\n rationale: decision.rationale,\n confidence: decision.confidence,\n payload: decision.payload,\n });\n try {\n const prev = existsSync(path) ? readFileSync(path, 'utf-8') : '';\n writeFileSync(path, prev + line + '\\n');\n } catch (err) {\n logger.warn(COMPONENT, `Failed to enqueue advisory: ${(err as Error).message}`);\n }\n}\n\n/** Read recent advisories. Used by the agent loop to surface Soma's\n * observations in the next chat turn. Returns up to `limit` most-recent. */\nexport function readRecentAdvisories(userId: string, limit = 5): Array<{\n at: string;\n action: string;\n rationale: string;\n confidence: number;\n payload?: Record<string, unknown>;\n}> {\n const path = advisoriesPath(userId);\n if (!existsSync(path)) return [];\n try {\n const lines = readFileSync(path, 'utf-8').trim().split('\\n').filter(Boolean);\n return lines.slice(-limit).map(l => JSON.parse(l)).reverse();\n } catch {\n return [];\n }\n}\n\n// ─── Daily proactive-widget gift (v6.0) ────────────────────────────\n//\n// Tony's spec: \"I want the TITAN agent to make a customized widget for\n// its user, from info it knows about the user, every day or so if it\n// wants to.\"\n//\n// Mechanism:\n// 1. Once a day (default — 22h interval, slight jitter), the agent\n// considers gifting the user a custom widget.\n// 2. Pulls the user's Soma profile (long-term observations + baseline\n// drives), recent patterns, and active Space context.\n// 3. Composes a /api/message internal prompt asking the agent to\n// \"design and pin a widget you think the user would appreciate\n// based on what you know about them — or skip if nothing comes to\n// mind.\" Conservative bias: the agent SHOULD skip when in doubt.\n// 4. The agent (real LLM) either calls create_widget itself or\n// politely declines. Either way the decision is logged to\n// `~/.titan/users/<userId>/soma-gifts.jsonl` so we can audit.\n//\n// This is the \"TITAN gifts you a widget on its own, occasionally\"\n// behavior — different from the 5-min advisory pulse which only files\n// advisories without calling the LLM.\n\nconst DAILY_GIFT_INTERVAL_MS = 22 * 60 * 60 * 1000; // 22h, lets it drift\nconst DAILY_GIFT_MIN_GAP_MS = 18 * 60 * 60 * 1000; // never less than 18h between gifts\n\nfunction giftsPath(userId: string): string {\n const env = process.env.TITAN_HOME;\n const home = env\n ? (env.startsWith('~/') ? join(homedir(), env.slice(2)) : env)\n : join(homedir(), '.titan');\n return join(home, 'users', userId, 'soma-gifts.jsonl');\n}\n\n/** Read the most recent gift attempt timestamp (epoch ms), or 0 if none. */\nfunction lastGiftAt(userId: string): number {\n const path = giftsPath(userId);\n if (!existsSync(path)) return 0;\n try {\n const lines = readFileSync(path, 'utf-8').trim().split('\\n').filter(Boolean);\n const last = lines[lines.length - 1];\n if (!last) return 0;\n const entry = JSON.parse(last) as { at?: string };\n return entry.at ? Date.parse(entry.at) : 0;\n } catch { return 0; }\n}\n\nfunction logGiftAttempt(userId: string, payload: Record<string, unknown>): void {\n const path = giftsPath(userId);\n try { mkdirSync(join(path, '..'), { recursive: true }); } catch { /* exists */ }\n const line = JSON.stringify({ at: new Date().toISOString(), ...payload });\n try {\n const prev = existsSync(path) ? readFileSync(path, 'utf-8') : '';\n writeFileSync(path, prev + line + '\\n');\n } catch (err) {\n logger.warn(COMPONENT, `gift log write failed: ${(err as Error).message}`);\n }\n}\n\n/**\n * Decide whether to gift a custom widget today. Pure function — no side\n * effects. Returns null when we should NOT gift right now, or an object\n * with a one-paragraph brief the LLM can use to design the widget.\n */\nexport interface GiftBrief {\n /** Short rationale shown to the user when the widget lands. */\n rationale: string;\n /** Compact context the LLM sees — recent observations + patterns. */\n context: string;\n}\n\nexport function decideDailyGift(userId = 'default-user', now = Date.now(), opts?: { force?: boolean }): GiftBrief | null {\n const force = opts?.force === true;\n if (!force) {\n const since = now - lastGiftAt(userId);\n if (since < DAILY_GIFT_MIN_GAP_MS) return null;\n }\n\n // Pull what we know about the user.\n const profile = readSomaProfile(userId);\n // v6.0.3 — When force=true (manual user trigger from the SOMA panel),\n // skip the \"learned >= 3 observations\" gate. The user explicitly asked\n // for a gift; Soma should at least try, even on day-one with a thin\n // profile. The agent prompt still lets the LLM decline if nothing\n // meaningful comes to mind.\n if (!force && profile.learnedAboutUser.length < 3) {\n return null;\n }\n // Don't gift when frustrated — same throttling as advisories.\n // Manual trigger overrides this too.\n if (!force && profile.baseline.frustration > 0.7) return null;\n\n const recent = profile.learnedAboutUser.slice(-10);\n const aggs = aggregatePatterns(userId, { windowDays: 7, topN: 5 });\n const activeSpace = getActiveSpace();\n\n const context = [\n `User profile (most recent 10 observations):`,\n ...recent.map(o => ` - ${o}`),\n ``,\n `Top patterns last 7 days:`,\n ...(aggs.length === 0\n ? [' (none yet — user is new or quiet)']\n : aggs.map(a => ` - ${a.signal} ×${a.count}`)),\n ``,\n activeSpace ? `Active Space: ${activeSpace.name} (${activeSpace.widgets.length} widget(s) pinned).` : 'No active Space.',\n ].join('\\n');\n\n return {\n rationale: `Soma noticed it's been ~24h since the last surprise. Generating a widget from what's known about the user.`,\n context,\n };\n}\n\n/**\n * Actually execute the daily gift — invokes the LLM via the agent's own\n * `processMessage` flow with a brief that asks the agent to either build\n * a widget or politely decline. The agent's create_widget call (if it\n * makes one) routes through the normal widgetEmitter side-channel.\n */\nexport async function tryDailyGift(userId = 'default-user', opts?: { force?: boolean }): Promise<{ attempted: boolean; reason: string }> {\n const brief = decideDailyGift(userId, Date.now(), opts);\n if (!brief) {\n return { attempted: false, reason: 'cooldown / not enough profile / soma blocked' };\n }\n logger.info(COMPONENT, `[DailyGift] attempting for ${userId}`);\n logGiftAttempt(userId, { phase: 'start' });\n\n const prompt = [\n `You have an opportunity to gift the user a custom widget right now.`,\n `This is a SPONTANEOUS, OPTIONAL gesture — not a response to a request.`,\n `Decline if nothing meaningful comes to mind. Better to skip than to ship something generic.`,\n ``,\n `What you know about THIS user:`,\n brief.context,\n ``,\n `If you decide to build, call \\`create_widget\\` ONCE with a name + source that genuinely reflects something specific to this user (a tracker for a thing they care about, a tool for a recurring task, a dashboard for a domain they work in). Title it something they'd recognize as personal.`,\n ``,\n `If you decline, just reply with one sentence explaining why (\"nothing strong enough today\"). Do NOT make excuses, do NOT promise a future gift.`,\n ].join('\\n');\n\n try {\n // Lazy-import to avoid circular deps at module load.\n const { processMessage } = await import('./agent.js');\n const result = await processMessage(prompt, 'soma-initiative', userId, {});\n const toolCalls = (result.toolsUsed || []).join(', ');\n const widgetMade = (result.toolsUsed || []).includes('create_widget');\n logGiftAttempt(userId, {\n phase: 'done',\n widgetMade,\n toolsUsed: toolCalls,\n content: (result.content || '').slice(0, 240),\n });\n logger.info(COMPONENT, `[DailyGift] ${widgetMade ? 'built a widget' : 'declined'} (tools: ${toolCalls})`);\n return { attempted: true, reason: widgetMade ? 'widget shipped' : 'agent declined' };\n } catch (err) {\n logGiftAttempt(userId, { phase: 'error', error: (err as Error).message });\n logger.warn(COMPONENT, `[DailyGift] error: ${(err as Error).message}`);\n return { attempted: false, reason: `error: ${(err as Error).message}` };\n }\n}\n\n// ─── Driver ────────────────────────────────────────────────────────\n\nlet timerHandle: NodeJS.Timeout | null = null;\nlet dailyGiftHandle: NodeJS.Timeout | null = null;\n\n/** Start the cron-driven pulse loop. Idempotent. */\nexport function startSomaInitiative(opts?: { intervalMs?: number; userId?: string; dailyGiftIntervalMs?: number }): void {\n if (timerHandle) return;\n const interval = opts?.intervalMs ?? PULSE_INTERVAL_MS;\n const userId = opts?.userId ?? 'default-user';\n\n timerHandle = setInterval(() => {\n try {\n // Idle detection is approximate — for now we treat every tick as\n // \"fully idle\" and let `decidePulse`'s conservative confidence\n // threshold do the throttling. Once gateway-side idle tracking\n // wires in (e.g. last /api/message timestamp), this can be\n // replaced with a real `idleMs`.\n executePulse(userId, Infinity);\n } catch (err) {\n logger.warn(COMPONENT, `Pulse error: ${(err as Error).message}`);\n }\n }, interval);\n if (timerHandle.unref) timerHandle.unref();\n logger.info(COMPONENT, `Started somaInitiative loop (interval=${Math.round(interval / 1000)}s, user=${userId}).`);\n\n // v6.0 — Daily proactive-widget gift loop. Fires once every\n // ~22h with jitter; the cooldown check inside decideDailyGift also\n // prevents over-firing if interval is short. Independent timer so\n // its long cadence doesn't gum up the 5-min advisory pulse.\n const giftInterval = opts?.dailyGiftIntervalMs ?? DAILY_GIFT_INTERVAL_MS;\n dailyGiftHandle = setInterval(() => {\n void tryDailyGift(userId);\n }, giftInterval);\n if (dailyGiftHandle.unref) dailyGiftHandle.unref();\n logger.info(COMPONENT, `Started daily-gift loop (interval=${Math.round(giftInterval / 3600_000)}h, user=${userId}).`);\n}\n\nexport function stopSomaInitiative(): void {\n if (timerHandle) { clearInterval(timerHandle); timerHandle = null; }\n if (dailyGiftHandle) { clearInterval(dailyGiftHandle); dailyGiftHandle = null; }\n}\n\nexport function __resetForTests(): void {\n stopSomaInitiative();\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/agent/somaInitiative.ts"],"sourcesContent":["/**\n * TITAN — somaInitiative (v6.0 step 11)\n *\n * Why this exists\n * ---------------\n * The Presence thesis: \"TITAN acts without being asked.\" This module is the\n * concrete engine. While the user is idle, a cron-driven scout fires every\n * N minutes:\n *\n * 1. Read the user's pattern aggregates (step 12)\n * 2. Read Soma drive state (step 14 + organism layer)\n * 3. Read time-of-day, recent activity, pending todos\n * 4. Decide if there's a useful surface to surface\n * 5. If yes — fire a side-channel widget event (existing widgetEmitter)\n * labelled \"Soma noticed…\" so the user sees it appeared\n * autonomously\n *\n * Most pulses end in \"nothing to do.\" That's by design — the loop is\n * conservative. Better to miss a chance to help than spam the user with\n * unwanted widgets.\n *\n * Reference:\n * - ~/.claude/projects/-Users-michaelelliott/memory/titan-v6-living-canvas.md\n * (v6.0 step 11)\n * - src/storage/patterns.ts — input signals\n * - src/storage/somaProfile.ts — drive baselines\n * - src/agent/widgetEmitter.ts — output channel\n */\nimport logger from '../utils/logger.js';\nimport { deriveSuggestions, recordSignal, aggregatePatterns } from '../storage/patterns.js';\nimport { readSomaProfile } from '../storage/somaProfile.js';\nimport { getActiveSpace } from '../storage/spaces.js';\n\nconst COMPONENT = 'SomaInitiative';\n\n/** How often the loop fires when active. 5 minutes by Tony's spec. */\nconst PULSE_INTERVAL_MS = 5 * 60 * 1000;\n\n/** Time-of-day buckets the heuristics check against (24h, local). */\nfunction timeBucket(now = new Date()): string {\n const hour = now.getHours();\n const day = now.getDay();\n const dayKind = day === 0 || day === 6 ? 'weekend' : 'weekday';\n return `${dayKind}-${hour.toString().padStart(2, '0')}`;\n}\n\n/** Single pulse — pure function of inputs, no side effects yet. */\nexport interface PulseDecision {\n /** What the loop decided to do this pulse. */\n action: 'nothing' | 'pin-widget' | 'create-space' | 'add-cron' | 'note';\n /** Human-readable explanation (used in logs + the `Soma noticed…` chip). */\n rationale?: string;\n /** Confidence the suggestion is welcome (0..1). */\n confidence?: number;\n /** Optional payload for the action — varies by kind. */\n payload?: Record<string, unknown>;\n}\n\n/**\n * Decide what (if anything) Soma should do this pulse. Pure function — no\n * side effects. The actual emission happens in `executePulse`.\n *\n * Inputs:\n * - userId — whose patterns/profile to read\n * - idleMs — milliseconds since last user activity\n * - opts.minIdleMs — only act when truly idle (default 2 min)\n */\nexport function decidePulse(\n userId = 'default-user',\n idleMs = Infinity,\n opts?: { minIdleMs?: number },\n): PulseDecision {\n const minIdle = opts?.minIdleMs ?? 2 * 60 * 1000;\n if (idleMs < minIdle) {\n return { action: 'nothing', rationale: 'user is active' };\n }\n\n // Record the time bucket as a pattern signal so the loop's own activity\n // becomes part of the user's pattern history. This is harmless and\n // helps timing-based suggestions emerge over time.\n recordSignal(userId, `time:${timeBucket()}`);\n\n const suggestions = deriveSuggestions(userId, { windowDays: 14 });\n if (suggestions.length === 0) {\n return { action: 'nothing', rationale: 'no patterns above confidence threshold' };\n }\n\n // Pick the single highest-confidence suggestion. Future iterations could\n // surface multiple, but conservative-first is the right default.\n const best = suggestions.sort((a, b) => b.confidence - a.confidence)[0];\n\n // Don't suggest the same thing repeatedly — dedup on signal in pattern\n // history. We rely on the pattern aggregation cooldown (only suggests\n // when count >= 3 in window) as the primary throttle; here we layer a\n // small extra check using the active Space's frustration baseline.\n const profile = readSomaProfile(userId);\n if (profile.baseline.frustration > 0.7) {\n // User has been frustrated. Don't pile on with proactive suggestions\n // — they'll feel like noise. Wait for the frustration to subside.\n return { action: 'nothing', rationale: 'soma frustration high — holding suggestions' };\n }\n\n return {\n action: best.kind === 'pin-widget' ? 'pin-widget'\n : best.kind === 'create-space' ? 'create-space'\n : best.kind === 'add-cron' ? 'add-cron'\n : 'note',\n rationale: best.rationale,\n confidence: best.confidence,\n payload: { signal: best.signal, activeSpaceId: getActiveSpace()?.id },\n };\n}\n\n/**\n * Fire one pulse + execute its decision. Used both by the cron driver and\n * by tests to step the loop deterministically.\n *\n * Returns the decision for telemetry / test assertions.\n */\nexport function executePulse(userId = 'default-user', idleMs = Infinity): PulseDecision {\n const d = decidePulse(userId, idleMs);\n if (d.action === 'nothing') {\n // Don't log every \"nothing happened\" tick — too noisy.\n return d;\n }\n logger.info(COMPONENT, `[Pulse] action=${d.action} confidence=${d.confidence?.toFixed(2)} rationale=\"${d.rationale}\"`);\n\n // For now the loop emits a logged advisory rather than auto-creating\n // widgets. Auto-creation requires:\n // - the gateway SSE stream to be open (otherwise the widget event\n // fires into the void)\n // - the agent to author the actual widget code, which it can't do\n // from a non-LLM context\n //\n // The right next step (v6.0+) is to enqueue these advisories so the\n // user sees them in a \"Soma noticed…\" panel; the agent's NEXT chat\n // turn can then pick them up and act. That keeps the loop's\n // side-effects observable + reversible.\n enqueueAdvisory(userId, d);\n return d;\n}\n\n// ─── Advisory queue ─────────────────────────────────────────────────\n\nimport { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nfunction advisoriesPath(userId: string): string {\n const env = process.env.TITAN_HOME;\n const home = env\n ? (env.startsWith('~/') ? join(homedir(), env.slice(2)) : env)\n : join(homedir(), '.titan');\n return join(home, 'users', userId, 'soma-advisories.jsonl');\n}\n\n// v6.0.2 — Advisory dedup + retention windows.\n//\n// Pre-v6.0.2, `enqueueAdvisory` blindly appended every pulse decision.\n// Since `decidePulse` is stable (same activity pattern → same advisory)\n// the file accumulated hundreds of duplicate entries — 308 in production\n// at the time this fix shipped, of which only ~10 were unique. The\n// SomaAdvisoryToast widget polls this file and surfaces \"new\" entries,\n// so duplicates re-spam the user with the same suggestion every 30s.\n//\n// Fix: before appending, check whether the same (action, rationale) pair\n// was filed within `ADVISORY_DEDUP_WINDOW_MS` (12h default). If yes,\n// silently skip. ALSO prune entries older than `ADVISORY_RETENTION_MS`\n// (7 days) on every write so the file doesn't grow unbounded.\n//\n// Override either window via env (TITAN_SOMA_DEDUP_WINDOW_MS /\n// TITAN_SOMA_ADVISORY_RETENTION_MS) for testing or aggressive tuning.\n\nconst DEFAULT_DEDUP_WINDOW_MS = 12 * 60 * 60 * 1000; // 12h\nconst DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7d\n\nfunction getDedupWindowMs(): number {\n const raw = process.env.TITAN_SOMA_DEDUP_WINDOW_MS;\n const n = raw ? Number(raw) : NaN;\n return Number.isFinite(n) && n >= 0 ? n : DEFAULT_DEDUP_WINDOW_MS;\n}\n\nfunction getRetentionMs(): number {\n const raw = process.env.TITAN_SOMA_ADVISORY_RETENTION_MS;\n const n = raw ? Number(raw) : NaN;\n return Number.isFinite(n) && n >= 0 ? n : DEFAULT_RETENTION_MS;\n}\n\ninterface AdvisoryRecord {\n at: string;\n action: string;\n rationale: string;\n confidence: number;\n payload?: Record<string, unknown>;\n}\n\nfunction parseAdvisoryFile(raw: string): AdvisoryRecord[] {\n if (!raw.trim()) return [];\n const out: AdvisoryRecord[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const rec = JSON.parse(trimmed) as AdvisoryRecord;\n if (rec && typeof rec.at === 'string' && typeof rec.action === 'string') out.push(rec);\n } catch { /* skip corrupt line — never let one bad row break dedup */ }\n }\n return out;\n}\n\n/** Normalize rationale so trivial variations (whitespace, capitalization,\n * trailing punctuation) collapse to the same dedup key. */\nfunction dedupKey(action: string, rationale: string): string {\n const r = rationale.toLowerCase().replace(/\\s+/g, ' ').replace(/[.!?]+\\s*$/, '').trim();\n return `${action}::${r}`;\n}\n\nexport function enqueueAdvisory(userId: string, decision: PulseDecision, now: number = Date.now()): void {\n if (decision.action === 'nothing') return;\n const path = advisoriesPath(userId);\n try { mkdirSync(join(path, '..'), { recursive: true }); } catch { /* exists */ }\n\n // Normalize optionals up-front so dedup + persistence work with\n // concrete values (PulseDecision.rationale/confidence are optional\n // on the type, but every advisory needs a stable form on disk).\n const rationale = (decision.rationale ?? '').trim() || decision.action;\n const confidence = typeof decision.confidence === 'number' && Number.isFinite(decision.confidence) ? decision.confidence : 0.5;\n\n const dedupWindow = getDedupWindowMs();\n const retention = getRetentionMs();\n const incomingKey = dedupKey(decision.action, rationale);\n\n try {\n const existing = existsSync(path) ? parseAdvisoryFile(readFileSync(path, 'utf-8')) : [];\n\n // Prune entries older than retention so the file stays bounded.\n const retentionCutoff = now - retention;\n const retained = existing.filter(r => {\n const t = new Date(r.at).getTime();\n return Number.isFinite(t) && t >= retentionCutoff;\n });\n\n // Dedup: same (action, normalized-rationale) within the dedup window → skip.\n const dedupCutoff = now - dedupWindow;\n const isDup = retained.some(r => {\n const t = new Date(r.at).getTime();\n if (!Number.isFinite(t) || t < dedupCutoff) return false;\n return dedupKey(r.action, r.rationale) === incomingKey;\n });\n if (isDup) {\n // Common case during a steady-state behaviour pattern. Don't\n // spam the agent log either — debug level only.\n logger.debug(COMPONENT, `Skipping duplicate advisory within ${Math.round(dedupWindow / 3600_000)}h window: ${decision.action} — ${rationale.slice(0, 60)}`);\n // Rewrite the pruned file IF we actually pruned anything, so\n // the retention sweep still happens even when we're deduping.\n if (retained.length !== existing.length) {\n const body = retained.map(r => JSON.stringify(r)).join('\\n') + (retained.length > 0 ? '\\n' : '');\n writeFileSync(path, body);\n }\n return;\n }\n\n // New advisory — append.\n const fresh: AdvisoryRecord = {\n at: new Date(now).toISOString(),\n action: decision.action,\n rationale,\n confidence,\n payload: decision.payload,\n };\n const final = [...retained, fresh];\n const body = final.map(r => JSON.stringify(r)).join('\\n') + '\\n';\n writeFileSync(path, body);\n if (retained.length !== existing.length) {\n logger.info(COMPONENT, `Pruned ${existing.length - retained.length} stale advisory entries (>${Math.round(retention / 86400_000)}d old)`);\n }\n } catch (err) {\n logger.warn(COMPONENT, `Failed to enqueue advisory: ${(err as Error).message}`);\n }\n}\n\n/** Read recent advisories. Used by the agent loop to surface Soma's\n * observations in the next chat turn. Returns up to `limit` most-recent. */\nexport function readRecentAdvisories(userId: string, limit = 5): Array<{\n at: string;\n action: string;\n rationale: string;\n confidence: number;\n payload?: Record<string, unknown>;\n}> {\n const path = advisoriesPath(userId);\n if (!existsSync(path)) return [];\n try {\n const lines = readFileSync(path, 'utf-8').trim().split('\\n').filter(Boolean);\n return lines.slice(-limit).map(l => JSON.parse(l)).reverse();\n } catch {\n return [];\n }\n}\n\n// ─── Daily proactive-widget gift (v6.0) ────────────────────────────\n//\n// Tony's spec: \"I want the TITAN agent to make a customized widget for\n// its user, from info it knows about the user, every day or so if it\n// wants to.\"\n//\n// Mechanism:\n// 1. Once a day (default — 22h interval, slight jitter), the agent\n// considers gifting the user a custom widget.\n// 2. Pulls the user's Soma profile (long-term observations + baseline\n// drives), recent patterns, and active Space context.\n// 3. Composes a /api/message internal prompt asking the agent to\n// \"design and pin a widget you think the user would appreciate\n// based on what you know about them — or skip if nothing comes to\n// mind.\" Conservative bias: the agent SHOULD skip when in doubt.\n// 4. The agent (real LLM) either calls create_widget itself or\n// politely declines. Either way the decision is logged to\n// `~/.titan/users/<userId>/soma-gifts.jsonl` so we can audit.\n//\n// This is the \"TITAN gifts you a widget on its own, occasionally\"\n// behavior — different from the 5-min advisory pulse which only files\n// advisories without calling the LLM.\n\nconst DAILY_GIFT_INTERVAL_MS = 22 * 60 * 60 * 1000; // 22h, lets it drift\nconst DAILY_GIFT_MIN_GAP_MS = 18 * 60 * 60 * 1000; // never less than 18h between gifts\n\nfunction giftsPath(userId: string): string {\n const env = process.env.TITAN_HOME;\n const home = env\n ? (env.startsWith('~/') ? join(homedir(), env.slice(2)) : env)\n : join(homedir(), '.titan');\n return join(home, 'users', userId, 'soma-gifts.jsonl');\n}\n\n/** Read the most recent gift attempt timestamp (epoch ms), or 0 if none. */\nfunction lastGiftAt(userId: string): number {\n const path = giftsPath(userId);\n if (!existsSync(path)) return 0;\n try {\n const lines = readFileSync(path, 'utf-8').trim().split('\\n').filter(Boolean);\n const last = lines[lines.length - 1];\n if (!last) return 0;\n const entry = JSON.parse(last) as { at?: string };\n return entry.at ? Date.parse(entry.at) : 0;\n } catch { return 0; }\n}\n\nfunction logGiftAttempt(userId: string, payload: Record<string, unknown>): void {\n const path = giftsPath(userId);\n try { mkdirSync(join(path, '..'), { recursive: true }); } catch { /* exists */ }\n const line = JSON.stringify({ at: new Date().toISOString(), ...payload });\n try {\n const prev = existsSync(path) ? readFileSync(path, 'utf-8') : '';\n writeFileSync(path, prev + line + '\\n');\n } catch (err) {\n logger.warn(COMPONENT, `gift log write failed: ${(err as Error).message}`);\n }\n}\n\n/**\n * Decide whether to gift a custom widget today. Pure function — no side\n * effects. Returns null when we should NOT gift right now, or an object\n * with a one-paragraph brief the LLM can use to design the widget.\n */\nexport interface GiftBrief {\n /** Short rationale shown to the user when the widget lands. */\n rationale: string;\n /** Compact context the LLM sees — recent observations + patterns. */\n context: string;\n}\n\nexport function decideDailyGift(userId = 'default-user', now = Date.now(), opts?: { force?: boolean }): GiftBrief | null {\n const force = opts?.force === true;\n if (!force) {\n const since = now - lastGiftAt(userId);\n if (since < DAILY_GIFT_MIN_GAP_MS) return null;\n }\n\n // Pull what we know about the user.\n const profile = readSomaProfile(userId);\n // v6.0.3 — When force=true (manual user trigger from the SOMA panel),\n // skip the \"learned >= 3 observations\" gate. The user explicitly asked\n // for a gift; Soma should at least try, even on day-one with a thin\n // profile. The agent prompt still lets the LLM decline if nothing\n // meaningful comes to mind.\n if (!force && profile.learnedAboutUser.length < 3) {\n return null;\n }\n // Don't gift when frustrated — same throttling as advisories.\n // Manual trigger overrides this too.\n if (!force && profile.baseline.frustration > 0.7) return null;\n\n const recent = profile.learnedAboutUser.slice(-10);\n const aggs = aggregatePatterns(userId, { windowDays: 7, topN: 5 });\n const activeSpace = getActiveSpace();\n\n const context = [\n `User profile (most recent 10 observations):`,\n ...recent.map(o => ` - ${o}`),\n ``,\n `Top patterns last 7 days:`,\n ...(aggs.length === 0\n ? [' (none yet — user is new or quiet)']\n : aggs.map(a => ` - ${a.signal} ×${a.count}`)),\n ``,\n activeSpace ? `Active Space: ${activeSpace.name} (${activeSpace.widgets.length} widget(s) pinned).` : 'No active Space.',\n ].join('\\n');\n\n return {\n rationale: `Soma noticed it's been ~24h since the last surprise. Generating a widget from what's known about the user.`,\n context,\n };\n}\n\n/**\n * Actually execute the daily gift — invokes the LLM via the agent's own\n * `processMessage` flow with a brief that asks the agent to either build\n * a widget or politely decline. The agent's create_widget call (if it\n * makes one) routes through the normal widgetEmitter side-channel.\n */\nexport async function tryDailyGift(userId = 'default-user', opts?: { force?: boolean }): Promise<{ attempted: boolean; reason: string }> {\n const brief = decideDailyGift(userId, Date.now(), opts);\n if (!brief) {\n return { attempted: false, reason: 'cooldown / not enough profile / soma blocked' };\n }\n logger.info(COMPONENT, `[DailyGift] attempting for ${userId}`);\n logGiftAttempt(userId, { phase: 'start' });\n\n const prompt = [\n `You have an opportunity to gift the user a custom widget right now.`,\n `This is a SPONTANEOUS, OPTIONAL gesture — not a response to a request.`,\n `Decline if nothing meaningful comes to mind. Better to skip than to ship something generic.`,\n ``,\n `What you know about THIS user:`,\n brief.context,\n ``,\n `If you decide to build, call \\`create_widget\\` ONCE with a name + source that genuinely reflects something specific to this user (a tracker for a thing they care about, a tool for a recurring task, a dashboard for a domain they work in). Title it something they'd recognize as personal.`,\n ``,\n `If you decline, just reply with one sentence explaining why (\"nothing strong enough today\"). Do NOT make excuses, do NOT promise a future gift.`,\n ].join('\\n');\n\n try {\n // Lazy-import to avoid circular deps at module load.\n const { processMessage } = await import('./agent.js');\n const result = await processMessage(prompt, 'soma-initiative', userId, {});\n const toolCalls = (result.toolsUsed || []).join(', ');\n const widgetMade = (result.toolsUsed || []).includes('create_widget');\n logGiftAttempt(userId, {\n phase: 'done',\n widgetMade,\n toolsUsed: toolCalls,\n content: (result.content || '').slice(0, 240),\n });\n logger.info(COMPONENT, `[DailyGift] ${widgetMade ? 'built a widget' : 'declined'} (tools: ${toolCalls})`);\n return { attempted: true, reason: widgetMade ? 'widget shipped' : 'agent declined' };\n } catch (err) {\n logGiftAttempt(userId, { phase: 'error', error: (err as Error).message });\n logger.warn(COMPONENT, `[DailyGift] error: ${(err as Error).message}`);\n return { attempted: false, reason: `error: ${(err as Error).message}` };\n }\n}\n\n// ─── Driver ────────────────────────────────────────────────────────\n\nlet timerHandle: NodeJS.Timeout | null = null;\nlet dailyGiftHandle: NodeJS.Timeout | null = null;\n\n/** Start the cron-driven pulse loop. Idempotent. */\nexport function startSomaInitiative(opts?: { intervalMs?: number; userId?: string; dailyGiftIntervalMs?: number }): void {\n if (timerHandle) return;\n const interval = opts?.intervalMs ?? PULSE_INTERVAL_MS;\n const userId = opts?.userId ?? 'default-user';\n\n timerHandle = setInterval(() => {\n try {\n // Idle detection is approximate — for now we treat every tick as\n // \"fully idle\" and let `decidePulse`'s conservative confidence\n // threshold do the throttling. Once gateway-side idle tracking\n // wires in (e.g. last /api/message timestamp), this can be\n // replaced with a real `idleMs`.\n executePulse(userId, Infinity);\n } catch (err) {\n logger.warn(COMPONENT, `Pulse error: ${(err as Error).message}`);\n }\n }, interval);\n if (timerHandle.unref) timerHandle.unref();\n logger.info(COMPONENT, `Started somaInitiative loop (interval=${Math.round(interval / 1000)}s, user=${userId}).`);\n\n // v6.0 — Daily proactive-widget gift loop. Fires once every\n // ~22h with jitter; the cooldown check inside decideDailyGift also\n // prevents over-firing if interval is short. Independent timer so\n // its long cadence doesn't gum up the 5-min advisory pulse.\n const giftInterval = opts?.dailyGiftIntervalMs ?? DAILY_GIFT_INTERVAL_MS;\n dailyGiftHandle = setInterval(() => {\n void tryDailyGift(userId);\n }, giftInterval);\n if (dailyGiftHandle.unref) dailyGiftHandle.unref();\n logger.info(COMPONENT, `Started daily-gift loop (interval=${Math.round(giftInterval / 3600_000)}h, user=${userId}).`);\n}\n\nexport function stopSomaInitiative(): void {\n if (timerHandle) { clearInterval(timerHandle); timerHandle = null; }\n if (dailyGiftHandle) { clearInterval(dailyGiftHandle); dailyGiftHandle = null; }\n}\n\nexport function __resetForTests(): void {\n stopSomaInitiative();\n}\n"],"mappings":"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","names":["body"]}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Router } from "express";
|
|
3
3
|
import logger from "../../utils/logger.js";
|
|
4
4
|
import { setupSSEFlush } from "../../utils/sseFlush.js";
|
|
5
|
-
import { loadConfig } from "../../config/config.js";
|
|
5
|
+
import { loadConfig, saveConfig } from "../../config/config.js";
|
|
6
6
|
import {
|
|
7
7
|
initAutopilot,
|
|
8
8
|
stopAutopilot,
|
|
@@ -74,13 +74,18 @@ function createLifecycleRouter() {
|
|
|
74
74
|
cfg.autopilot.dryRun = dryRun;
|
|
75
75
|
setAutopilotDryRun(dryRun);
|
|
76
76
|
}
|
|
77
|
+
try {
|
|
78
|
+
saveConfig(cfg);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.warn(COMPONENT, `Autopilot toggle saved in memory but FAILED to persist: ${err.message}`);
|
|
81
|
+
}
|
|
77
82
|
if (enable) {
|
|
78
83
|
initAutopilot(cfg);
|
|
79
84
|
} else {
|
|
80
85
|
stopAutopilot();
|
|
81
86
|
}
|
|
82
87
|
const status = getAutopilotStatus();
|
|
83
|
-
res.json({ enabled: enable, dryRun: status.dryRun });
|
|
88
|
+
res.json({ enabled: enable, dryRun: status.dryRun, persisted: true });
|
|
84
89
|
} catch (e) {
|
|
85
90
|
logger.error(COMPONENT, `Endpoint error: ${e.message}`);
|
|
86
91
|
res.status(500).json({ error: "Something went wrong on our end. Please try again in a moment." });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/gateway/routes/agents.ts"],"sourcesContent":["/**\n * Agents Router (Lifecycle)\n *\n * Extracted from gateway/server.ts.\n * Consolidates autopilot, goals, and daemon routes.\n */\n\nimport { Router, type Request, type Response } from 'express';\nimport logger from '../../utils/logger.js';\nimport { setupSSEFlush } from '../../utils/sseFlush.js';\nimport { loadConfig } from '../../config/config.js';\n\n// Autopilot\nimport {\n initAutopilot,\n stopAutopilot,\n runAutopilotNow,\n getAutopilotStatus,\n getRunHistory,\n setAutopilotDryRun,\n} from '../../agent/autopilot.js';\n\n// Goals\nimport {\n listGoals,\n createGoal,\n getGoal,\n deleteGoal,\n updateGoal,\n completeSubtask,\n addSubtask,\n dedupeGoalsBulk,\n} from '../../agent/goals.js';\n\n// Daemon\nimport {\n getDaemonStatus,\n pauseDaemonManual,\n resumeDaemon,\n titanEvents,\n} from '../../agent/daemon.js';\n\nconst COMPONENT = 'AgentsRouter';\n\nconst DAEMON_SSE_EVENTS = ['daemon:started', 'daemon:stopped', 'daemon:paused', 'daemon:resumed',\n 'daemon:heartbeat', 'goal:subtask:ready', 'health:ollama:down',\n 'health:ollama:degraded', 'cron:stuck',\n 'initiative:start', 'initiative:complete', 'initiative:no_progress',\n 'initiative:tool_call', 'initiative:tool_result', 'initiative:round'];\n\nexport function createLifecycleRouter(): Router {\n const router = Router();\n\n // ── Autopilot ───────────────────────────────────────────────\n router.get('/autopilot/status', (_req, res) => {\n res.json(getAutopilotStatus());\n });\n\n router.get('/autopilot/history', (req, res) => {\n const limit = parseInt(req.query.limit as string, 10) || 30;\n res.json(getRunHistory(limit));\n });\n\n router.post('/autopilot/run', async (req, res) => {\n try {\n const dryRun = typeof req.body?.dryRun === 'boolean' ? req.body.dryRun : undefined;\n const result = await runAutopilotNow({ dryRun });\n res.json(result);\n } catch (e) {\n logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' });\n }\n });\n\n router.post('/autopilot/toggle', (req, res) => {\n try {\n const cfg = loadConfig();\n const enable = typeof req.body.enabled === 'boolean' ? req.body.enabled : !cfg.autopilot.enabled;\n const dryRun = typeof req.body.dryRun === 'boolean' ? req.body.dryRun : undefined;\n\n cfg.autopilot.enabled = enable;\n if (typeof dryRun === 'boolean') {\n (cfg.autopilot as Record<string, unknown>).dryRun = dryRun;\n setAutopilotDryRun(dryRun);\n }\n\n if (enable) {\n initAutopilot(cfg);\n } else {\n stopAutopilot();\n }\n const status = getAutopilotStatus();\n res.json({ enabled: enable, dryRun: status.dryRun });\n } catch (e) {\n logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' });\n }\n });\n\n // ── Goals API ─────────────────────────────────────────────\n\n router.get('/goals', (_req, res) => {\n res.json({ goals: listGoals() });\n });\n\n router.post('/goals', (req, res) => {\n const { title, description, subtasks, priority, tags, force } = req.body;\n if (!title) { res.status(400).json({ error: 'title is required' }); return; }\n try {\n const goal = createGoal({\n title,\n description: description || '',\n subtasks: subtasks || [],\n priority,\n tags,\n force: !!force,\n });\n res.status(201).json({ goal });\n } catch (err) {\n res.status(429).json({ error: (err as Error).message });\n }\n });\n\n router.get('/goals/dedupe', (_req, res) => {\n const result = dedupeGoalsBulk();\n res.status(200).json({ success: true, ...result });\n });\n\n router.get('/goals/:id', (req, res) => {\n const goal = getGoal(req.params.id);\n if (!goal) { res.status(404).json({ error: 'Goal not found' }); return; }\n res.json({ goal });\n });\n\n router.delete('/goals/:id', (req, res) => {\n const deleted = deleteGoal(req.params.id);\n if (!deleted) { res.status(404).json({ error: 'Goal not found' }); return; }\n res.json({ deleted: true });\n });\n\n // v4.3.1: update a goal's top-level fields (status, priority, title, description, etc.).\n // Previously the only way to pause a stuck goal was to hand-edit ~/.titan/goals.json and\n // restart the gateway — which is what we did on Titan PC to clear 3 failed Upwork goals.\n // This endpoint closes that gap so the UI \"pause\" action works end-to-end.\n router.patch('/goals/:id', (req, res) => {\n const updated = updateGoal(req.params.id, req.body || {});\n if (!updated) { res.status(404).json({ error: 'Goal not found' }); return; }\n res.json({ goal: updated });\n });\n\n router.post('/goals/:id/subtasks', (req, res) => {\n const { title, description } = req.body;\n if (!title) { res.status(400).json({ error: 'title is required' }); return; }\n const subtask = addSubtask(req.params.id, title, description || '');\n if (!subtask) { res.status(404).json({ error: 'Goal not found' }); return; }\n res.status(201).json({ subtask });\n });\n\n router.post('/goals/:id/subtasks/:sid/complete', (req, res) => {\n const ok = completeSubtask(req.params.id, req.params.sid, req.body.result || 'Completed via UI');\n if (!ok) { res.status(404).json({ error: 'Goal or subtask not found' }); return; }\n res.json({ completed: true });\n });\n\n // v4.1: retry a failed subtask — resets status, clears error, zeros retries.\n router.post('/goals/:id/subtasks/:sid/retry', async (req, res) => {\n const { retrySubtask } = await import('../../agent/goals.js');\n const ok = retrySubtask(req.params.id, req.params.sid);\n if (!ok) { res.status(404).json({ error: 'Goal or subtask not found' }); return; }\n res.json({ retried: true });\n });\n\n // v4.1: edit a subtask's title/description.\n router.patch('/goals/:id/subtasks/:sid', async (req, res) => {\n const { updateSubtask } = await import('../../agent/goals.js');\n const { title, description } = req.body || {};\n const ok = updateSubtask(req.params.id, req.params.sid, { title, description });\n if (!ok) { res.status(404).json({ error: 'Goal or subtask not found' }); return; }\n res.json({ updated: true });\n });\n\n // ── Daemon API ────────────────────────────────────────────\n\n router.get('/daemon/status', (_req, res) => {\n res.json(getDaemonStatus());\n });\n\n router.post('/daemon/stop', (_req, res) => {\n pauseDaemonManual();\n res.json({ paused: true });\n });\n\n router.post('/daemon/resume', (_req, res) => {\n resumeDaemon();\n res.json({ resumed: true });\n });\n\n router.get('/daemon/stream', (req, res) => {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n const sseWrite = setupSSEFlush(res);\n\n const onEvent = (event: string, data: unknown) => {\n try { sseWrite(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`); } catch { /* client gone */ }\n };\n\n const events = DAEMON_SSE_EVENTS;\n\n // Store per-client listener references so we only remove THIS client's listeners on disconnect\n const listeners = new Map<string, (data: unknown) => void>();\n for (const evt of events) {\n const handler = (data: unknown) => onEvent(evt, data);\n listeners.set(evt, handler);\n titanEvents.on(evt, handler);\n }\n\n const keepalive = setInterval(() => {\n try { sseWrite(': keepalive\\n\\n'); } catch { /* client gone */ }\n }, 15_000);\n\n req.on('close', () => {\n clearInterval(keepalive);\n for (const [evt, handler] of listeners) {\n titanEvents.removeListener(evt, handler);\n }\n });\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/gateway/routes/agents.ts"],"sourcesContent":["/**\n * Agents Router (Lifecycle)\n *\n * Extracted from gateway/server.ts.\n * Consolidates autopilot, goals, and daemon routes.\n */\n\nimport { Router, type Request, type Response } from 'express';\nimport logger from '../../utils/logger.js';\nimport { setupSSEFlush } from '../../utils/sseFlush.js';\nimport { loadConfig, saveConfig } from '../../config/config.js';\n\n// Autopilot\nimport {\n initAutopilot,\n stopAutopilot,\n runAutopilotNow,\n getAutopilotStatus,\n getRunHistory,\n setAutopilotDryRun,\n} from '../../agent/autopilot.js';\n\n// Goals\nimport {\n listGoals,\n createGoal,\n getGoal,\n deleteGoal,\n updateGoal,\n completeSubtask,\n addSubtask,\n dedupeGoalsBulk,\n} from '../../agent/goals.js';\n\n// Daemon\nimport {\n getDaemonStatus,\n pauseDaemonManual,\n resumeDaemon,\n titanEvents,\n} from '../../agent/daemon.js';\n\nconst COMPONENT = 'AgentsRouter';\n\nconst DAEMON_SSE_EVENTS = ['daemon:started', 'daemon:stopped', 'daemon:paused', 'daemon:resumed',\n 'daemon:heartbeat', 'goal:subtask:ready', 'health:ollama:down',\n 'health:ollama:degraded', 'cron:stuck',\n 'initiative:start', 'initiative:complete', 'initiative:no_progress',\n 'initiative:tool_call', 'initiative:tool_result', 'initiative:round'];\n\nexport function createLifecycleRouter(): Router {\n const router = Router();\n\n // ── Autopilot ───────────────────────────────────────────────\n router.get('/autopilot/status', (_req, res) => {\n res.json(getAutopilotStatus());\n });\n\n router.get('/autopilot/history', (req, res) => {\n const limit = parseInt(req.query.limit as string, 10) || 30;\n res.json(getRunHistory(limit));\n });\n\n router.post('/autopilot/run', async (req, res) => {\n try {\n const dryRun = typeof req.body?.dryRun === 'boolean' ? req.body.dryRun : undefined;\n const result = await runAutopilotNow({ dryRun });\n res.json(result);\n } catch (e) {\n logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' });\n }\n });\n\n router.post('/autopilot/toggle', (req, res) => {\n try {\n const cfg = loadConfig();\n const enable = typeof req.body.enabled === 'boolean' ? req.body.enabled : !cfg.autopilot.enabled;\n const dryRun = typeof req.body.dryRun === 'boolean' ? req.body.dryRun : undefined;\n\n cfg.autopilot.enabled = enable;\n if (typeof dryRun === 'boolean') {\n (cfg.autopilot as Record<string, unknown>).dryRun = dryRun;\n setAutopilotDryRun(dryRun);\n }\n\n // v6.0.2 — Persist the toggle to disk. Pre-fix this endpoint\n // mutated `cfg` (a reference to the in-memory cache) but never\n // called `saveConfig`, so the change vanished on the next\n // service restart. Result: a user could explicitly disable\n // autopilot, restart the service for any reason, and find the\n // daemon firing again on its 2am cron because the on-disk\n // `autopilot.enabled: true` was reloaded as authoritative.\n try {\n saveConfig(cfg);\n } catch (err) {\n // Persistence failure is loud — the in-memory toggle still\n // takes effect for this process, but the user needs to know\n // it won't survive a restart.\n logger.warn(COMPONENT, `Autopilot toggle saved in memory but FAILED to persist: ${(err as Error).message}`);\n }\n\n if (enable) {\n initAutopilot(cfg);\n } else {\n stopAutopilot();\n }\n const status = getAutopilotStatus();\n res.json({ enabled: enable, dryRun: status.dryRun, persisted: true });\n } catch (e) {\n logger.error(COMPONENT, `Endpoint error: ${(e as Error).message}`); res.status(500).json({ error: 'Something went wrong on our end. Please try again in a moment.' });\n }\n });\n\n // ── Goals API ─────────────────────────────────────────────\n\n router.get('/goals', (_req, res) => {\n res.json({ goals: listGoals() });\n });\n\n router.post('/goals', (req, res) => {\n const { title, description, subtasks, priority, tags, force } = req.body;\n if (!title) { res.status(400).json({ error: 'title is required' }); return; }\n try {\n const goal = createGoal({\n title,\n description: description || '',\n subtasks: subtasks || [],\n priority,\n tags,\n force: !!force,\n });\n res.status(201).json({ goal });\n } catch (err) {\n res.status(429).json({ error: (err as Error).message });\n }\n });\n\n router.get('/goals/dedupe', (_req, res) => {\n const result = dedupeGoalsBulk();\n res.status(200).json({ success: true, ...result });\n });\n\n router.get('/goals/:id', (req, res) => {\n const goal = getGoal(req.params.id);\n if (!goal) { res.status(404).json({ error: 'Goal not found' }); return; }\n res.json({ goal });\n });\n\n router.delete('/goals/:id', (req, res) => {\n const deleted = deleteGoal(req.params.id);\n if (!deleted) { res.status(404).json({ error: 'Goal not found' }); return; }\n res.json({ deleted: true });\n });\n\n // v4.3.1: update a goal's top-level fields (status, priority, title, description, etc.).\n // Previously the only way to pause a stuck goal was to hand-edit ~/.titan/goals.json and\n // restart the gateway — which is what we did on Titan PC to clear 3 failed Upwork goals.\n // This endpoint closes that gap so the UI \"pause\" action works end-to-end.\n router.patch('/goals/:id', (req, res) => {\n const updated = updateGoal(req.params.id, req.body || {});\n if (!updated) { res.status(404).json({ error: 'Goal not found' }); return; }\n res.json({ goal: updated });\n });\n\n router.post('/goals/:id/subtasks', (req, res) => {\n const { title, description } = req.body;\n if (!title) { res.status(400).json({ error: 'title is required' }); return; }\n const subtask = addSubtask(req.params.id, title, description || '');\n if (!subtask) { res.status(404).json({ error: 'Goal not found' }); return; }\n res.status(201).json({ subtask });\n });\n\n router.post('/goals/:id/subtasks/:sid/complete', (req, res) => {\n const ok = completeSubtask(req.params.id, req.params.sid, req.body.result || 'Completed via UI');\n if (!ok) { res.status(404).json({ error: 'Goal or subtask not found' }); return; }\n res.json({ completed: true });\n });\n\n // v4.1: retry a failed subtask — resets status, clears error, zeros retries.\n router.post('/goals/:id/subtasks/:sid/retry', async (req, res) => {\n const { retrySubtask } = await import('../../agent/goals.js');\n const ok = retrySubtask(req.params.id, req.params.sid);\n if (!ok) { res.status(404).json({ error: 'Goal or subtask not found' }); return; }\n res.json({ retried: true });\n });\n\n // v4.1: edit a subtask's title/description.\n router.patch('/goals/:id/subtasks/:sid', async (req, res) => {\n const { updateSubtask } = await import('../../agent/goals.js');\n const { title, description } = req.body || {};\n const ok = updateSubtask(req.params.id, req.params.sid, { title, description });\n if (!ok) { res.status(404).json({ error: 'Goal or subtask not found' }); return; }\n res.json({ updated: true });\n });\n\n // ── Daemon API ────────────────────────────────────────────\n\n router.get('/daemon/status', (_req, res) => {\n res.json(getDaemonStatus());\n });\n\n router.post('/daemon/stop', (_req, res) => {\n pauseDaemonManual();\n res.json({ paused: true });\n });\n\n router.post('/daemon/resume', (_req, res) => {\n resumeDaemon();\n res.json({ resumed: true });\n });\n\n router.get('/daemon/stream', (req, res) => {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n const sseWrite = setupSSEFlush(res);\n\n const onEvent = (event: string, data: unknown) => {\n try { sseWrite(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`); } catch { /* client gone */ }\n };\n\n const events = DAEMON_SSE_EVENTS;\n\n // Store per-client listener references so we only remove THIS client's listeners on disconnect\n const listeners = new Map<string, (data: unknown) => void>();\n for (const evt of events) {\n const handler = (data: unknown) => onEvent(evt, data);\n listeners.set(evt, handler);\n titanEvents.on(evt, handler);\n }\n\n const keepalive = setInterval(() => {\n try { sseWrite(': keepalive\\n\\n'); } catch { /* client gone */ }\n }, 15_000);\n\n req.on('close', () => {\n clearInterval(keepalive);\n for (const [evt, handler] of listeners) {\n titanEvents.removeListener(evt, handler);\n }\n });\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
|
package/dist/utils/constants.js
CHANGED
|
@@ -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 = "6.0.
|
|
4
|
+
const TITAN_VERSION = "6.0.2";
|
|
5
5
|
const TITAN_CODENAME = "Living Canvas";
|
|
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 = '6.0.
|
|
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 = '6.0.2';\nexport const TITAN_CODENAME = 'Living Canvas';\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\n// v6.0.1 — Hardcoded floor only. The actual default at runtime is picked\n// by `getDefaultModelId()` in `src/providers/defaultModel.ts`, which\n// detects the user's available API keys (Anthropic / OpenAI / Google /\n// OpenRouter) and selects accordingly. Falls back to local Ollama when\n// no cloud keys are set. This constant is the last-resort fallback for\n// callsites that don't go through the schema thunk.\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;AASzB,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": "6.0.
|
|
3
|
+
"version": "6.0.2",
|
|
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": {
|
package/ui/dist/sw.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* but a default falls back to the source-controlled value here.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
const CACHE_NAME = 'titan-' + ('
|
|
23
|
+
const CACHE_NAME = 'titan-' + ('1778640198695');
|
|
24
24
|
const ASSETS_PREFIX = '/assets/';
|
|
25
25
|
|
|
26
26
|
self.addEventListener('install', (event) => {
|