titan-agent 6.0.1 → 6.0.3

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/agent/goalProposer.ts"],"sourcesContent":["/**\n * TITAN — Self-Directed Goal Proposer\n *\n * Runs during the nightly dreaming cycle (Phase 4: Dream) after memory\n * consolidation has happened. Each registered agent examines recent activity,\n * open issues, failed subtasks, and consolidation findings, then proposes\n * 0-3 new goals it thinks would be worth doing.\n *\n * Proposals go into the Command Post approval queue as `type: 'goal_proposal'`.\n * A human (or designated approver agent) accepts or rejects them. On accept,\n * the existing createGoal() pipeline fires and Initiative picks up the work.\n *\n * Opt-in via config.agent.autoProposeGoals (default false).\n * Rate-limited per-agent via config.agent.proposalRateLimitPerDay.\n */\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { mkdirIfNotExists } from '../utils/helpers.js';\nimport { loadConfig } from '../config/config.js';\nimport { chat } from '../providers/router.js';\nimport { auxChat, resolveAuxiliaryModel } from '../providers/auxiliary.js';\nimport { applyOutputGuardrails } from './outputGuardrails.js';\nimport { getActivity, requestGoalProposalApproval, type CPApproval } from './commandPost.js';\nimport { listGoals } from './goals.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'GoalProposer';\nconst RATE_STATE_PATH = join(TITAN_HOME, 'goal-proposer-state.json');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport interface ProposedGoal {\n title: string;\n description: string;\n rationale: string;\n priority?: number;\n tags?: string[];\n parentGoalId?: string;\n subtasks?: Array<{ title: string; description: string; dependsOn?: string[] }>;\n}\n\nexport interface GoalProposerContext {\n /** Recent activity feed entries — max last 50. */\n recentActivity?: string[];\n /** Titles of currently active goals so proposals don't duplicate. */\n activeGoals?: string[];\n /** Titles of recently failed subtasks worth retrying or reframing. */\n failedSubtasks?: string[];\n /** Free-form notes from the dreaming consolidation log. */\n consolidationNotes?: string;\n /**\n * v4.9.0-local.4: extra prompt blocks (episodic recall, experiment\n * history, identity) pre-loaded by the caller. Keeps buildPrompt\n * synchronous.\n */\n extraBlocks?: string[];\n}\n\ninterface RateLimitState {\n /** Map of agentId → ISO timestamps of proposals filed in the last 24h. */\n proposalsByAgent: Record<string, string[]>;\n}\n\n// ── Rate Limiting ────────────────────────────────────────────────\n\nfunction loadRateState(): RateLimitState {\n if (!existsSync(RATE_STATE_PATH)) return { proposalsByAgent: {} };\n try {\n const raw = readFileSync(RATE_STATE_PATH, 'utf-8');\n const parsed = JSON.parse(raw) as Partial<RateLimitState>;\n // v4.9.0-local.6: defensive normalize. A prior bug (+ the goal-\n // reset script) can write `{}` to this file, losing the\n // `proposalsByAgent` key. Without this, every\n // `state.proposalsByAgent[agentId]` access crashes with\n // \"Cannot read properties of undefined (reading '<agent>')\"\n // and proposals silently fail for hours.\n return {\n proposalsByAgent: parsed?.proposalsByAgent ?? {},\n };\n } catch {\n return { proposalsByAgent: {} };\n }\n}\n\nfunction saveRateState(state: RateLimitState): void {\n try {\n mkdirIfNotExists(TITAN_HOME);\n writeFileSync(RATE_STATE_PATH, JSON.stringify(state, null, 2), 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `Failed to save rate state: ${(err as Error).message}`);\n }\n}\n\n/** Returns how many slots the agent has remaining in the current 24h window. */\nexport function remainingSlots(agentId: string, limitPerDay: number): number {\n const state = loadRateState();\n const now = Date.now();\n const dayMs = 24 * 3600 * 1000;\n const stamps = (state.proposalsByAgent[agentId] || []).filter(t => now - new Date(t).getTime() < dayMs);\n return Math.max(0, limitPerDay - stamps.length);\n}\n\nfunction recordProposal(agentId: string): void {\n const state = loadRateState();\n const now = Date.now();\n const dayMs = 24 * 3600 * 1000;\n const existing = (state.proposalsByAgent[agentId] || []).filter(t => now - new Date(t).getTime() < dayMs);\n existing.push(new Date().toISOString());\n state.proposalsByAgent[agentId] = existing;\n saveRateState(state);\n}\n\n// ── Prompt ───────────────────────────────────────────────────────\n\nfunction buildPrompt(agentId: string, slotsLeft: number, ctx: GoalProposerContext): string {\n const sections: string[] = [];\n sections.push(`You are agent \"${agentId}\". You have been given a quiet window to reflect on the system's current state and propose new goals that would meaningfully help.`);\n sections.push(`You may propose 0 to ${slotsLeft} goals. It is OK — often preferable — to propose zero if nothing is clearly worth doing.`);\n sections.push('');\n\n if (ctx.activeGoals && ctx.activeGoals.length) {\n sections.push('## Currently Active Goals (do not duplicate)');\n for (const title of ctx.activeGoals.slice(0, 20)) sections.push(`- ${title}`);\n sections.push('');\n }\n if (ctx.recentActivity && ctx.recentActivity.length) {\n sections.push('## Recent Activity (last ~50 events)');\n for (const line of ctx.recentActivity.slice(-50)) sections.push(`- ${line}`);\n sections.push('');\n }\n if (ctx.failedSubtasks && ctx.failedSubtasks.length) {\n sections.push('## Recently Failed Subtasks');\n for (const title of ctx.failedSubtasks.slice(0, 20)) sections.push(`- ${title}`);\n sections.push('');\n }\n if (ctx.consolidationNotes) {\n sections.push('## Memory Consolidation Notes');\n sections.push(ctx.consolidationNotes);\n sections.push('');\n }\n\n // v4.9.0-local.4: extra memory blocks (episodic, experiments,\n // identity) pre-loaded by the async caller and passed through ctx.\n // Keeps buildPrompt synchronous while still giving the proposer\n // full context of what TITAN has already done + who it is.\n if (ctx.extraBlocks && ctx.extraBlocks.length > 0) {\n for (const block of ctx.extraBlocks) {\n if (block && block.trim()) {\n sections.push(block);\n sections.push('');\n }\n }\n }\n\n sections.push('## Output Format');\n sections.push('Return ONLY a JSON array (no prose, no markdown fences). Each element:');\n sections.push('```');\n sections.push('{');\n sections.push(' \"title\": \"short imperative, under 80 chars\",');\n sections.push(' \"description\": \"what success looks like, 1-3 sentences\",');\n sections.push(' \"rationale\": \"why this goal is worth doing NOW\",');\n sections.push(' \"priority\": 1-5 (1 = highest),');\n sections.push(' \"tags\": [\"optional\", \"labels\"],');\n sections.push(' \"subtasks\": [{\"title\": \"...\", \"description\": \"...\"}]');\n sections.push('}');\n sections.push('```');\n sections.push('If nothing is worth proposing, return `[]`. Never return more than the slot limit.');\n\n return sections.join('\\n');\n}\n\n/** JSON schema passed to Ollama's native structured-outputs `format` field.\n * Constrains the model to emit an array of proposal objects matching the\n * fields normalizeProposal() accepts. Belt-and-suspenders — the downstream\n * defensive parser is still the authoritative validator. */\nconst PROPOSAL_ARRAY_SCHEMA: Record<string, unknown> = {\n type: 'array',\n items: {\n type: 'object',\n required: ['title', 'description', 'rationale'],\n properties: {\n title: { type: 'string' },\n description: { type: 'string' },\n rationale: { type: 'string' },\n priority: { type: 'number' },\n tags: { type: 'array', items: { type: 'string' } },\n subtasks: {\n type: 'array',\n items: {\n type: 'object',\n required: ['title', 'description'],\n properties: {\n title: { type: 'string' },\n description: { type: 'string' },\n dependsOn: { type: 'array', items: { type: 'string' } },\n },\n },\n },\n },\n },\n};\n\n// ── JSON Extraction ──────────────────────────────────────────────\n\n/** Defensively parse a JSON array from LLM output. Returns [] on failure. */\nfunction extractProposalArray(raw: string): unknown[] {\n const trimmed = raw.trim();\n // Try direct parse first.\n try {\n const parsed = JSON.parse(trimmed);\n if (Array.isArray(parsed)) return parsed;\n } catch { /* fall through */ }\n // Strip code fences.\n const fenceStripped = trimmed.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '');\n try {\n const parsed = JSON.parse(fenceStripped);\n if (Array.isArray(parsed)) return parsed;\n } catch { /* fall through */ }\n // Find the first `[...]` substring.\n const match = trimmed.match(/\\[[\\s\\S]*\\]/);\n if (match) {\n try {\n const parsed = JSON.parse(match[0]);\n if (Array.isArray(parsed)) return parsed;\n } catch { /* give up */ }\n }\n return [];\n}\n\nfunction normalizeProposal(raw: unknown): ProposedGoal | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n let title = typeof r.title === 'string' ? r.title.trim() : '';\n let description = typeof r.description === 'string' ? r.description.trim() : '';\n const rationale = typeof r.rationale === 'string' ? r.rationale.trim() : '';\n if (!title || !description || !rationale) return null;\n if (title.length > 200 || description.length > 2000 || rationale.length > 2000) return null;\n\n const priority = typeof r.priority === 'number' && r.priority >= 1 && r.priority <= 5\n ? Math.floor(r.priority)\n : undefined;\n let tags: string[] | undefined = Array.isArray(r.tags)\n ? r.tags.filter((t): t is string => typeof t === 'string' && t.length < 40).slice(0, 6)\n : undefined;\n const parentGoalId = typeof r.parentGoalId === 'string' ? r.parentGoalId : undefined;\n\n type Subtask = { title: string; description: string; dependsOn?: string[] };\n let subtasks: Subtask[] | undefined;\n if (Array.isArray(r.subtasks)) {\n const collected: Subtask[] = [];\n for (const s of r.subtasks) {\n if (!s || typeof s !== 'object') continue;\n const rec = s as Record<string, unknown>;\n const t = typeof rec.title === 'string' ? rec.title.trim() : '';\n const d = typeof rec.description === 'string' ? rec.description.trim() : '';\n if (!t || !d) continue;\n const deps = Array.isArray(rec.dependsOn)\n ? rec.dependsOn.filter((x): x is string => typeof x === 'string')\n : undefined;\n // Subtask-level rewrite happens after we know the parent goal's\n // self-mod status (below), so stash refs.\n void t; void d;\n collected.push({ title: t, description: d, dependsOn: deps });\n if (collected.length >= 12) break;\n }\n if (collected.length > 0) subtasks = collected;\n }\n\n // v4.9.0-local.8: self-mod disambiguation. When the proposer emits a\n // goal that sounds like it wants to modify \"the framework\" / \"core\" /\n // etc, the specialist picking it up historically interpreted this as\n // \"build something under ~/titan-saas\" because that's where Next.js\n // project scaffolding lives. We close the ambiguity at creation time:\n //\n // 1. Detect self-mod trigger words in title + description + tags\n // 2. If matched, ensure tags include 'self-mod' so the toolRunner\n // scope-lock + staging gate activates for work on this goal\n // 3. Append an explicit scope-lock note to the description pointing\n // at the actual target path\n // 4. Rewrite common ambiguous phrases in subtasks to spell out the\n // target path\n const selfModTriggers = [\n /\\bself[\\s-]?heal/i,\n /\\bself[\\s-]?repair/i,\n /\\bself[\\s-]?mod/i,\n /\\bcore[\\s-]framework/i,\n /\\bTITAN['’]?s?\\s+(own|core|framework|architecture|source|runtime)/i,\n /\\bframework\\s+(component|module|core|runtime)/i,\n ];\n const selfModTagValues = new Set([\n 'self-healing', 'self-repair', 'self-mod', 'self-modification',\n 'core-framework', 'framework', 'architecture', 'core', 'autonomy',\n ]);\n const tagsLower = new Set((tags || []).map(t => t.toLowerCase()));\n const haystack = `${title}\\n${description}\\n${(tags || []).join(' ')}`;\n const matchedByText = selfModTriggers.some(re => re.test(haystack));\n const matchedByTag = [...tagsLower].some(t => selfModTagValues.has(t));\n const isSelfMod = matchedByText || matchedByTag;\n\n if (isSelfMod) {\n const cfg = loadConfig();\n const target = (cfg.autonomy?.selfMod as { target?: string } | undefined)?.target || '/opt/TITAN';\n // Ensure the canonical 'self-mod' tag is present so toolRunner sees it\n tags = tags ? [...tags] : [];\n if (!tagsLower.has('self-mod')) tags.push('self-mod');\n tags = tags.slice(0, 6);\n\n // Append an unmistakable scope-lock note to the description — the\n // specialist reading this goal sees the target path explicitly\n // instead of having to infer \"the framework\" from context.\n const scopeNote = `\\n\\n[SCOPE-LOCK] This is a self-modification goal. All file writes MUST target ${target} (TITAN's own source tree). Writes to any other path will be refused by the toolRunner scope-lock. When staging is enabled, writes are diverted to ${target}/../self-mod-staging/<goalId>/ and surface as a self_mod_pr approval for human review before applying.`;\n if (!/\\[SCOPE-LOCK\\]/.test(description)) {\n description = (description + scopeNote).slice(0, 2000);\n }\n\n // Rewrite common ambiguous phrases in title/subtasks so the\n // specialist-level prompt mentions the target explicitly.\n const rewrite = (s: string): string => s\n .replace(/\\b(the\\s+)?core\\s+framework\\b/gi, `${target} (TITAN core framework)`)\n .replace(/\\b(the\\s+)?(TITAN\\s+)?framework\\b(?!\\s+component)/gi, `${target} (TITAN framework)`);\n title = rewrite(title).slice(0, 200);\n if (subtasks) {\n subtasks = subtasks.map(s => ({\n title: rewrite(s.title).slice(0, 200),\n description: rewrite(s.description).slice(0, 2000),\n dependsOn: s.dependsOn,\n }));\n }\n }\n\n return { title, description, rationale, priority, tags, parentGoalId, subtasks };\n}\n\n// ── Main Entry Point ─────────────────────────────────────────────\n\n/**\n * Generate goal proposals for a single agent and file them as pending approvals.\n * Returns the list of CPApproval records created (may be empty).\n *\n * Called by the dreaming watcher's Phase 4 (Dream). Safe to call ad-hoc from\n * debug endpoints or tests.\n */\nexport async function generateGoalProposals(\n agentId: string,\n ctx: GoalProposerContext,\n type: 'goal_proposal' | 'soma_proposal' = 'goal_proposal'\n): Promise<CPApproval[]> {\n const config = loadConfig();\n const enabled = config.agent.autoProposeGoals;\n if (!enabled) {\n logger.debug(COMPONENT, `autoProposeGoals disabled — skipping for agent ${agentId}`);\n return [];\n }\n\n const limit = config.agent.proposalRateLimitPerDay;\n const slotsLeft = remainingSlots(agentId, limit);\n if (slotsLeft <= 0) {\n logger.info(COMPONENT, `Agent ${agentId} has hit daily proposal limit (${limit}) — skipping`);\n return [];\n }\n\n const modelAlias = config.agent.proposalModel || 'fast';\n const model = config.agent.modelAliases[modelAlias] || modelAlias;\n\n // v4.9.0-local.4: pre-load extra memory blocks (episodic, experiments,\n // identity) before building the proposer prompt. Closes the repeat-\n // task feedback loop — the proposer now sees what TITAN has recently\n // done and won't re-propose the same ant colony sim three times.\n // Each block is best-effort; silent fallthrough if a module isn't\n // available at proposer time.\n const extraBlocks: string[] = [];\n try {\n const { renderRecallBlock } = await import('../memory/episodic.js');\n const block = renderRecallBlock({ limit: 12, windowHours: 72 });\n if (block) extraBlocks.push(block);\n } catch { /* ok */ }\n try {\n const { renderRecentExperimentsBlock } = await import('../memory/experiments.js');\n const block = renderRecentExperimentsBlock(8);\n if (block) extraBlocks.push(block);\n } catch { /* ok */ }\n try {\n const { getIdentity } = await import('../memory/identity.js');\n const id = getIdentity();\n if (id) {\n extraBlocks.push([\n '## Your identity (persistent)',\n `Mission: ${id.core.mission}`,\n `Non-negotiables: ${id.core.nonNegotiables.slice(0, 3).join('; ')}`,\n 'Propose ONLY goals that align with the mission and never violate a non-negotiable.',\n ].join('\\n'));\n }\n } catch { /* ok */ }\n\n const ctxWithBlocks: GoalProposerContext = { ...ctx, extraBlocks };\n const prompt = buildPrompt(agentId, slotsLeft, ctxWithBlocks);\n\n // v4.13 ancestor-extraction: route goal-proposal JSON extraction through\n // the auxiliary model client. The main agent model (gemma4:31b on the\n // Titan PC default) produces empty arrays for structured JSON tasks; a\n // dedicated fast+cheap model (minimax-m2.7:cloud) reliably produces valid\n // proposals. Falls back to the main `model` when no auxiliary is\n // configured.\n const auxModel = resolveAuxiliaryModel('json_extraction');\n const effectiveModel = auxModel || model;\n const isOllamaEffective = effectiveModel.toLowerCase().startsWith('ollama/');\n\n let rawContent: string;\n try {\n const response = await auxChat(\n 'json_extraction',\n {\n messages: [\n { role: 'system', content: 'You are a careful autonomous agent proposing new work. Output ONLY valid JSON. No explanation, no prose.' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.4,\n maxTokens: 1500,\n ...(isOllamaEffective ? { format: PROPOSAL_ARRAY_SCHEMA } : {}),\n },\n model, // fallback to main agent model if no aux is configured\n );\n if (!response) {\n logger.warn(COMPONENT, `Auxiliary call returned null for agent ${agentId} — treating as no proposals`);\n return [];\n }\n rawContent = response.content || '';\n if (auxModel && auxModel !== model) {\n logger.info(COMPONENT, `Agent ${agentId} goal-proposal routed via auxiliary model ${auxModel} (main: ${model})`);\n }\n } catch (err) {\n logger.warn(COMPONENT, `LLM call failed for agent ${agentId}: ${(err as Error).message}`);\n return [];\n }\n\n // Strip chain-of-thought leakage before parsing JSON.\n const guarded = applyOutputGuardrails(rawContent, { type: 'sub_agent' });\n const parsed = extractProposalArray(guarded.content);\n if (parsed.length === 0) {\n logger.info(COMPONENT, `Agent ${agentId} proposed no goals (parsed empty array)`);\n return [];\n }\n\n const proposals: ProposedGoal[] = [];\n for (const item of parsed) {\n const normalized = normalizeProposal(item);\n if (normalized) proposals.push(normalized);\n if (proposals.length >= slotsLeft) break;\n }\n\n // v5.0.0: dedupe against ALL recent goals (not just active) and enforce\n // goal-overload backoff. Prevents the runaway loops that produced 1000+\n // duplicate \"Publish content\" goals.\n let allGoalTitles: string[] = [];\n let activeGoalCount = 0;\n try {\n const { listGoals } = await import('./goals.js');\n const all = listGoals();\n allGoalTitles = all.map(g => g.title);\n activeGoalCount = all.filter(g => g.status === 'active').length;\n } catch { /* best-effort */ }\n\n // Overload backoff: if the system is already swamped, only allow\n // cleanup / meta proposals (titles containing \"resolve\", \"cancel\",\n // \"close\", \"audit\", \"clean\", \"dedupe\"). Everything else is deferred\n // until the backlog drops.\n const isOverload = activeGoalCount >= 25;\n const isCleanupProposal = (t: string) => /\\b(resolve|cancel|close|audit|clean|dedupe|consolidate|prune)\\b/i.test(t);\n\n const approvals: CPApproval[] = [];\n for (const proposal of proposals) {\n // Dedupe against ANY existing goal (active, paused, completed, failed)\n const dup = allGoalTitles.find(t => titleSimilarity(t, proposal.title) >= 0.72);\n if (dup) {\n logger.info(COMPONENT, `Agent ${agentId} skipped duplicate proposal \"${proposal.title}\" (matches existing goal \"${dup}\")`);\n continue;\n }\n // Overload gate\n if (isOverload && !isCleanupProposal(proposal.title)) {\n logger.info(COMPONENT, `Agent ${agentId} skipped proposal \"${proposal.title}\" — goal overload (${activeGoalCount} active). Only cleanup proposals allowed.`);\n continue;\n }\n try {\n const approval = requestGoalProposalApproval(agentId, proposal, type);\n approvals.push(approval);\n recordProposal(agentId);\n allGoalTitles.push(proposal.title); // prevent intra-batch dupes too\n logger.info(COMPONENT, `Agent ${agentId} filed proposal \"${proposal.title}\" (approval ${approval.id})`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to file proposal \"${proposal.title}\": ${(err as Error).message}`);\n }\n }\n\n return approvals;\n}\n\n/**\n * v4.5.6: simple title similarity for dedupe. Normalizes case, strips\n * filler words, compares token overlap (Jaccard). 0.72 threshold catches\n * \"Satiate Hunger\" vs \"Satiate hunger\" vs \"Satiate hunger backlog\"\n * but not \"Satiate Purpose\" vs \"Satiate hunger\" — which is what we want.\n */\nfunction titleSimilarity(a: string, b: string): number {\n const tokenize = (s: string) => new Set(\n s.toLowerCase()\n .replace(/[^\\w\\s]/g, ' ')\n .split(/\\s+/)\n .filter(w => w.length > 2 && !STOP_WORDS.has(w))\n );\n const ta = tokenize(a);\n const tb = tokenize(b);\n if (ta.size === 0 || tb.size === 0) return 0;\n let intersection = 0;\n for (const t of ta) if (tb.has(t)) intersection++;\n const union = ta.size + tb.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\nconst STOP_WORDS = new Set([\n 'the', 'and', 'for', 'with', 'new', 'novel', 'build', 'using', 'from',\n 'into', 'over', 'onto', 'that', 'this', 'some', 'any',\n]);\n\n// ── Context Helpers ──────────────────────────────────────────────\n\n/**\n * Build the default context for a goal proposer run from current TITAN state.\n * Extracted so tests can construct contexts deterministically.\n */\nexport function buildDefaultContext(): GoalProposerContext {\n const activeGoals = listGoals('active').map(g => g.title);\n const failedSubtasks: string[] = [];\n for (const g of listGoals()) {\n for (const st of g.subtasks || []) {\n if (st.status === 'failed') failedSubtasks.push(`${g.title} → ${st.title}`);\n }\n }\n const recentActivity: string[] = [];\n try {\n const feed = getActivity({ limit: 50 });\n for (const entry of feed) {\n recentActivity.push(`[${entry.type}] ${entry.message}`);\n }\n } catch { /* feed may be unavailable in early boot */ }\n\n return {\n activeGoals,\n failedSubtasks: failedSubtasks.slice(0, 20),\n recentActivity,\n };\n}\n"],"mappings":"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","names":["listGoals"]}
1
+ {"version":3,"sources":["../../src/agent/goalProposer.ts"],"sourcesContent":["/**\n * TITAN — Self-Directed Goal Proposer\n *\n * Runs during the nightly dreaming cycle (Phase 4: Dream) after memory\n * consolidation has happened. Each registered agent examines recent activity,\n * open issues, failed subtasks, and consolidation findings, then proposes\n * 0-3 new goals it thinks would be worth doing.\n *\n * Proposals go into the Command Post approval queue as `type: 'goal_proposal'`.\n * A human (or designated approver agent) accepts or rejects them. On accept,\n * the existing createGoal() pipeline fires and Initiative picks up the work.\n *\n * Opt-in via config.agent.autoProposeGoals (default false).\n * Rate-limited per-agent via config.agent.proposalRateLimitPerDay.\n */\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport { mkdirIfNotExists } from '../utils/helpers.js';\nimport { loadConfig } from '../config/config.js';\nimport { chat } from '../providers/router.js';\nimport { auxChat, resolveAuxiliaryModel } from '../providers/auxiliary.js';\nimport { applyOutputGuardrails } from './outputGuardrails.js';\nimport { getActivity, requestGoalProposalApproval, type CPApproval } from './commandPost.js';\nimport { listGoals } from './goals.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'GoalProposer';\nconst RATE_STATE_PATH = join(TITAN_HOME, 'goal-proposer-state.json');\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport interface ProposedGoal {\n title: string;\n description: string;\n rationale: string;\n priority?: number;\n tags?: string[];\n parentGoalId?: string;\n subtasks?: Array<{ title: string; description: string; dependsOn?: string[] }>;\n}\n\nexport interface GoalProposerContext {\n /** Recent activity feed entries — max last 50. */\n recentActivity?: string[];\n /** Titles of currently active goals so proposals don't duplicate. */\n activeGoals?: string[];\n /** Titles of recently failed subtasks worth retrying or reframing. */\n failedSubtasks?: string[];\n /** Free-form notes from the dreaming consolidation log. */\n consolidationNotes?: string;\n /**\n * v4.9.0-local.4: extra prompt blocks (episodic recall, experiment\n * history, identity) pre-loaded by the caller. Keeps buildPrompt\n * synchronous.\n */\n extraBlocks?: string[];\n}\n\ninterface RateLimitState {\n /** Map of agentId → ISO timestamps of proposals filed in the last 24h. */\n proposalsByAgent: Record<string, string[]>;\n}\n\n// ── Rate Limiting ────────────────────────────────────────────────\n\nfunction loadRateState(): RateLimitState {\n if (!existsSync(RATE_STATE_PATH)) return { proposalsByAgent: {} };\n try {\n const raw = readFileSync(RATE_STATE_PATH, 'utf-8');\n const parsed = JSON.parse(raw) as Partial<RateLimitState>;\n // v4.9.0-local.6: defensive normalize. A prior bug (+ the goal-\n // reset script) can write `{}` to this file, losing the\n // `proposalsByAgent` key. Without this, every\n // `state.proposalsByAgent[agentId]` access crashes with\n // \"Cannot read properties of undefined (reading '<agent>')\"\n // and proposals silently fail for hours.\n return {\n proposalsByAgent: parsed?.proposalsByAgent ?? {},\n };\n } catch {\n return { proposalsByAgent: {} };\n }\n}\n\nfunction saveRateState(state: RateLimitState): void {\n try {\n mkdirIfNotExists(TITAN_HOME);\n writeFileSync(RATE_STATE_PATH, JSON.stringify(state, null, 2), 'utf-8');\n } catch (err) {\n logger.warn(COMPONENT, `Failed to save rate state: ${(err as Error).message}`);\n }\n}\n\n/** Returns how many slots the agent has remaining in the current 24h window. */\nexport function remainingSlots(agentId: string, limitPerDay: number): number {\n const state = loadRateState();\n const now = Date.now();\n const dayMs = 24 * 3600 * 1000;\n const stamps = (state.proposalsByAgent[agentId] || []).filter(t => now - new Date(t).getTime() < dayMs);\n return Math.max(0, limitPerDay - stamps.length);\n}\n\nfunction recordProposal(agentId: string): void {\n const state = loadRateState();\n const now = Date.now();\n const dayMs = 24 * 3600 * 1000;\n const existing = (state.proposalsByAgent[agentId] || []).filter(t => now - new Date(t).getTime() < dayMs);\n existing.push(new Date().toISOString());\n state.proposalsByAgent[agentId] = existing;\n saveRateState(state);\n}\n\n// ── Prompt ───────────────────────────────────────────────────────\n\nfunction buildPrompt(agentId: string, slotsLeft: number, ctx: GoalProposerContext): string {\n const sections: string[] = [];\n sections.push(`You are agent \"${agentId}\". You have been given a quiet window to reflect on the system's current state and propose new goals that would meaningfully help.`);\n sections.push(`You may propose 0 to ${slotsLeft} goals. It is OK — often preferable — to propose zero if nothing is clearly worth doing.`);\n sections.push('');\n\n if (ctx.activeGoals && ctx.activeGoals.length) {\n sections.push('## Currently Active Goals (do not duplicate)');\n for (const title of ctx.activeGoals.slice(0, 20)) sections.push(`- ${title}`);\n sections.push('');\n }\n if (ctx.recentActivity && ctx.recentActivity.length) {\n sections.push('## Recent Activity (last ~50 events)');\n for (const line of ctx.recentActivity.slice(-50)) sections.push(`- ${line}`);\n sections.push('');\n }\n if (ctx.failedSubtasks && ctx.failedSubtasks.length) {\n sections.push('## Recently Failed Subtasks');\n for (const title of ctx.failedSubtasks.slice(0, 20)) sections.push(`- ${title}`);\n sections.push('');\n }\n if (ctx.consolidationNotes) {\n sections.push('## Memory Consolidation Notes');\n sections.push(ctx.consolidationNotes);\n sections.push('');\n }\n\n // v4.9.0-local.4: extra memory blocks (episodic, experiments,\n // identity) pre-loaded by the async caller and passed through ctx.\n // Keeps buildPrompt synchronous while still giving the proposer\n // full context of what TITAN has already done + who it is.\n if (ctx.extraBlocks && ctx.extraBlocks.length > 0) {\n for (const block of ctx.extraBlocks) {\n if (block && block.trim()) {\n sections.push(block);\n sections.push('');\n }\n }\n }\n\n sections.push('## Output Format');\n sections.push('Return ONLY a JSON array (no prose, no markdown fences). Each element:');\n sections.push('```');\n sections.push('{');\n sections.push(' \"title\": \"short imperative, under 80 chars\",');\n sections.push(' \"description\": \"what success looks like, 1-3 sentences\",');\n sections.push(' \"rationale\": \"why this goal is worth doing NOW\",');\n sections.push(' \"priority\": 1-5 (1 = highest),');\n sections.push(' \"tags\": [\"optional\", \"labels\"],');\n sections.push(' \"subtasks\": [{\"title\": \"...\", \"description\": \"...\"}]');\n sections.push('}');\n sections.push('```');\n sections.push('If nothing is worth proposing, return `[]`. Never return more than the slot limit.');\n\n return sections.join('\\n');\n}\n\n/** JSON schema passed to Ollama's native structured-outputs `format` field.\n * Constrains the model to emit an array of proposal objects matching the\n * fields normalizeProposal() accepts. Belt-and-suspenders — the downstream\n * defensive parser is still the authoritative validator. */\nconst PROPOSAL_ARRAY_SCHEMA: Record<string, unknown> = {\n type: 'array',\n items: {\n type: 'object',\n required: ['title', 'description', 'rationale'],\n properties: {\n title: { type: 'string' },\n description: { type: 'string' },\n rationale: { type: 'string' },\n priority: { type: 'number' },\n tags: { type: 'array', items: { type: 'string' } },\n subtasks: {\n type: 'array',\n items: {\n type: 'object',\n required: ['title', 'description'],\n properties: {\n title: { type: 'string' },\n description: { type: 'string' },\n dependsOn: { type: 'array', items: { type: 'string' } },\n },\n },\n },\n },\n },\n};\n\n// ── JSON Extraction ──────────────────────────────────────────────\n\n/** Defensively parse a JSON array from LLM output. Returns [] on failure. */\nfunction extractProposalArray(raw: string): unknown[] {\n const trimmed = raw.trim();\n // Try direct parse first.\n try {\n const parsed = JSON.parse(trimmed);\n if (Array.isArray(parsed)) return parsed;\n } catch { /* fall through */ }\n // Strip code fences.\n const fenceStripped = trimmed.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '');\n try {\n const parsed = JSON.parse(fenceStripped);\n if (Array.isArray(parsed)) return parsed;\n } catch { /* fall through */ }\n // Find the first `[...]` substring.\n const match = trimmed.match(/\\[[\\s\\S]*\\]/);\n if (match) {\n try {\n const parsed = JSON.parse(match[0]);\n if (Array.isArray(parsed)) return parsed;\n } catch { /* give up */ }\n }\n return [];\n}\n\n/**\n * Exported for tests — normalizes a single raw LLM proposal into the\n * canonical ProposedGoal shape, OR returns null if it's filtered out\n * (malformed, or classified as self-mod while\n * `autonomy.selfMod.autoCreateGoals` is off).\n */\nexport function normalizeProposal(raw: unknown): ProposedGoal | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n let title = typeof r.title === 'string' ? r.title.trim() : '';\n let description = typeof r.description === 'string' ? r.description.trim() : '';\n const rationale = typeof r.rationale === 'string' ? r.rationale.trim() : '';\n if (!title || !description || !rationale) return null;\n if (title.length > 200 || description.length > 2000 || rationale.length > 2000) return null;\n\n const priority = typeof r.priority === 'number' && r.priority >= 1 && r.priority <= 5\n ? Math.floor(r.priority)\n : undefined;\n let tags: string[] | undefined = Array.isArray(r.tags)\n ? r.tags.filter((t): t is string => typeof t === 'string' && t.length < 40).slice(0, 6)\n : undefined;\n const parentGoalId = typeof r.parentGoalId === 'string' ? r.parentGoalId : undefined;\n\n type Subtask = { title: string; description: string; dependsOn?: string[] };\n let subtasks: Subtask[] | undefined;\n if (Array.isArray(r.subtasks)) {\n const collected: Subtask[] = [];\n for (const s of r.subtasks) {\n if (!s || typeof s !== 'object') continue;\n const rec = s as Record<string, unknown>;\n const t = typeof rec.title === 'string' ? rec.title.trim() : '';\n const d = typeof rec.description === 'string' ? rec.description.trim() : '';\n if (!t || !d) continue;\n const deps = Array.isArray(rec.dependsOn)\n ? rec.dependsOn.filter((x): x is string => typeof x === 'string')\n : undefined;\n // Subtask-level rewrite happens after we know the parent goal's\n // self-mod status (below), so stash refs.\n void t; void d;\n collected.push({ title: t, description: d, dependsOn: deps });\n if (collected.length >= 12) break;\n }\n if (collected.length > 0) subtasks = collected;\n }\n\n // v4.9.0-local.8: self-mod disambiguation. When the proposer emits a\n // goal that sounds like it wants to modify \"the framework\" / \"core\" /\n // etc, the specialist picking it up historically interpreted this as\n // \"build something under ~/titan-saas\" because that's where Next.js\n // project scaffolding lives. We close the ambiguity at creation time:\n //\n // 1. Detect self-mod trigger words in title + description + tags\n // 2. If matched, ensure tags include 'self-mod' so the toolRunner\n // scope-lock + staging gate activates for work on this goal\n // 3. Append an explicit scope-lock note to the description pointing\n // at the actual target path\n // 4. Rewrite common ambiguous phrases in subtasks to spell out the\n // target path\n const selfModTriggers = [\n /\\bself[\\s-]?heal/i,\n /\\bself[\\s-]?repair/i,\n /\\bself[\\s-]?mod/i,\n /\\bcore[\\s-]framework/i,\n /\\bTITAN['’]?s?\\s+(own|core|framework|architecture|source|runtime)/i,\n /\\bframework\\s+(component|module|core|runtime)/i,\n ];\n const selfModTagValues = new Set([\n 'self-healing', 'self-repair', 'self-mod', 'self-modification',\n 'core-framework', 'framework', 'architecture', 'core', 'autonomy',\n ]);\n const tagsLower = new Set((tags || []).map(t => t.toLowerCase()));\n const haystack = `${title}\\n${description}\\n${(tags || []).join(' ')}`;\n const matchedByText = selfModTriggers.some(re => re.test(haystack));\n const matchedByTag = [...tagsLower].some(t => selfModTagValues.has(t));\n const isSelfMod = matchedByText || matchedByTag;\n\n if (isSelfMod) {\n const cfg = loadConfig();\n // v6.0.3 — gate autonomous self-mod goal creation. When the flag is\n // off (default), we drop the proposal here BEFORE it reaches the\n // approval queue. The self-repair daemon's findings still surface\n // via the custom self_repair approval path; what we're suppressing\n // is the autonomous \"let's spin up a goal to rewrite the framework\"\n // path from the dreaming/Soma proposer. See schema.ts\n // autonomy.selfMod.autoCreateGoals for full rationale.\n const selfModBlock = cfg.autonomy?.selfMod as { target?: string; autoCreateGoals?: boolean } | undefined;\n const autoCreateGoals = selfModBlock?.autoCreateGoals === true;\n if (!autoCreateGoals) {\n logger.info(\n COMPONENT,\n `Dropping self-mod proposal \"${title.slice(0, 60)}\" — autonomy.selfMod.autoCreateGoals is disabled.`\n );\n return null;\n }\n const target = selfModBlock?.target || '/opt/TITAN';\n // Ensure the canonical 'self-mod' tag is present so toolRunner sees it\n tags = tags ? [...tags] : [];\n if (!tagsLower.has('self-mod')) tags.push('self-mod');\n tags = tags.slice(0, 6);\n\n // Append an unmistakable scope-lock note to the description — the\n // specialist reading this goal sees the target path explicitly\n // instead of having to infer \"the framework\" from context.\n const scopeNote = `\\n\\n[SCOPE-LOCK] This is a self-modification goal. All file writes MUST target ${target} (TITAN's own source tree). Writes to any other path will be refused by the toolRunner scope-lock. When staging is enabled, writes are diverted to ${target}/../self-mod-staging/<goalId>/ and surface as a self_mod_pr approval for human review before applying.`;\n if (!/\\[SCOPE-LOCK\\]/.test(description)) {\n description = (description + scopeNote).slice(0, 2000);\n }\n\n // Rewrite common ambiguous phrases in title/subtasks so the\n // specialist-level prompt mentions the target explicitly.\n const rewrite = (s: string): string => s\n .replace(/\\b(the\\s+)?core\\s+framework\\b/gi, `${target} (TITAN core framework)`)\n .replace(/\\b(the\\s+)?(TITAN\\s+)?framework\\b(?!\\s+component)/gi, `${target} (TITAN framework)`);\n title = rewrite(title).slice(0, 200);\n if (subtasks) {\n subtasks = subtasks.map(s => ({\n title: rewrite(s.title).slice(0, 200),\n description: rewrite(s.description).slice(0, 2000),\n dependsOn: s.dependsOn,\n }));\n }\n }\n\n return { title, description, rationale, priority, tags, parentGoalId, subtasks };\n}\n\n// ── Main Entry Point ─────────────────────────────────────────────\n\n/**\n * Generate goal proposals for a single agent and file them as pending approvals.\n * Returns the list of CPApproval records created (may be empty).\n *\n * Called by the dreaming watcher's Phase 4 (Dream). Safe to call ad-hoc from\n * debug endpoints or tests.\n */\nexport async function generateGoalProposals(\n agentId: string,\n ctx: GoalProposerContext,\n type: 'goal_proposal' | 'soma_proposal' = 'goal_proposal'\n): Promise<CPApproval[]> {\n const config = loadConfig();\n const enabled = config.agent.autoProposeGoals;\n if (!enabled) {\n logger.debug(COMPONENT, `autoProposeGoals disabled — skipping for agent ${agentId}`);\n return [];\n }\n\n const limit = config.agent.proposalRateLimitPerDay;\n const slotsLeft = remainingSlots(agentId, limit);\n if (slotsLeft <= 0) {\n logger.info(COMPONENT, `Agent ${agentId} has hit daily proposal limit (${limit}) — skipping`);\n return [];\n }\n\n const modelAlias = config.agent.proposalModel || 'fast';\n const model = config.agent.modelAliases[modelAlias] || modelAlias;\n\n // v4.9.0-local.4: pre-load extra memory blocks (episodic, experiments,\n // identity) before building the proposer prompt. Closes the repeat-\n // task feedback loop — the proposer now sees what TITAN has recently\n // done and won't re-propose the same ant colony sim three times.\n // Each block is best-effort; silent fallthrough if a module isn't\n // available at proposer time.\n const extraBlocks: string[] = [];\n try {\n const { renderRecallBlock } = await import('../memory/episodic.js');\n const block = renderRecallBlock({ limit: 12, windowHours: 72 });\n if (block) extraBlocks.push(block);\n } catch { /* ok */ }\n try {\n const { renderRecentExperimentsBlock } = await import('../memory/experiments.js');\n const block = renderRecentExperimentsBlock(8);\n if (block) extraBlocks.push(block);\n } catch { /* ok */ }\n try {\n const { getIdentity } = await import('../memory/identity.js');\n const id = getIdentity();\n if (id) {\n extraBlocks.push([\n '## Your identity (persistent)',\n `Mission: ${id.core.mission}`,\n `Non-negotiables: ${id.core.nonNegotiables.slice(0, 3).join('; ')}`,\n 'Propose ONLY goals that align with the mission and never violate a non-negotiable.',\n ].join('\\n'));\n }\n } catch { /* ok */ }\n\n const ctxWithBlocks: GoalProposerContext = { ...ctx, extraBlocks };\n const prompt = buildPrompt(agentId, slotsLeft, ctxWithBlocks);\n\n // v4.13 ancestor-extraction: route goal-proposal JSON extraction through\n // the auxiliary model client. The main agent model (gemma4:31b on the\n // Titan PC default) produces empty arrays for structured JSON tasks; a\n // dedicated fast+cheap model (minimax-m2.7:cloud) reliably produces valid\n // proposals. Falls back to the main `model` when no auxiliary is\n // configured.\n const auxModel = resolveAuxiliaryModel('json_extraction');\n const effectiveModel = auxModel || model;\n const isOllamaEffective = effectiveModel.toLowerCase().startsWith('ollama/');\n\n let rawContent: string;\n try {\n const response = await auxChat(\n 'json_extraction',\n {\n messages: [\n { role: 'system', content: 'You are a careful autonomous agent proposing new work. Output ONLY valid JSON. No explanation, no prose.' },\n { role: 'user', content: prompt },\n ],\n temperature: 0.4,\n maxTokens: 1500,\n ...(isOllamaEffective ? { format: PROPOSAL_ARRAY_SCHEMA } : {}),\n },\n model, // fallback to main agent model if no aux is configured\n );\n if (!response) {\n logger.warn(COMPONENT, `Auxiliary call returned null for agent ${agentId} — treating as no proposals`);\n return [];\n }\n rawContent = response.content || '';\n if (auxModel && auxModel !== model) {\n logger.info(COMPONENT, `Agent ${agentId} goal-proposal routed via auxiliary model ${auxModel} (main: ${model})`);\n }\n } catch (err) {\n logger.warn(COMPONENT, `LLM call failed for agent ${agentId}: ${(err as Error).message}`);\n return [];\n }\n\n // Strip chain-of-thought leakage before parsing JSON.\n const guarded = applyOutputGuardrails(rawContent, { type: 'sub_agent' });\n const parsed = extractProposalArray(guarded.content);\n if (parsed.length === 0) {\n logger.info(COMPONENT, `Agent ${agentId} proposed no goals (parsed empty array)`);\n return [];\n }\n\n const proposals: ProposedGoal[] = [];\n for (const item of parsed) {\n const normalized = normalizeProposal(item);\n if (normalized) proposals.push(normalized);\n if (proposals.length >= slotsLeft) break;\n }\n\n // v5.0.0: dedupe against ALL recent goals (not just active) and enforce\n // goal-overload backoff. Prevents the runaway loops that produced 1000+\n // duplicate \"Publish content\" goals.\n let allGoalTitles: string[] = [];\n let activeGoalCount = 0;\n try {\n const { listGoals } = await import('./goals.js');\n const all = listGoals();\n allGoalTitles = all.map(g => g.title);\n activeGoalCount = all.filter(g => g.status === 'active').length;\n } catch { /* best-effort */ }\n\n // Overload backoff: if the system is already swamped, only allow\n // cleanup / meta proposals (titles containing \"resolve\", \"cancel\",\n // \"close\", \"audit\", \"clean\", \"dedupe\"). Everything else is deferred\n // until the backlog drops.\n const isOverload = activeGoalCount >= 25;\n const isCleanupProposal = (t: string) => /\\b(resolve|cancel|close|audit|clean|dedupe|consolidate|prune)\\b/i.test(t);\n\n const approvals: CPApproval[] = [];\n for (const proposal of proposals) {\n // Dedupe against ANY existing goal (active, paused, completed, failed)\n const dup = allGoalTitles.find(t => titleSimilarity(t, proposal.title) >= 0.72);\n if (dup) {\n logger.info(COMPONENT, `Agent ${agentId} skipped duplicate proposal \"${proposal.title}\" (matches existing goal \"${dup}\")`);\n continue;\n }\n // Overload gate\n if (isOverload && !isCleanupProposal(proposal.title)) {\n logger.info(COMPONENT, `Agent ${agentId} skipped proposal \"${proposal.title}\" — goal overload (${activeGoalCount} active). Only cleanup proposals allowed.`);\n continue;\n }\n try {\n const approval = requestGoalProposalApproval(agentId, proposal, type);\n approvals.push(approval);\n recordProposal(agentId);\n allGoalTitles.push(proposal.title); // prevent intra-batch dupes too\n logger.info(COMPONENT, `Agent ${agentId} filed proposal \"${proposal.title}\" (approval ${approval.id})`);\n } catch (err) {\n logger.warn(COMPONENT, `Failed to file proposal \"${proposal.title}\": ${(err as Error).message}`);\n }\n }\n\n return approvals;\n}\n\n/**\n * v4.5.6: simple title similarity for dedupe. Normalizes case, strips\n * filler words, compares token overlap (Jaccard). 0.72 threshold catches\n * \"Satiate Hunger\" vs \"Satiate hunger\" vs \"Satiate hunger backlog\"\n * but not \"Satiate Purpose\" vs \"Satiate hunger\" — which is what we want.\n */\nfunction titleSimilarity(a: string, b: string): number {\n const tokenize = (s: string) => new Set(\n s.toLowerCase()\n .replace(/[^\\w\\s]/g, ' ')\n .split(/\\s+/)\n .filter(w => w.length > 2 && !STOP_WORDS.has(w))\n );\n const ta = tokenize(a);\n const tb = tokenize(b);\n if (ta.size === 0 || tb.size === 0) return 0;\n let intersection = 0;\n for (const t of ta) if (tb.has(t)) intersection++;\n const union = ta.size + tb.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\nconst STOP_WORDS = new Set([\n 'the', 'and', 'for', 'with', 'new', 'novel', 'build', 'using', 'from',\n 'into', 'over', 'onto', 'that', 'this', 'some', 'any',\n]);\n\n// ── Context Helpers ──────────────────────────────────────────────\n\n/**\n * Build the default context for a goal proposer run from current TITAN state.\n * Extracted so tests can construct contexts deterministically.\n */\nexport function buildDefaultContext(): GoalProposerContext {\n const activeGoals = listGoals('active').map(g => g.title);\n const failedSubtasks: string[] = [];\n for (const g of listGoals()) {\n for (const st of g.subtasks || []) {\n if (st.status === 'failed') failedSubtasks.push(`${g.title} → ${st.title}`);\n }\n }\n const recentActivity: string[] = [];\n try {\n const feed = getActivity({ limit: 50 });\n for (const entry of feed) {\n recentActivity.push(`[${entry.type}] ${entry.message}`);\n }\n } catch { /* feed may be unavailable in early boot */ }\n\n return {\n activeGoals,\n failedSubtasks: failedSubtasks.slice(0, 20),\n recentActivity,\n };\n}\n"],"mappings":";AAeA,SAAS,YAAY,cAAc,qBAAqB;AACxD,SAAS,YAAY;AACrB,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AACjC,SAAS,kBAAkB;AAE3B,SAAS,SAAS,6BAA6B;AAC/C,SAAS,6BAA6B;AACtC,SAAS,aAAa,mCAAoD;AAC1E,SAAS,iBAAiB;AAC1B,OAAO,YAAY;AAEnB,MAAM,YAAY;AAClB,MAAM,kBAAkB,KAAK,YAAY,0BAA0B;AAsCnE,SAAS,gBAAgC;AACrC,MAAI,CAAC,WAAW,eAAe,EAAG,QAAO,EAAE,kBAAkB,CAAC,EAAE;AAChE,MAAI;AACA,UAAM,MAAM,aAAa,iBAAiB,OAAO;AACjD,UAAM,SAAS,KAAK,MAAM,GAAG;AAO7B,WAAO;AAAA,MACH,kBAAkB,QAAQ,oBAAoB,CAAC;AAAA,IACnD;AAAA,EACJ,QAAQ;AACJ,WAAO,EAAE,kBAAkB,CAAC,EAAE;AAAA,EAClC;AACJ;AAEA,SAAS,cAAc,OAA6B;AAChD,MAAI;AACA,qBAAiB,UAAU;AAC3B,kBAAc,iBAAiB,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,OAAO;AAAA,EAC1E,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,8BAA+B,IAAc,OAAO,EAAE;AAAA,EACjF;AACJ;AAGO,SAAS,eAAe,SAAiB,aAA6B;AACzE,QAAM,QAAQ,cAAc;AAC5B,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,QAAQ,KAAK,OAAO;AAC1B,QAAM,UAAU,MAAM,iBAAiB,OAAO,KAAK,CAAC,GAAG,OAAO,OAAK,MAAM,IAAI,KAAK,CAAC,EAAE,QAAQ,IAAI,KAAK;AACtG,SAAO,KAAK,IAAI,GAAG,cAAc,OAAO,MAAM;AAClD;AAEA,SAAS,eAAe,SAAuB;AAC3C,QAAM,QAAQ,cAAc;AAC5B,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,QAAQ,KAAK,OAAO;AAC1B,QAAM,YAAY,MAAM,iBAAiB,OAAO,KAAK,CAAC,GAAG,OAAO,OAAK,MAAM,IAAI,KAAK,CAAC,EAAE,QAAQ,IAAI,KAAK;AACxG,WAAS,MAAK,oBAAI,KAAK,GAAE,YAAY,CAAC;AACtC,QAAM,iBAAiB,OAAO,IAAI;AAClC,gBAAc,KAAK;AACvB;AAIA,SAAS,YAAY,SAAiB,WAAmB,KAAkC;AACvF,QAAM,WAAqB,CAAC;AAC5B,WAAS,KAAK,kBAAkB,OAAO,oIAAoI;AAC3K,WAAS,KAAK,wBAAwB,SAAS,oGAA0F;AACzI,WAAS,KAAK,EAAE;AAEhB,MAAI,IAAI,eAAe,IAAI,YAAY,QAAQ;AAC3C,aAAS,KAAK,8CAA8C;AAC5D,eAAW,SAAS,IAAI,YAAY,MAAM,GAAG,EAAE,EAAG,UAAS,KAAK,KAAK,KAAK,EAAE;AAC5E,aAAS,KAAK,EAAE;AAAA,EACpB;AACA,MAAI,IAAI,kBAAkB,IAAI,eAAe,QAAQ;AACjD,aAAS,KAAK,sCAAsC;AACpD,eAAW,QAAQ,IAAI,eAAe,MAAM,GAAG,EAAG,UAAS,KAAK,KAAK,IAAI,EAAE;AAC3E,aAAS,KAAK,EAAE;AAAA,EACpB;AACA,MAAI,IAAI,kBAAkB,IAAI,eAAe,QAAQ;AACjD,aAAS,KAAK,6BAA6B;AAC3C,eAAW,SAAS,IAAI,eAAe,MAAM,GAAG,EAAE,EAAG,UAAS,KAAK,KAAK,KAAK,EAAE;AAC/E,aAAS,KAAK,EAAE;AAAA,EACpB;AACA,MAAI,IAAI,oBAAoB;AACxB,aAAS,KAAK,+BAA+B;AAC7C,aAAS,KAAK,IAAI,kBAAkB;AACpC,aAAS,KAAK,EAAE;AAAA,EACpB;AAMA,MAAI,IAAI,eAAe,IAAI,YAAY,SAAS,GAAG;AAC/C,eAAW,SAAS,IAAI,aAAa;AACjC,UAAI,SAAS,MAAM,KAAK,GAAG;AACvB,iBAAS,KAAK,KAAK;AACnB,iBAAS,KAAK,EAAE;AAAA,MACpB;AAAA,IACJ;AAAA,EACJ;AAEA,WAAS,KAAK,kBAAkB;AAChC,WAAS,KAAK,wEAAwE;AACtF,WAAS,KAAK,KAAK;AACnB,WAAS,KAAK,GAAG;AACjB,WAAS,KAAK,gDAAgD;AAC9D,WAAS,KAAK,4DAA4D;AAC1E,WAAS,KAAK,oDAAoD;AAClE,WAAS,KAAK,kCAAkC;AAChD,WAAS,KAAK,mCAAmC;AACjD,WAAS,KAAK,wDAAwD;AACtE,WAAS,KAAK,GAAG;AACjB,WAAS,KAAK,KAAK;AACnB,WAAS,KAAK,oFAAoF;AAElG,SAAO,SAAS,KAAK,IAAI;AAC7B;AAMA,MAAM,wBAAiD;AAAA,EACnD,MAAM;AAAA,EACN,OAAO;AAAA,IACH,MAAM;AAAA,IACN,UAAU,CAAC,SAAS,eAAe,WAAW;AAAA,IAC9C,YAAY;AAAA,MACR,OAAO,EAAE,MAAM,SAAS;AAAA,MACxB,aAAa,EAAE,MAAM,SAAS;AAAA,MAC9B,WAAW,EAAE,MAAM,SAAS;AAAA,MAC5B,UAAU,EAAE,MAAM,SAAS;AAAA,MAC3B,MAAM,EAAE,MAAM,SAAS,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,MACjD,UAAU;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,UACH,MAAM;AAAA,UACN,UAAU,CAAC,SAAS,aAAa;AAAA,UACjC,YAAY;AAAA,YACR,OAAO,EAAE,MAAM,SAAS;AAAA,YACxB,aAAa,EAAE,MAAM,SAAS;AAAA,YAC9B,WAAW,EAAE,MAAM,SAAS,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,UAC1D;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AACJ;AAKA,SAAS,qBAAqB,KAAwB;AAClD,QAAM,UAAU,IAAI,KAAK;AAEzB,MAAI;AACA,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,QAAI,MAAM,QAAQ,MAAM,EAAG,QAAO;AAAA,EACtC,QAAQ;AAAA,EAAqB;AAE7B,QAAM,gBAAgB,QAAQ,QAAQ,qBAAqB,EAAE,EAAE,QAAQ,WAAW,EAAE;AACpF,MAAI;AACA,UAAM,SAAS,KAAK,MAAM,aAAa;AACvC,QAAI,MAAM,QAAQ,MAAM,EAAG,QAAO;AAAA,EACtC,QAAQ;AAAA,EAAqB;AAE7B,QAAM,QAAQ,QAAQ,MAAM,aAAa;AACzC,MAAI,OAAO;AACP,QAAI;AACA,YAAM,SAAS,KAAK,MAAM,MAAM,CAAC,CAAC;AAClC,UAAI,MAAM,QAAQ,MAAM,EAAG,QAAO;AAAA,IACtC,QAAQ;AAAA,IAAgB;AAAA,EAC5B;AACA,SAAO,CAAC;AACZ;AAQO,SAAS,kBAAkB,KAAmC;AACjE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,IAAI;AACV,MAAI,QAAQ,OAAO,EAAE,UAAU,WAAW,EAAE,MAAM,KAAK,IAAI;AAC3D,MAAI,cAAc,OAAO,EAAE,gBAAgB,WAAW,EAAE,YAAY,KAAK,IAAI;AAC7E,QAAM,YAAY,OAAO,EAAE,cAAc,WAAW,EAAE,UAAU,KAAK,IAAI;AACzE,MAAI,CAAC,SAAS,CAAC,eAAe,CAAC,UAAW,QAAO;AACjD,MAAI,MAAM,SAAS,OAAO,YAAY,SAAS,OAAQ,UAAU,SAAS,IAAM,QAAO;AAEvF,QAAM,WAAW,OAAO,EAAE,aAAa,YAAY,EAAE,YAAY,KAAK,EAAE,YAAY,IAC9E,KAAK,MAAM,EAAE,QAAQ,IACrB;AACN,MAAI,OAA6B,MAAM,QAAQ,EAAE,IAAI,IAC/C,EAAE,KAAK,OAAO,CAAC,MAAmB,OAAO,MAAM,YAAY,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,IACpF;AACN,QAAM,eAAe,OAAO,EAAE,iBAAiB,WAAW,EAAE,eAAe;AAG3E,MAAI;AACJ,MAAI,MAAM,QAAQ,EAAE,QAAQ,GAAG;AAC3B,UAAM,YAAuB,CAAC;AAC9B,eAAW,KAAK,EAAE,UAAU;AACxB,UAAI,CAAC,KAAK,OAAO,MAAM,SAAU;AACjC,YAAM,MAAM;AACZ,YAAM,IAAI,OAAO,IAAI,UAAU,WAAW,IAAI,MAAM,KAAK,IAAI;AAC7D,YAAM,IAAI,OAAO,IAAI,gBAAgB,WAAW,IAAI,YAAY,KAAK,IAAI;AACzE,UAAI,CAAC,KAAK,CAAC,EAAG;AACd,YAAM,OAAO,MAAM,QAAQ,IAAI,SAAS,IAClC,IAAI,UAAU,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,IAC9D;AAGN,WAAK;AAAG,WAAK;AACb,gBAAU,KAAK,EAAE,OAAO,GAAG,aAAa,GAAG,WAAW,KAAK,CAAC;AAC5D,UAAI,UAAU,UAAU,GAAI;AAAA,IAChC;AACA,QAAI,UAAU,SAAS,EAAG,YAAW;AAAA,EACzC;AAeA,QAAM,kBAAkB;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACJ;AACA,QAAM,mBAAmB,oBAAI,IAAI;AAAA,IAC7B;AAAA,IAAgB;AAAA,IAAe;AAAA,IAAY;AAAA,IAC3C;AAAA,IAAkB;AAAA,IAAa;AAAA,IAAgB;AAAA,IAAQ;AAAA,EAC3D,CAAC;AACD,QAAM,YAAY,IAAI,KAAK,QAAQ,CAAC,GAAG,IAAI,OAAK,EAAE,YAAY,CAAC,CAAC;AAChE,QAAM,WAAW,GAAG,KAAK;AAAA,EAAK,WAAW;AAAA,GAAM,QAAQ,CAAC,GAAG,KAAK,GAAG,CAAC;AACpE,QAAM,gBAAgB,gBAAgB,KAAK,QAAM,GAAG,KAAK,QAAQ,CAAC;AAClE,QAAM,eAAe,CAAC,GAAG,SAAS,EAAE,KAAK,OAAK,iBAAiB,IAAI,CAAC,CAAC;AACrE,QAAM,YAAY,iBAAiB;AAEnC,MAAI,WAAW;AACX,UAAM,MAAM,WAAW;AAQvB,UAAM,eAAe,IAAI,UAAU;AACnC,UAAM,kBAAkB,cAAc,oBAAoB;AAC1D,QAAI,CAAC,iBAAiB;AAClB,aAAO;AAAA,QACH;AAAA,QACA,+BAA+B,MAAM,MAAM,GAAG,EAAE,CAAC;AAAA,MACrD;AACA,aAAO;AAAA,IACX;AACA,UAAM,SAAS,cAAc,UAAU;AAEvC,WAAO,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC;AAC3B,QAAI,CAAC,UAAU,IAAI,UAAU,EAAG,MAAK,KAAK,UAAU;AACpD,WAAO,KAAK,MAAM,GAAG,CAAC;AAKtB,UAAM,YAAY;AAAA;AAAA,6EAAkF,MAAM,sJAAsJ,MAAM;AACtQ,QAAI,CAAC,iBAAiB,KAAK,WAAW,GAAG;AACrC,qBAAe,cAAc,WAAW,MAAM,GAAG,GAAI;AAAA,IACzD;AAIA,UAAM,UAAU,CAAC,MAAsB,EAClC,QAAQ,mCAAmC,GAAG,MAAM,yBAAyB,EAC7E,QAAQ,uDAAuD,GAAG,MAAM,oBAAoB;AACjG,YAAQ,QAAQ,KAAK,EAAE,MAAM,GAAG,GAAG;AACnC,QAAI,UAAU;AACV,iBAAW,SAAS,IAAI,QAAM;AAAA,QAC1B,OAAO,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,GAAG;AAAA,QACpC,aAAa,QAAQ,EAAE,WAAW,EAAE,MAAM,GAAG,GAAI;AAAA,QACjD,WAAW,EAAE;AAAA,MACjB,EAAE;AAAA,IACN;AAAA,EACJ;AAEA,SAAO,EAAE,OAAO,aAAa,WAAW,UAAU,MAAM,cAAc,SAAS;AACnF;AAWA,eAAsB,sBAClB,SACA,KACA,OAA0C,iBACrB;AACrB,QAAM,SAAS,WAAW;AAC1B,QAAM,UAAU,OAAO,MAAM;AAC7B,MAAI,CAAC,SAAS;AACV,WAAO,MAAM,WAAW,uDAAkD,OAAO,EAAE;AACnF,WAAO,CAAC;AAAA,EACZ;AAEA,QAAM,QAAQ,OAAO,MAAM;AAC3B,QAAM,YAAY,eAAe,SAAS,KAAK;AAC/C,MAAI,aAAa,GAAG;AAChB,WAAO,KAAK,WAAW,SAAS,OAAO,kCAAkC,KAAK,mBAAc;AAC5F,WAAO,CAAC;AAAA,EACZ;AAEA,QAAM,aAAa,OAAO,MAAM,iBAAiB;AACjD,QAAM,QAAQ,OAAO,MAAM,aAAa,UAAU,KAAK;AAQvD,QAAM,cAAwB,CAAC;AAC/B,MAAI;AACA,UAAM,EAAE,kBAAkB,IAAI,MAAM,OAAO,uBAAuB;AAClE,UAAM,QAAQ,kBAAkB,EAAE,OAAO,IAAI,aAAa,GAAG,CAAC;AAC9D,QAAI,MAAO,aAAY,KAAK,KAAK;AAAA,EACrC,QAAQ;AAAA,EAAW;AACnB,MAAI;AACA,UAAM,EAAE,6BAA6B,IAAI,MAAM,OAAO,0BAA0B;AAChF,UAAM,QAAQ,6BAA6B,CAAC;AAC5C,QAAI,MAAO,aAAY,KAAK,KAAK;AAAA,EACrC,QAAQ;AAAA,EAAW;AACnB,MAAI;AACA,UAAM,EAAE,YAAY,IAAI,MAAM,OAAO,uBAAuB;AAC5D,UAAM,KAAK,YAAY;AACvB,QAAI,IAAI;AACJ,kBAAY,KAAK;AAAA,QACb;AAAA,QACA,YAAY,GAAG,KAAK,OAAO;AAAA,QAC3B,oBAAoB,GAAG,KAAK,eAAe,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,QACjE;AAAA,MACJ,EAAE,KAAK,IAAI,CAAC;AAAA,IAChB;AAAA,EACJ,QAAQ;AAAA,EAAW;AAEnB,QAAM,gBAAqC,EAAE,GAAG,KAAK,YAAY;AACjE,QAAM,SAAS,YAAY,SAAS,WAAW,aAAa;AAQ5D,QAAM,WAAW,sBAAsB,iBAAiB;AACxD,QAAM,iBAAiB,YAAY;AACnC,QAAM,oBAAoB,eAAe,YAAY,EAAE,WAAW,SAAS;AAE3E,MAAI;AACJ,MAAI;AACA,UAAM,WAAW,MAAM;AAAA,MACnB;AAAA,MACA;AAAA,QACI,UAAU;AAAA,UACN,EAAE,MAAM,UAAU,SAAS,2GAA2G;AAAA,UACtI,EAAE,MAAM,QAAQ,SAAS,OAAO;AAAA,QACpC;AAAA,QACA,aAAa;AAAA,QACb,WAAW;AAAA,QACX,GAAI,oBAAoB,EAAE,QAAQ,sBAAsB,IAAI,CAAC;AAAA,MACjE;AAAA,MACA;AAAA;AAAA,IACJ;AACA,QAAI,CAAC,UAAU;AACX,aAAO,KAAK,WAAW,0CAA0C,OAAO,kCAA6B;AACrG,aAAO,CAAC;AAAA,IACZ;AACA,iBAAa,SAAS,WAAW;AACjC,QAAI,YAAY,aAAa,OAAO;AAChC,aAAO,KAAK,WAAW,SAAS,OAAO,6CAA6C,QAAQ,WAAW,KAAK,GAAG;AAAA,IACnH;AAAA,EACJ,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,6BAA6B,OAAO,KAAM,IAAc,OAAO,EAAE;AACxF,WAAO,CAAC;AAAA,EACZ;AAGA,QAAM,UAAU,sBAAsB,YAAY,EAAE,MAAM,YAAY,CAAC;AACvE,QAAM,SAAS,qBAAqB,QAAQ,OAAO;AACnD,MAAI,OAAO,WAAW,GAAG;AACrB,WAAO,KAAK,WAAW,SAAS,OAAO,yCAAyC;AAChF,WAAO,CAAC;AAAA,EACZ;AAEA,QAAM,YAA4B,CAAC;AACnC,aAAW,QAAQ,QAAQ;AACvB,UAAM,aAAa,kBAAkB,IAAI;AACzC,QAAI,WAAY,WAAU,KAAK,UAAU;AACzC,QAAI,UAAU,UAAU,UAAW;AAAA,EACvC;AAKA,MAAI,gBAA0B,CAAC;AAC/B,MAAI,kBAAkB;AACtB,MAAI;AACA,UAAM,EAAE,WAAAA,WAAU,IAAI,MAAM,OAAO,YAAY;AAC/C,UAAM,MAAMA,WAAU;AACtB,oBAAgB,IAAI,IAAI,OAAK,EAAE,KAAK;AACpC,sBAAkB,IAAI,OAAO,OAAK,EAAE,WAAW,QAAQ,EAAE;AAAA,EAC7D,QAAQ;AAAA,EAAoB;AAM5B,QAAM,aAAa,mBAAmB;AACtC,QAAM,oBAAoB,CAAC,MAAc,mEAAmE,KAAK,CAAC;AAElH,QAAM,YAA0B,CAAC;AACjC,aAAW,YAAY,WAAW;AAE9B,UAAM,MAAM,cAAc,KAAK,OAAK,gBAAgB,GAAG,SAAS,KAAK,KAAK,IAAI;AAC9E,QAAI,KAAK;AACL,aAAO,KAAK,WAAW,SAAS,OAAO,gCAAgC,SAAS,KAAK,6BAA6B,GAAG,IAAI;AACzH;AAAA,IACJ;AAEA,QAAI,cAAc,CAAC,kBAAkB,SAAS,KAAK,GAAG;AAClD,aAAO,KAAK,WAAW,SAAS,OAAO,sBAAsB,SAAS,KAAK,2BAAsB,eAAe,2CAA2C;AAC3J;AAAA,IACJ;AACA,QAAI;AACA,YAAM,WAAW,4BAA4B,SAAS,UAAU,IAAI;AACpE,gBAAU,KAAK,QAAQ;AACvB,qBAAe,OAAO;AACtB,oBAAc,KAAK,SAAS,KAAK;AACjC,aAAO,KAAK,WAAW,SAAS,OAAO,oBAAoB,SAAS,KAAK,eAAe,SAAS,EAAE,GAAG;AAAA,IAC1G,SAAS,KAAK;AACV,aAAO,KAAK,WAAW,4BAA4B,SAAS,KAAK,MAAO,IAAc,OAAO,EAAE;AAAA,IACnG;AAAA,EACJ;AAEA,SAAO;AACX;AAQA,SAAS,gBAAgB,GAAW,GAAmB;AACnD,QAAM,WAAW,CAAC,MAAc,IAAI;AAAA,IAChC,EAAE,YAAY,EACT,QAAQ,YAAY,GAAG,EACvB,MAAM,KAAK,EACX,OAAO,OAAK,EAAE,SAAS,KAAK,CAAC,WAAW,IAAI,CAAC,CAAC;AAAA,EACvD;AACA,QAAM,KAAK,SAAS,CAAC;AACrB,QAAM,KAAK,SAAS,CAAC;AACrB,MAAI,GAAG,SAAS,KAAK,GAAG,SAAS,EAAG,QAAO;AAC3C,MAAI,eAAe;AACnB,aAAW,KAAK,GAAI,KAAI,GAAG,IAAI,CAAC,EAAG;AACnC,QAAM,QAAQ,GAAG,OAAO,GAAG,OAAO;AAClC,SAAO,UAAU,IAAI,IAAI,eAAe;AAC5C;AAEA,MAAM,aAAa,oBAAI,IAAI;AAAA,EACvB;AAAA,EAAO;AAAA,EAAO;AAAA,EAAO;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAS;AAAA,EAAS;AAAA,EAAS;AAAA,EAC/D;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AAAA,EAAQ;AACpD,CAAC;AAQM,SAAS,sBAA2C;AACvD,QAAM,cAAc,UAAU,QAAQ,EAAE,IAAI,OAAK,EAAE,KAAK;AACxD,QAAM,iBAA2B,CAAC;AAClC,aAAW,KAAK,UAAU,GAAG;AACzB,eAAW,MAAM,EAAE,YAAY,CAAC,GAAG;AAC/B,UAAI,GAAG,WAAW,SAAU,gBAAe,KAAK,GAAG,EAAE,KAAK,WAAM,GAAG,KAAK,EAAE;AAAA,IAC9E;AAAA,EACJ;AACA,QAAM,iBAA2B,CAAC;AAClC,MAAI;AACA,UAAM,OAAO,YAAY,EAAE,OAAO,GAAG,CAAC;AACtC,eAAW,SAAS,MAAM;AACtB,qBAAe,KAAK,IAAI,MAAM,IAAI,KAAK,MAAM,OAAO,EAAE;AAAA,IAC1D;AAAA,EACJ,QAAQ;AAAA,EAA8C;AAEtD,SAAO;AAAA,IACH;AAAA,IACA,gBAAgB,eAAe,MAAM,GAAG,EAAE;AAAA,IAC1C;AAAA,EACJ;AACJ;","names":["listGoals"]}
@@ -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
- function enqueueAdvisory(userId, decision) {
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 line = JSON.stringify({
61
- at: (/* @__PURE__ */ new Date()).toISOString(),
62
- action: decision.action,
63
- rationale: decision.rationale,
64
- confidence: decision.confidence,
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 prev = existsSync(path) ? readFileSync(path, "utf-8") : "";
69
- writeFileSync(path, prev + line + "\n");
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":";AA4BA,OAAO,YAAY;AACnB,SAAS,mBAAmB,cAAc,yBAAyB;AACnE,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAE/B,MAAM,YAAY;AAGlB,MAAM,oBAAoB,IAAI,KAAK;AAGnC,SAAS,WAAW,MAAM,oBAAI,KAAK,GAAW;AAC1C,QAAM,OAAO,IAAI,SAAS;AAC1B,QAAM,MAAM,IAAI,OAAO;AACvB,QAAM,UAAU,QAAQ,KAAK,QAAQ,IAAI,YAAY;AACrD,SAAO,GAAG,OAAO,IAAI,KAAK,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AACzD;AAuBO,SAAS,YACZ,SAAS,gBACT,SAAS,UACT,MACa;AACb,QAAM,UAAU,MAAM,aAAa,IAAI,KAAK;AAC5C,MAAI,SAAS,SAAS;AAClB,WAAO,EAAE,QAAQ,WAAW,WAAW,iBAAiB;AAAA,EAC5D;AAKA,eAAa,QAAQ,QAAQ,WAAW,CAAC,EAAE;AAE3C,QAAM,cAAc,kBAAkB,QAAQ,EAAE,YAAY,GAAG,CAAC;AAChE,MAAI,YAAY,WAAW,GAAG;AAC1B,WAAO,EAAE,QAAQ,WAAW,WAAW,yCAAyC;AAAA,EACpF;AAIA,QAAM,OAAO,YAAY,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC;AAMtE,QAAM,UAAU,gBAAgB,MAAM;AACtC,MAAI,QAAQ,SAAS,cAAc,KAAK;AAGpC,WAAO,EAAE,QAAQ,WAAW,WAAW,mDAA8C;AAAA,EACzF;AAEA,SAAO;AAAA,IACH,QAAQ,KAAK,SAAS,eAAe,eAC7B,KAAK,SAAS,iBAAiB,iBAC/B,KAAK,SAAS,aAAa,aAC3B;AAAA,IACR,WAAW,KAAK;AAAA,IAChB,YAAY,KAAK;AAAA,IACjB,SAAS,EAAE,QAAQ,KAAK,QAAQ,eAAe,eAAe,GAAG,GAAG;AAAA,EACxE;AACJ;AAQO,SAAS,aAAa,SAAS,gBAAgB,SAAS,UAAyB;AACpF,QAAM,IAAI,YAAY,QAAQ,MAAM;AACpC,MAAI,EAAE,WAAW,WAAW;AAExB,WAAO;AAAA,EACX;AACA,SAAO,KAAK,WAAW,kBAAkB,EAAE,MAAM,eAAe,EAAE,YAAY,QAAQ,CAAC,CAAC,eAAe,EAAE,SAAS,GAAG;AAarH,kBAAgB,QAAQ,CAAC;AACzB,SAAO;AACX;AAIA,SAAS,eAAe,cAAc,YAAY,iBAAiB;AACnE,SAAS,YAAY;AACrB,SAAS,eAAe;AAExB,SAAS,eAAe,QAAwB;AAC5C,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,OAAO,MACN,IAAI,WAAW,IAAI,IAAI,KAAK,QAAQ,GAAG,IAAI,MAAM,CAAC,CAAC,IAAI,MACxD,KAAK,QAAQ,GAAG,QAAQ;AAC9B,SAAO,KAAK,MAAM,SAAS,QAAQ,uBAAuB;AAC9D;AAmBA,MAAM,0BAA0B,KAAK,KAAK,KAAK;AAC/C,MAAM,uBAAuB,IAAI,KAAK,KAAK,KAAK;AAEhD,SAAS,mBAA2B;AAChC,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,IAAI,MAAM,OAAO,GAAG,IAAI;AAC9B,SAAO,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI,IAAI;AAC9C;AAEA,SAAS,iBAAyB;AAC9B,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,IAAI,MAAM,OAAO,GAAG,IAAI;AAC9B,SAAO,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI,IAAI;AAC9C;AAUA,SAAS,kBAAkB,KAA+B;AACtD,MAAI,CAAC,IAAI,KAAK,EAAG,QAAO,CAAC;AACzB,QAAM,MAAwB,CAAC;AAC/B,aAAW,QAAQ,IAAI,MAAM,IAAI,GAAG;AAChC,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,QAAS;AACd,QAAI;AACA,YAAM,MAAM,KAAK,MAAM,OAAO;AAC9B,UAAI,OAAO,OAAO,IAAI,OAAO,YAAY,OAAO,IAAI,WAAW,SAAU,KAAI,KAAK,GAAG;AAAA,IACzF,QAAQ;AAAA,IAA8D;AAAA,EAC1E;AACA,SAAO;AACX;AAIA,SAAS,SAAS,QAAgB,WAA2B;AACzD,QAAM,IAAI,UAAU,YAAY,EAAE,QAAQ,QAAQ,GAAG,EAAE,QAAQ,cAAc,EAAE,EAAE,KAAK;AACtF,SAAO,GAAG,MAAM,KAAK,CAAC;AAC1B;AAEO,SAAS,gBAAgB,QAAgB,UAAyB,MAAc,KAAK,IAAI,GAAS;AACrG,MAAI,SAAS,WAAW,UAAW;AACnC,QAAM,OAAO,eAAe,MAAM;AAClC,MAAI;AAAE,cAAU,KAAK,MAAM,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAAA,EAAG,QAAQ;AAAA,EAAe;AAK/E,QAAM,aAAa,SAAS,aAAa,IAAI,KAAK,KAAK,SAAS;AAChE,QAAM,aAAa,OAAO,SAAS,eAAe,YAAY,OAAO,SAAS,SAAS,UAAU,IAAI,SAAS,aAAa;AAE3H,QAAM,cAAc,iBAAiB;AACrC,QAAM,YAAY,eAAe;AACjC,QAAM,cAAc,SAAS,SAAS,QAAQ,SAAS;AAEvD,MAAI;AACA,UAAM,WAAW,WAAW,IAAI,IAAI,kBAAkB,aAAa,MAAM,OAAO,CAAC,IAAI,CAAC;AAGtF,UAAM,kBAAkB,MAAM;AAC9B,UAAM,WAAW,SAAS,OAAO,OAAK;AAClC,YAAM,IAAI,IAAI,KAAK,EAAE,EAAE,EAAE,QAAQ;AACjC,aAAO,OAAO,SAAS,CAAC,KAAK,KAAK;AAAA,IACtC,CAAC;AAGD,UAAM,cAAc,MAAM;AAC1B,UAAM,QAAQ,SAAS,KAAK,OAAK;AAC7B,YAAM,IAAI,IAAI,KAAK,EAAE,EAAE,EAAE,QAAQ;AACjC,UAAI,CAAC,OAAO,SAAS,CAAC,KAAK,IAAI,YAAa,QAAO;AACnD,aAAO,SAAS,EAAE,QAAQ,EAAE,SAAS,MAAM;AAAA,IAC/C,CAAC;AACD,QAAI,OAAO;AAGP,aAAO,MAAM,WAAW,sCAAsC,KAAK,MAAM,cAAc,IAAQ,CAAC,aAAa,SAAS,MAAM,WAAM,UAAU,MAAM,GAAG,EAAE,CAAC,EAAE;AAG1J,UAAI,SAAS,WAAW,SAAS,QAAQ;AACrC,cAAMA,QAAO,SAAS,IAAI,OAAK,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,IAAI,KAAK,SAAS,SAAS,IAAI,OAAO;AAC7F,sBAAc,MAAMA,KAAI;AAAA,MAC5B;AACA;AAAA,IACJ;AAGA,UAAM,QAAwB;AAAA,MAC1B,IAAI,IAAI,KAAK,GAAG,EAAE,YAAY;AAAA,MAC9B,QAAQ,SAAS;AAAA,MACjB;AAAA,MACA;AAAA,MACA,SAAS,SAAS;AAAA,IACtB;AACA,UAAM,QAAQ,CAAC,GAAG,UAAU,KAAK;AACjC,UAAM,OAAO,MAAM,IAAI,OAAK,KAAK,UAAU,CAAC,CAAC,EAAE,KAAK,IAAI,IAAI;AAC5D,kBAAc,MAAM,IAAI;AACxB,QAAI,SAAS,WAAW,SAAS,QAAQ;AACrC,aAAO,KAAK,WAAW,UAAU,SAAS,SAAS,SAAS,MAAM,6BAA6B,KAAK,MAAM,YAAY,KAAS,CAAC,QAAQ;AAAA,IAC5I;AAAA,EACJ,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,+BAAgC,IAAc,OAAO,EAAE;AAAA,EAClF;AACJ;AAIO,SAAS,qBAAqB,QAAgB,QAAQ,GAM1D;AACC,QAAM,OAAO,eAAe,MAAM;AAClC,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO,CAAC;AAC/B,MAAI;AACA,UAAM,QAAQ,aAAa,MAAM,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,EAAE,OAAO,OAAO;AAC3E,WAAO,MAAM,MAAM,CAAC,KAAK,EAAE,IAAI,OAAK,KAAK,MAAM,CAAC,CAAC,EAAE,QAAQ;AAAA,EAC/D,QAAQ;AACJ,WAAO,CAAC;AAAA,EACZ;AACJ;AAyBA,MAAM,yBAAyB,KAAK,KAAK,KAAK;AAC9C,MAAM,wBAAwB,KAAK,KAAK,KAAK;AAE7C,SAAS,UAAU,QAAwB;AACvC,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,OAAO,MACN,IAAI,WAAW,IAAI,IAAI,KAAK,QAAQ,GAAG,IAAI,MAAM,CAAC,CAAC,IAAI,MACxD,KAAK,QAAQ,GAAG,QAAQ;AAC9B,SAAO,KAAK,MAAM,SAAS,QAAQ,kBAAkB;AACzD;AAGA,SAAS,WAAW,QAAwB;AACxC,QAAM,OAAO,UAAU,MAAM;AAC7B,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,MAAI;AACA,UAAM,QAAQ,aAAa,MAAM,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,EAAE,OAAO,OAAO;AAC3E,UAAM,OAAO,MAAM,MAAM,SAAS,CAAC;AACnC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,WAAO,MAAM,KAAK,KAAK,MAAM,MAAM,EAAE,IAAI;AAAA,EAC7C,QAAQ;AAAE,WAAO;AAAA,EAAG;AACxB;AAEA,SAAS,eAAe,QAAgB,SAAwC;AAC5E,QAAM,OAAO,UAAU,MAAM;AAC7B,MAAI;AAAE,cAAU,KAAK,MAAM,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAAA,EAAG,QAAQ;AAAA,EAAe;AAC/E,QAAM,OAAO,KAAK,UAAU,EAAE,KAAI,oBAAI,KAAK,GAAE,YAAY,GAAG,GAAG,QAAQ,CAAC;AACxE,MAAI;AACA,UAAM,OAAO,WAAW,IAAI,IAAI,aAAa,MAAM,OAAO,IAAI;AAC9D,kBAAc,MAAM,OAAO,OAAO,IAAI;AAAA,EAC1C,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,0BAA2B,IAAc,OAAO,EAAE;AAAA,EAC7E;AACJ;AAcO,SAAS,gBAAgB,SAAS,gBAAgB,MAAM,KAAK,IAAI,GAAG,MAA8C;AACrH,QAAM,QAAQ,MAAM,UAAU;AAC9B,MAAI,CAAC,OAAO;AACR,UAAM,QAAQ,MAAM,WAAW,MAAM;AACrC,QAAI,QAAQ,sBAAuB,QAAO;AAAA,EAC9C;AAGA,QAAM,UAAU,gBAAgB,MAAM;AAMtC,MAAI,CAAC,SAAS,QAAQ,iBAAiB,SAAS,GAAG;AAC/C,WAAO;AAAA,EACX;AAGA,MAAI,CAAC,SAAS,QAAQ,SAAS,cAAc,IAAK,QAAO;AAEzD,QAAM,SAAS,QAAQ,iBAAiB,MAAM,GAAG;AACjD,QAAM,OAAO,kBAAkB,QAAQ,EAAE,YAAY,GAAG,MAAM,EAAE,CAAC;AACjE,QAAM,cAAc,eAAe;AAEnC,QAAM,UAAU;AAAA,IACZ;AAAA,IACA,GAAG,OAAO,IAAI,OAAK,OAAO,CAAC,EAAE;AAAA,IAC7B;AAAA,IACA;AAAA,IACA,GAAI,KAAK,WAAW,IACd,CAAC,0CAAqC,IACtC,KAAK,IAAI,OAAK,OAAO,EAAE,MAAM,QAAK,EAAE,KAAK,EAAE;AAAA,IACjD;AAAA,IACA,cAAc,iBAAiB,YAAY,IAAI,KAAK,YAAY,QAAQ,MAAM,wBAAwB;AAAA,EAC1G,EAAE,KAAK,IAAI;AAEX,SAAO;AAAA,IACH,WAAW;AAAA,IACX;AAAA,EACJ;AACJ;AAQA,eAAsB,aAAa,SAAS,gBAAgB,MAA6E;AACrI,QAAM,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,GAAG,IAAI;AACtD,MAAI,CAAC,OAAO;AACR,WAAO,EAAE,WAAW,OAAO,QAAQ,+CAA+C;AAAA,EACtF;AACA,SAAO,KAAK,WAAW,8BAA8B,MAAM,EAAE;AAC7D,iBAAe,QAAQ,EAAE,OAAO,QAAQ,CAAC;AAEzC,QAAM,SAAS;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACJ,EAAE,KAAK,IAAI;AAEX,MAAI;AAEA,UAAM,EAAE,eAAe,IAAI,MAAM,OAAO,YAAY;AACpD,UAAM,SAAS,MAAM,eAAe,QAAQ,mBAAmB,QAAQ,CAAC,CAAC;AACzE,UAAM,aAAa,OAAO,aAAa,CAAC,GAAG,KAAK,IAAI;AACpD,UAAM,cAAc,OAAO,aAAa,CAAC,GAAG,SAAS,eAAe;AACpE,mBAAe,QAAQ;AAAA,MACnB,OAAO;AAAA,MACP;AAAA,MACA,WAAW;AAAA,MACX,UAAU,OAAO,WAAW,IAAI,MAAM,GAAG,GAAG;AAAA,IAChD,CAAC;AACD,WAAO,KAAK,WAAW,eAAe,aAAa,mBAAmB,UAAU,YAAY,SAAS,GAAG;AACxG,WAAO,EAAE,WAAW,MAAM,QAAQ,aAAa,mBAAmB,iBAAiB;AAAA,EACvF,SAAS,KAAK;AACV,mBAAe,QAAQ,EAAE,OAAO,SAAS,OAAQ,IAAc,QAAQ,CAAC;AACxE,WAAO,KAAK,WAAW,sBAAuB,IAAc,OAAO,EAAE;AACrE,WAAO,EAAE,WAAW,OAAO,QAAQ,UAAW,IAAc,OAAO,GAAG;AAAA,EAC1E;AACJ;AAIA,IAAI,cAAqC;AACzC,IAAI,kBAAyC;AAGtC,SAAS,oBAAoB,MAAqF;AACrH,MAAI,YAAa;AACjB,QAAM,WAAW,MAAM,cAAc;AACrC,QAAM,SAAS,MAAM,UAAU;AAE/B,gBAAc,YAAY,MAAM;AAC5B,QAAI;AAMA,mBAAa,QAAQ,QAAQ;AAAA,IACjC,SAAS,KAAK;AACV,aAAO,KAAK,WAAW,gBAAiB,IAAc,OAAO,EAAE;AAAA,IACnE;AAAA,EACJ,GAAG,QAAQ;AACX,MAAI,YAAY,MAAO,aAAY,MAAM;AACzC,SAAO,KAAK,WAAW,yCAAyC,KAAK,MAAM,WAAW,GAAI,CAAC,WAAW,MAAM,IAAI;AAMhH,QAAM,eAAe,MAAM,uBAAuB;AAClD,oBAAkB,YAAY,MAAM;AAChC,SAAK,aAAa,MAAM;AAAA,EAC5B,GAAG,YAAY;AACf,MAAI,gBAAgB,MAAO,iBAAgB,MAAM;AACjD,SAAO,KAAK,WAAW,qCAAqC,KAAK,MAAM,eAAe,IAAQ,CAAC,WAAW,MAAM,IAAI;AACxH;AAEO,SAAS,qBAA2B;AACvC,MAAI,aAAa;AAAE,kBAAc,WAAW;AAAG,kBAAc;AAAA,EAAM;AACnE,MAAI,iBAAiB;AAAE,kBAAc,eAAe;AAAG,sBAAkB;AAAA,EAAM;AACnF;AAEO,SAAS,kBAAwB;AACpC,qBAAmB;AACvB;","names":["body"]}
@@ -768,6 +768,27 @@ const TitanConfigSchema = z.object({
768
768
  * directly (scope-lock still enforces target prefix).
769
769
  */
770
770
  staging: z.boolean().default(true),
771
+ /**
772
+ * v6.0.3 — Self-repair goal-creation gate.
773
+ *
774
+ * When false (default), the goal proposer drops any autonomously
775
+ * generated proposal that classifies as self-mod / self-repair /
776
+ * framework-modification work before it reaches the approval
777
+ * queue. The self-repair daemon still surfaces *findings* via
778
+ * `custom`-type `self_repair` approvals (so Tony sees what's
779
+ * wrong), but TITAN will not spin up new active goals to "fix
780
+ * itself" without explicit opt-in.
781
+ *
782
+ * Why off by default: in v6.0.2 we observed the Soma pressure
783
+ * loop + dreaming proposer cooperating to spawn 7 simultaneous
784
+ * "rewrite the framework" goals that recursed on each other,
785
+ * filling the active mission queue with self-referential
786
+ * busywork. This was real autonomy, but in the wrong direction.
787
+ *
788
+ * Set true to restore prior behavior (autonomous self-repair
789
+ * goal creation enabled).
790
+ */
791
+ autoCreateGoals: z.boolean().default(false),
771
792
  /**
772
793
  * Directory for staged self-mod bundles. Each approved goal
773
794
  * gets its own subdir. Relative paths resolve under TITAN_HOME.