titan-agent 6.4.4 → 6.5.0

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.
Files changed (55) hide show
  1. package/dist/agent/agentLessons.js.map +1 -1
  2. package/dist/agent/agentLoop.js +5 -1
  3. package/dist/agent/agentLoop.js.map +1 -1
  4. package/dist/agent/orchestrator.js +5 -14
  5. package/dist/agent/orchestrator.js.map +1 -1
  6. package/dist/agent/somaInitiative.js.map +1 -1
  7. package/dist/agent/toolRunner.js +35 -0
  8. package/dist/agent/toolRunner.js.map +1 -1
  9. package/dist/channels/discord.js +10 -6
  10. package/dist/channels/discord.js.map +1 -1
  11. package/dist/channels/slack.js +7 -2
  12. package/dist/channels/slack.js.map +1 -1
  13. package/dist/channels/telegram.js +1 -1
  14. package/dist/channels/telegram.js.map +1 -1
  15. package/dist/config/schema.js +6 -0
  16. package/dist/config/schema.js.map +1 -1
  17. package/dist/gateway/openai-compat.js +69 -10
  18. package/dist/gateway/openai-compat.js.map +1 -1
  19. package/dist/gateway/server.js +79 -0
  20. package/dist/gateway/server.js.map +1 -1
  21. package/dist/migrations/runner.js.map +1 -1
  22. package/dist/migrations/safeRun.js.map +1 -1
  23. package/dist/providers/anthropic.js +7 -4
  24. package/dist/providers/anthropic.js.map +1 -1
  25. package/dist/providers/base.js +8 -1
  26. package/dist/providers/base.js.map +1 -1
  27. package/dist/providers/google.js +7 -4
  28. package/dist/providers/google.js.map +1 -1
  29. package/dist/providers/ollama.js +8 -7
  30. package/dist/providers/ollama.js.map +1 -1
  31. package/dist/providers/openai.js +7 -4
  32. package/dist/providers/openai.js.map +1 -1
  33. package/dist/providers/openai_compat.js +7 -4
  34. package/dist/providers/openai_compat.js.map +1 -1
  35. package/dist/providers/router.js +7 -3
  36. package/dist/providers/router.js.map +1 -1
  37. package/dist/skills/builtin/backup.js.map +1 -1
  38. package/dist/skills/builtin/browser.js +11 -21
  39. package/dist/skills/builtin/browser.js.map +1 -1
  40. package/dist/skills/builtin/calendar.js +6 -2
  41. package/dist/skills/builtin/calendar.js.map +1 -1
  42. package/dist/skills/builtin/canvas_spaces.js.map +1 -1
  43. package/dist/skills/builtin/data_analysis.js +1 -1
  44. package/dist/skills/builtin/data_analysis.js.map +1 -1
  45. package/dist/skills/builtin/jira_linear.js +6 -4
  46. package/dist/skills/builtin/jira_linear.js.map +1 -1
  47. package/dist/skills/builtin/web_browser.js +6 -0
  48. package/dist/skills/builtin/web_browser.js.map +1 -1
  49. package/dist/skills/builtin/web_fetch.js +1 -0
  50. package/dist/skills/builtin/web_fetch.js.map +1 -1
  51. package/dist/storage/starterSpaces.js.map +1 -1
  52. package/dist/utils/constants.js +1 -1
  53. package/dist/utils/constants.js.map +1 -1
  54. package/package.json +1 -1
  55. package/ui/dist/sw.js +1 -1
@@ -4,7 +4,6 @@ import { loadConfig } from "../config/config.js";
4
4
  import { spawnSubAgent, SUB_AGENT_TEMPLATES } from "./subAgent.js";
5
5
  import logger from "../utils/logger.js";
6
6
  import { createIssue, updateIssue } from "./commandPost.js";
7
- import { queueWakeup } from "./agentWakeup.js";
8
7
  import { claimNextTask, completeQueuedTask, failQueuedTask, getQueueStatus } from "./taskQueue.js";
9
8
  import { decomposeHierarchically, executeHierarchicalPlan, summarizePlan, flattenPlan } from "./hierarchicalPlanner.js";
10
9
  const COMPONENT = "Orchestrator";
@@ -125,17 +124,8 @@ async function executeDelegationPlan(plan) {
125
124
  });
126
125
  logger.info(COMPONENT, `[CP] Created issue ${issue.id} for ${agentName}: ${t.task.slice(0, 60)}`);
127
126
  updateIssue(issue.id, { status: "in_progress" });
128
- queueWakeup({
129
- agentName,
130
- task: t.task,
131
- issueId: issue.id,
132
- issueIdentifier: issue.id,
133
- agentId: agentName,
134
- parentSessionId: null,
135
- templateName: t.template
136
- });
137
127
  } catch (e) {
138
- logger.warn(COMPONENT, `[CP] Issue creation failed: ${e.message} \u2014 falling back to direct spawn`);
128
+ logger.warn(COMPONENT, `[CP] Issue creation failed: ${e.message}`);
139
129
  }
140
130
  }
141
131
  const result = await spawnSubAgent({
@@ -175,11 +165,12 @@ ${priorContext}` : t.task;
175
165
  taskResults.set(t.index, result);
176
166
  results.push(result);
177
167
  }
178
- const synthesis = results.map((r, i) => {
179
- const task = plan.tasks[i];
168
+ const synthesis = plan.tasks.map((task, i) => {
169
+ const r = taskResults.get(i);
170
+ if (!r) return "";
180
171
  const status = r.success ? "\u2705" : "\u274C";
181
172
  return `${status} **${task?.template || "task"}**: ${r.content.slice(0, 500)}`;
182
- }).join("\n\n");
173
+ }).filter(Boolean).join("\n\n");
183
174
  const durationMs = Date.now() - startTime;
184
175
  logger.info(COMPONENT, `Delegation complete: ${results.filter((r) => r.success).length}/${results.length} succeeded (${durationMs}ms)`);
185
176
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/agent/orchestrator.ts"],"sourcesContent":["/**\n * TITAN — Sub-Agent Orchestrator\n * Analyzes tasks for delegation potential, breaks them into parallel assignments,\n * spawns sub-agents, and synthesizes results.\n */\nimport { chat } from '../providers/router.js';\nimport { loadConfig } from '../config/config.js';\nimport { spawnSubAgent, SUB_AGENT_TEMPLATES, type SubAgentResult, type ModelTier } from './subAgent.js';\nimport logger from '../utils/logger.js';\nimport { createIssue, updateIssue } from './commandPost.js';\nimport { queueWakeup } from './agentWakeup.js';\nimport { claimNextTask, completeQueuedTask, failQueuedTask, getQueueStatus, type QueuedTask } from './taskQueue.js';\nimport { decomposeHierarchically, executeHierarchicalPlan, summarizePlan, flattenPlan, type HierarchicalPlanResult } from './hierarchicalPlanner.js';\n\nconst COMPONENT = 'Orchestrator';\n\nexport interface DelegationTask {\n template: string; // 'explorer' | 'coder' | 'browser' | 'analyst'\n task: string;\n dependsOn?: number[]; // indices of tasks this depends on\n}\n\nexport interface DelegationPlan {\n shouldDelegate: boolean;\n reason: string;\n tasks: DelegationTask[];\n}\n\nexport interface OrchestratorResult {\n content: string;\n subResults: SubAgentResult[];\n durationMs: number;\n}\n\n/**\n * Cheap pre-filter: skip the LLM classifier for messages that are obviously\n * not multi-step (greetings, single questions, status checks, etc.). This\n * keeps the per-message LLM cost off for trivial inputs while letting the\n * classifier judge anything substantive.\n *\n * v6.3.0: replaces the prior regex-allowlist gate, which only fired on\n * messages containing literal \"and/then/parallel/simultaneously\" phrasing\n * and missed most real multi-step missions (e.g. \"build me a dashboard\n * with charts and a backend\" matched none of the prior patterns).\n */\nfunction isObviouslyTrivial(message: string): boolean {\n const trimmed = message.trim();\n // Greetings / one-word replies\n if (/^(hi|hello|hey|yo|sup|ok|okay|thanks|thx|ty|yes|no|nope|sure)[!?.,]?$/i.test(trimmed)) return true;\n // Single question that ends with ? and has no conjunction — likely a lookup, not a mission\n if (trimmed.length < 80 && /\\?$/.test(trimmed) && !/(?:\\band\\b|\\bthen\\b|,)/i.test(trimmed)) return true;\n // Status / acknowledgement check\n if (/^(status|ok\\??|done\\??|ready\\??|check|ping)$/i.test(trimmed)) return true;\n return false;\n}\n\n/** Analyze whether a message would benefit from sub-agent delegation */\nexport async function analyzeForDelegation(message: string): Promise<DelegationPlan> {\n const config = loadConfig();\n const fastModel = config.agent.modelAliases?.fast || 'ollama/qwen3.5:cloud';\n\n // Cheap heuristic — keep the LLM classifier off for the messages that\n // can't possibly be multi-step missions.\n const wordCount = message.split(/\\s+/).length;\n if (wordCount < 10) {\n return { shouldDelegate: false, reason: 'Message too short for delegation', tasks: [] };\n }\n if (isObviouslyTrivial(message)) {\n return { shouldDelegate: false, reason: 'Message is obviously not multi-step', tasks: [] };\n }\n\n // Anything that passes the cheap gate goes to the LLM classifier. We trust\n // it (with the prompt below) to recognize multi-step intent — including\n // shapes the old regex allowlist missed (\"build a dashboard with charts\n // and a backend\", \"audit these 4 files\", \"compare X vs Y vs Z\").\n\n try {\n const response = await chat({\n model: fastModel,\n messages: [\n {\n role: 'system',\n content: `You are TITAN's CEO task decomposer. Break complex tasks into small, focused sub-tasks for worker agents.\n\nAvailable workers (with engineering personas):\n- coder: reads/writes/edits files, runs shell commands (MAX 30 lines per edit)\n Personas: tdd-engineer, frontend-engineer, incremental-builder, simplifier\n- explorer: web research, searches, fetches URLs\n Personas: context-engineer, idea-refiner\n- browser: interactive web pages, form filling, screenshots\n Personas: browser-tester, perf-optimizer\n- analyst: data analysis, memory, code review\n Personas: code-reviewer, security-engineer, debugger, spec-writer\n\nWhen delegating, specify the persona that best fits the subtask.\nExample: { template: coder, task: ..., persona: tdd-engineer }\n\nCRITICAL RULES FOR CODING TASKS:\n- NEVER give a coder agent a task that requires writing >50 lines of code at once\n- Break large file changes into MULTIPLE small coder tasks:\n Example: \"Add network scanner to dashboard\" becomes:\n 1. coder: \"Read <repo>/dashboard.html and add a new <section> after the machines grid with id='network-scanner' and a heading 'Network Scanner'\"\n 2. coder: \"Add CSS styles for .scanner-grid and .scanner-card to the <style> block in <repo>/dashboard.html\"\n 3. coder: \"Add a JavaScript function scanNetwork() that fetches IPs 192.168.1.1-254 and updates the scanner section in <repo>/dashboard.html\"\n 4. coder: \"Add a call to scanNetwork() in the initialization block and a 60-second interval refresh in <repo>/dashboard.html\"\n\nEach coder task should edit ONE section of ONE file. Use edit_file, not write_file for existing files.\n\nRespond with ONLY valid JSON:\n{\n \"shouldDelegate\": true/false,\n \"reason\": \"brief explanation\",\n \"tasks\": [\n { \"template\": \"coder|explorer|browser|analyst\", \"task\": \"specific focused instruction with exact file path and what to change\" }\n ]\n}\n\nRules:\n- Delegate if 2+ sub-tasks exist\n- Each task: self-contained, actionable, <50 lines of code\n- Max 6 sub-tasks\n- Include exact file paths in task descriptions\n- For file edits: specify WHICH section to change (e.g. \"add after the </style> tag\")`,\n },\n { role: 'user', content: message },\n ],\n maxTokens: 500,\n temperature: 0.1,\n });\n\n let jsonStr = response.content.trim();\n if (jsonStr.startsWith('```')) {\n jsonStr = jsonStr.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '');\n }\n\n const parsed = JSON.parse(jsonStr) as DelegationPlan;\n\n // Validate\n if (!parsed.tasks || !Array.isArray(parsed.tasks)) {\n return { shouldDelegate: false, reason: 'Invalid delegation plan', tasks: [] };\n }\n\n // Cap at 4 tasks\n parsed.tasks = parsed.tasks.slice(0, 6);\n\n logger.info(COMPONENT, `Delegation analysis: ${parsed.shouldDelegate ? 'YES' : 'NO'} — ${parsed.reason} (${parsed.tasks.length} tasks)`);\n return parsed;\n } catch (err) {\n logger.warn(COMPONENT, `Delegation analysis failed: ${(err as Error).message}`);\n return { shouldDelegate: false, reason: 'Analysis failed', tasks: [] };\n }\n}\n\n/** Execute a delegation plan — runs independent tasks in parallel, dependent tasks sequentially */\nexport async function executeDelegationPlan(plan: DelegationPlan): Promise<OrchestratorResult> {\n const startTime = Date.now();\n const results: SubAgentResult[] = [];\n\n if (!plan.shouldDelegate || plan.tasks.length === 0) {\n return {\n content: 'No delegation needed.',\n subResults: [],\n durationMs: 0,\n };\n }\n\n logger.info(COMPONENT, `Executing delegation plan: ${plan.tasks.length} tasks`);\n\n // Group tasks: those with dependencies run after their deps, independent ones run in parallel\n const taskResults: Map<number, SubAgentResult> = new Map();\n\n // Find independent tasks (no dependsOn)\n const independent = plan.tasks.map((t, i) => ({ ...t, index: i }))\n .filter(t => !t.dependsOn || t.dependsOn.length === 0);\n\n const dependent = plan.tasks.map((t, i) => ({ ...t, index: i }))\n .filter(t => t.dependsOn && t.dependsOn.length > 0);\n\n // Execute independent tasks via Command Post (Paperclip pattern)\n const config = loadConfig();\n const cpEnabled = (config.commandPost as Record<string, unknown> | undefined)?.enabled;\n\n if (independent.length > 0) {\n const parallelResults = await Promise.all(\n independent.map(async (t) => {\n const template = SUB_AGENT_TEMPLATES[t.template] || SUB_AGENT_TEMPLATES.explorer;\n const agentName = template.name || t.template;\n\n // Create Command Post issue for tracking\n if (cpEnabled) {\n try {\n const issue = createIssue({\n title: t.task.slice(0, 80),\n description: t.task,\n priority: 'medium',\n createdByUser: 'orchestrator',\n });\n logger.info(COMPONENT, `[CP] Created issue ${issue.id} for ${agentName}: ${t.task.slice(0, 60)}`);\n updateIssue(issue.id, { status: 'in_progress' });\n\n // Queue wakeup for async execution\n queueWakeup({\n agentName,\n task: t.task,\n issueId: issue.id,\n issueIdentifier: issue.id,\n agentId: agentName,\n parentSessionId: null,\n templateName: t.template,\n });\n } catch (e) {\n logger.warn(COMPONENT, `[CP] Issue creation failed: ${(e as Error).message} — falling back to direct spawn`);\n }\n }\n\n // Execute (sync for now — wakeup handles async)\n const result = await spawnSubAgent({\n name: agentName,\n task: t.task,\n tools: template.tools,\n systemPrompt: template.systemPrompt,\n persona: (template as { persona?: string }).persona,\n tier: (template as { tier?: ModelTier }).tier,\n });\n return { index: t.index, result };\n })\n );\n for (const { index, result } of parallelResults) {\n taskResults.set(index, result);\n results.push(result);\n }\n }\n\n // Execute dependent tasks sequentially\n for (const t of dependent) {\n // Inject prior results into the task context\n const priorContext = (t.dependsOn || [])\n .map(depIdx => {\n const depResult = taskResults.get(depIdx);\n return depResult ? `Previous result: ${depResult.content.slice(0, 500)}` : '';\n })\n .filter(Boolean)\n .join('\\n');\n\n const enrichedTask = priorContext\n ? `${t.task}\\n\\nContext from previous steps:\\n${priorContext}`\n : t.task;\n\n const template = SUB_AGENT_TEMPLATES[t.template] || SUB_AGENT_TEMPLATES.explorer;\n const result = await spawnSubAgent({\n name: template.name || t.template,\n task: enrichedTask,\n tools: template.tools,\n systemPrompt: template.systemPrompt,\n persona: (template as { persona?: string }).persona,\n tier: (template as { tier?: ModelTier }).tier,\n });\n taskResults.set(t.index, result);\n results.push(result);\n }\n\n // Synthesize results\n const synthesis = results.map((r, i) => {\n const task = plan.tasks[i];\n const status = r.success ? '✅' : '❌';\n return `${status} **${task?.template || 'task'}**: ${r.content.slice(0, 500)}`;\n }).join('\\n\\n');\n\n const durationMs = Date.now() - startTime;\n logger.info(COMPONENT, `Delegation complete: ${results.filter(r => r.success).length}/${results.length} succeeded (${durationMs}ms)`);\n\n return {\n content: synthesis,\n subResults: results,\n durationMs,\n };\n}\n\n// ─── Task Queue Integration ────────────────────────────────────────────────\n\n/**\n * Work the shared task queue — claim and execute the next available task.\n * Pulls from goals, plans, and command post via the unified taskQueue facade.\n */\nexport async function executeFromQueue(agentId: string): Promise<SubAgentResult | null> {\n const claim = claimNextTask(agentId);\n if (!claim.success || !claim.task) {\n logger.debug(COMPONENT, `No tasks in queue for ${agentId}`);\n return null;\n }\n\n const task = claim.task;\n logger.info(COMPONENT, `Queue: ${agentId} executing \"${task.title}\" (${task.source}, priority ${task.priority})`);\n\n // Infer template from task source/description\n const template = inferQueueTemplate(task);\n const templateDef = SUB_AGENT_TEMPLATES[template] || SUB_AGENT_TEMPLATES.coder;\n\n try {\n const result = await spawnSubAgent({\n name: `Queue-${task.source}-${task.id.split(':').pop()}`,\n task: `${task.title}\\n\\n${task.description}`,\n tools: templateDef.tools,\n systemPrompt: templateDef.systemPrompt,\n tier: (templateDef as { tier?: ModelTier }).tier,\n });\n\n if (result.success) {\n completeQueuedTask(task.id, claim.checkoutRunId, result.content);\n } else {\n failQueuedTask(task.id, claim.checkoutRunId, result.content);\n }\n\n return result;\n } catch (err) {\n failQueuedTask(task.id, claim.checkoutRunId, (err as Error).message);\n throw err;\n }\n}\n\n/** Infer the best sub-agent template for a queued task */\nfunction inferQueueTemplate(task: QueuedTask): string {\n const text = `${task.title} ${task.description}`.toLowerCase();\n if (/\\b(write|create|build|code|implement|edit|fix|deploy)\\b/.test(text)) return 'coder';\n if (/\\b(research|search|find|discover|explore)\\b/.test(text)) return 'explorer';\n if (/\\b(analyze|report|summarize|compare|review)\\b/.test(text)) return 'analyst';\n if (/\\b(browse|navigate|login|click|form|page)\\b/.test(text)) return 'browser';\n return 'coder';\n}\n\n/**\n * Get a snapshot of the shared task queue for status reporting.\n */\nexport function getTaskQueueSnapshot() {\n return getQueueStatus();\n}\n\n// ─── Hierarchical Delegation ───────────────────────────────────────────────\n\n/**\n * Decompose a complex goal into a multi-level hierarchical plan and execute it.\n * Uses LLM-driven decomposition: goal → phases → tasks → subtasks (3 levels max).\n * Compound tasks recurse, simple tasks dispatch to sub-agents.\n */\nexport async function executeHierarchicalDelegation(\n goal: string,\n opts?: { maxDepth?: number; baseRounds?: number },\n): Promise<{ result: HierarchicalPlanResult; summary: string }> {\n const maxDepth = opts?.maxDepth ?? 3;\n const baseRounds = opts?.baseRounds ?? 15;\n\n logger.info(COMPONENT, `Hierarchical delegation: \"${goal.slice(0, 80)}...\" (maxDepth: ${maxDepth})`);\n\n // Phase 1: Decompose\n const plan = await decomposeHierarchically(goal, maxDepth);\n const taskCount = flattenPlan(plan).length;\n logger.info(COMPONENT, `Decomposed into ${taskCount} tasks across ${maxDepth} levels`);\n\n // Phase 2: Execute\n const result = await executeHierarchicalPlan(plan, 0, baseRounds);\n\n // Phase 3: Summarize\n const summary = summarizePlan(plan);\n logger.info(COMPONENT, `Hierarchical delegation complete: ${result.completedTasks}/${result.totalTasks} succeeded`);\n\n return { result, summary };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/agent/orchestrator.ts"],"sourcesContent":["/**\n * TITAN — Sub-Agent Orchestrator\n * Analyzes tasks for delegation potential, breaks them into parallel assignments,\n * spawns sub-agents, and synthesizes results.\n */\nimport { chat } from '../providers/router.js';\nimport { loadConfig } from '../config/config.js';\nimport { spawnSubAgent, SUB_AGENT_TEMPLATES, type SubAgentResult, type ModelTier } from './subAgent.js';\nimport logger from '../utils/logger.js';\nimport { createIssue, updateIssue } from './commandPost.js';\nimport { claimNextTask, completeQueuedTask, failQueuedTask, getQueueStatus, type QueuedTask } from './taskQueue.js';\nimport { decomposeHierarchically, executeHierarchicalPlan, summarizePlan, flattenPlan, type HierarchicalPlanResult } from './hierarchicalPlanner.js';\n\nconst COMPONENT = 'Orchestrator';\n\nexport interface DelegationTask {\n template: string; // 'explorer' | 'coder' | 'browser' | 'analyst'\n task: string;\n dependsOn?: number[]; // indices of tasks this depends on\n}\n\nexport interface DelegationPlan {\n shouldDelegate: boolean;\n reason: string;\n tasks: DelegationTask[];\n}\n\nexport interface OrchestratorResult {\n content: string;\n subResults: SubAgentResult[];\n durationMs: number;\n}\n\n/**\n * Cheap pre-filter: skip the LLM classifier for messages that are obviously\n * not multi-step (greetings, single questions, status checks, etc.). This\n * keeps the per-message LLM cost off for trivial inputs while letting the\n * classifier judge anything substantive.\n *\n * v6.3.0: replaces the prior regex-allowlist gate, which only fired on\n * messages containing literal \"and/then/parallel/simultaneously\" phrasing\n * and missed most real multi-step missions (e.g. \"build me a dashboard\n * with charts and a backend\" matched none of the prior patterns).\n */\nfunction isObviouslyTrivial(message: string): boolean {\n const trimmed = message.trim();\n // Greetings / one-word replies\n if (/^(hi|hello|hey|yo|sup|ok|okay|thanks|thx|ty|yes|no|nope|sure)[!?.,]?$/i.test(trimmed)) return true;\n // Single question that ends with ? and has no conjunction — likely a lookup, not a mission\n if (trimmed.length < 80 && /\\?$/.test(trimmed) && !/(?:\\band\\b|\\bthen\\b|,)/i.test(trimmed)) return true;\n // Status / acknowledgement check\n if (/^(status|ok\\??|done\\??|ready\\??|check|ping)$/i.test(trimmed)) return true;\n return false;\n}\n\n/** Analyze whether a message would benefit from sub-agent delegation */\nexport async function analyzeForDelegation(message: string): Promise<DelegationPlan> {\n const config = loadConfig();\n const fastModel = config.agent.modelAliases?.fast || 'ollama/qwen3.5:cloud';\n\n // Cheap heuristic — keep the LLM classifier off for the messages that\n // can't possibly be multi-step missions.\n const wordCount = message.split(/\\s+/).length;\n if (wordCount < 10) {\n return { shouldDelegate: false, reason: 'Message too short for delegation', tasks: [] };\n }\n if (isObviouslyTrivial(message)) {\n return { shouldDelegate: false, reason: 'Message is obviously not multi-step', tasks: [] };\n }\n\n // Anything that passes the cheap gate goes to the LLM classifier. We trust\n // it (with the prompt below) to recognize multi-step intent — including\n // shapes the old regex allowlist missed (\"build a dashboard with charts\n // and a backend\", \"audit these 4 files\", \"compare X vs Y vs Z\").\n\n try {\n const response = await chat({\n model: fastModel,\n messages: [\n {\n role: 'system',\n content: `You are TITAN's CEO task decomposer. Break complex tasks into small, focused sub-tasks for worker agents.\n\nAvailable workers (with engineering personas):\n- coder: reads/writes/edits files, runs shell commands (MAX 30 lines per edit)\n Personas: tdd-engineer, frontend-engineer, incremental-builder, simplifier\n- explorer: web research, searches, fetches URLs\n Personas: context-engineer, idea-refiner\n- browser: interactive web pages, form filling, screenshots\n Personas: browser-tester, perf-optimizer\n- analyst: data analysis, memory, code review\n Personas: code-reviewer, security-engineer, debugger, spec-writer\n\nWhen delegating, specify the persona that best fits the subtask.\nExample: { template: coder, task: ..., persona: tdd-engineer }\n\nCRITICAL RULES FOR CODING TASKS:\n- NEVER give a coder agent a task that requires writing >50 lines of code at once\n- Break large file changes into MULTIPLE small coder tasks:\n Example: \"Add network scanner to dashboard\" becomes:\n 1. coder: \"Read <repo>/dashboard.html and add a new <section> after the machines grid with id='network-scanner' and a heading 'Network Scanner'\"\n 2. coder: \"Add CSS styles for .scanner-grid and .scanner-card to the <style> block in <repo>/dashboard.html\"\n 3. coder: \"Add a JavaScript function scanNetwork() that fetches IPs 192.168.1.1-254 and updates the scanner section in <repo>/dashboard.html\"\n 4. coder: \"Add a call to scanNetwork() in the initialization block and a 60-second interval refresh in <repo>/dashboard.html\"\n\nEach coder task should edit ONE section of ONE file. Use edit_file, not write_file for existing files.\n\nRespond with ONLY valid JSON:\n{\n \"shouldDelegate\": true/false,\n \"reason\": \"brief explanation\",\n \"tasks\": [\n { \"template\": \"coder|explorer|browser|analyst\", \"task\": \"specific focused instruction with exact file path and what to change\" }\n ]\n}\n\nRules:\n- Delegate if 2+ sub-tasks exist\n- Each task: self-contained, actionable, <50 lines of code\n- Max 6 sub-tasks\n- Include exact file paths in task descriptions\n- For file edits: specify WHICH section to change (e.g. \"add after the </style> tag\")`,\n },\n { role: 'user', content: message },\n ],\n maxTokens: 500,\n temperature: 0.1,\n });\n\n let jsonStr = response.content.trim();\n if (jsonStr.startsWith('```')) {\n jsonStr = jsonStr.replace(/^```(?:json)?\\n?/, '').replace(/\\n?```$/, '');\n }\n\n const parsed = JSON.parse(jsonStr) as DelegationPlan;\n\n // Validate\n if (!parsed.tasks || !Array.isArray(parsed.tasks)) {\n return { shouldDelegate: false, reason: 'Invalid delegation plan', tasks: [] };\n }\n\n // Cap at 4 tasks\n parsed.tasks = parsed.tasks.slice(0, 6);\n\n logger.info(COMPONENT, `Delegation analysis: ${parsed.shouldDelegate ? 'YES' : 'NO'} — ${parsed.reason} (${parsed.tasks.length} tasks)`);\n return parsed;\n } catch (err) {\n logger.warn(COMPONENT, `Delegation analysis failed: ${(err as Error).message}`);\n return { shouldDelegate: false, reason: 'Analysis failed', tasks: [] };\n }\n}\n\n/** Execute a delegation plan — runs independent tasks in parallel, dependent tasks sequentially */\nexport async function executeDelegationPlan(plan: DelegationPlan): Promise<OrchestratorResult> {\n const startTime = Date.now();\n const results: SubAgentResult[] = [];\n\n if (!plan.shouldDelegate || plan.tasks.length === 0) {\n return {\n content: 'No delegation needed.',\n subResults: [],\n durationMs: 0,\n };\n }\n\n logger.info(COMPONENT, `Executing delegation plan: ${plan.tasks.length} tasks`);\n\n // Group tasks: those with dependencies run after their deps, independent ones run in parallel\n const taskResults: Map<number, SubAgentResult> = new Map();\n\n // Find independent tasks (no dependsOn)\n const independent = plan.tasks.map((t, i) => ({ ...t, index: i }))\n .filter(t => !t.dependsOn || t.dependsOn.length === 0);\n\n const dependent = plan.tasks.map((t, i) => ({ ...t, index: i }))\n .filter(t => t.dependsOn && t.dependsOn.length > 0);\n\n // Execute independent tasks via Command Post (Paperclip pattern)\n const config = loadConfig();\n const cpEnabled = (config.commandPost as Record<string, unknown> | undefined)?.enabled;\n\n if (independent.length > 0) {\n const parallelResults = await Promise.all(\n independent.map(async (t) => {\n const template = SUB_AGENT_TEMPLATES[t.template] || SUB_AGENT_TEMPLATES.explorer;\n const agentName = template.name || t.template;\n\n // Create Command Post issue for tracking\n if (cpEnabled) {\n try {\n const issue = createIssue({\n title: t.task.slice(0, 80),\n description: t.task,\n priority: 'medium',\n createdByUser: 'orchestrator',\n });\n logger.info(COMPONENT, `[CP] Created issue ${issue.id} for ${agentName}: ${t.task.slice(0, 60)}`);\n updateIssue(issue.id, { status: 'in_progress' });\n // v6.5 — the Command Post issue is for TRACKING ONLY. The\n // previous code ALSO queueWakeup()'d the same task here, which\n // executed it a SECOND time asynchronously (double LLM cost +\n // duplicated side effects) on top of the synchronous\n // spawnSubAgent below whose result is what we actually use.\n // Removed the duplicate async dispatch; tracking stays via the issue.\n } catch (e) {\n logger.warn(COMPONENT, `[CP] Issue creation failed: ${(e as Error).message}`);\n }\n }\n\n // Execute synchronously — the single source of the result used for synthesis\n const result = await spawnSubAgent({\n name: agentName,\n task: t.task,\n tools: template.tools,\n systemPrompt: template.systemPrompt,\n persona: (template as { persona?: string }).persona,\n tier: (template as { tier?: ModelTier }).tier,\n });\n return { index: t.index, result };\n })\n );\n for (const { index, result } of parallelResults) {\n taskResults.set(index, result);\n results.push(result);\n }\n }\n\n // Execute dependent tasks sequentially\n for (const t of dependent) {\n // Inject prior results into the task context\n const priorContext = (t.dependsOn || [])\n .map(depIdx => {\n const depResult = taskResults.get(depIdx);\n return depResult ? `Previous result: ${depResult.content.slice(0, 500)}` : '';\n })\n .filter(Boolean)\n .join('\\n');\n\n const enrichedTask = priorContext\n ? `${t.task}\\n\\nContext from previous steps:\\n${priorContext}`\n : t.task;\n\n const template = SUB_AGENT_TEMPLATES[t.template] || SUB_AGENT_TEMPLATES.explorer;\n const result = await spawnSubAgent({\n name: template.name || t.template,\n task: enrichedTask,\n tools: template.tools,\n systemPrompt: template.systemPrompt,\n persona: (template as { persona?: string }).persona,\n tier: (template as { tier?: ModelTier }).tier,\n });\n taskResults.set(t.index, result);\n results.push(result);\n }\n\n // Synthesize results. v6.5 — iterate plan.tasks by original index and pull\n // the matching result from taskResults (keyed by the true task index). The\n // old code zipped the REORDERED `results` array (independents first, then\n // dependents) against plan.tasks by array position, mislabelling every\n // result whenever the plan mixed independent + dependent tasks.\n const synthesis = plan.tasks.map((task, i) => {\n const r = taskResults.get(i);\n if (!r) return '';\n const status = r.success ? '✅' : '❌';\n return `${status} **${task?.template || 'task'}**: ${r.content.slice(0, 500)}`;\n }).filter(Boolean).join('\\n\\n');\n\n const durationMs = Date.now() - startTime;\n logger.info(COMPONENT, `Delegation complete: ${results.filter(r => r.success).length}/${results.length} succeeded (${durationMs}ms)`);\n\n return {\n content: synthesis,\n subResults: results,\n durationMs,\n };\n}\n\n// ─── Task Queue Integration ────────────────────────────────────────────────\n\n/**\n * Work the shared task queue — claim and execute the next available task.\n * Pulls from goals, plans, and command post via the unified taskQueue facade.\n */\nexport async function executeFromQueue(agentId: string): Promise<SubAgentResult | null> {\n const claim = claimNextTask(agentId);\n if (!claim.success || !claim.task) {\n logger.debug(COMPONENT, `No tasks in queue for ${agentId}`);\n return null;\n }\n\n const task = claim.task;\n logger.info(COMPONENT, `Queue: ${agentId} executing \"${task.title}\" (${task.source}, priority ${task.priority})`);\n\n // Infer template from task source/description\n const template = inferQueueTemplate(task);\n const templateDef = SUB_AGENT_TEMPLATES[template] || SUB_AGENT_TEMPLATES.coder;\n\n try {\n const result = await spawnSubAgent({\n name: `Queue-${task.source}-${task.id.split(':').pop()}`,\n task: `${task.title}\\n\\n${task.description}`,\n tools: templateDef.tools,\n systemPrompt: templateDef.systemPrompt,\n tier: (templateDef as { tier?: ModelTier }).tier,\n });\n\n if (result.success) {\n completeQueuedTask(task.id, claim.checkoutRunId, result.content);\n } else {\n failQueuedTask(task.id, claim.checkoutRunId, result.content);\n }\n\n return result;\n } catch (err) {\n failQueuedTask(task.id, claim.checkoutRunId, (err as Error).message);\n throw err;\n }\n}\n\n/** Infer the best sub-agent template for a queued task */\nfunction inferQueueTemplate(task: QueuedTask): string {\n const text = `${task.title} ${task.description}`.toLowerCase();\n if (/\\b(write|create|build|code|implement|edit|fix|deploy)\\b/.test(text)) return 'coder';\n if (/\\b(research|search|find|discover|explore)\\b/.test(text)) return 'explorer';\n if (/\\b(analyze|report|summarize|compare|review)\\b/.test(text)) return 'analyst';\n if (/\\b(browse|navigate|login|click|form|page)\\b/.test(text)) return 'browser';\n return 'coder';\n}\n\n/**\n * Get a snapshot of the shared task queue for status reporting.\n */\nexport function getTaskQueueSnapshot() {\n return getQueueStatus();\n}\n\n// ─── Hierarchical Delegation ───────────────────────────────────────────────\n\n/**\n * Decompose a complex goal into a multi-level hierarchical plan and execute it.\n * Uses LLM-driven decomposition: goal → phases → tasks → subtasks (3 levels max).\n * Compound tasks recurse, simple tasks dispatch to sub-agents.\n */\nexport async function executeHierarchicalDelegation(\n goal: string,\n opts?: { maxDepth?: number; baseRounds?: number },\n): Promise<{ result: HierarchicalPlanResult; summary: string }> {\n const maxDepth = opts?.maxDepth ?? 3;\n const baseRounds = opts?.baseRounds ?? 15;\n\n logger.info(COMPONENT, `Hierarchical delegation: \"${goal.slice(0, 80)}...\" (maxDepth: ${maxDepth})`);\n\n // Phase 1: Decompose\n const plan = await decomposeHierarchically(goal, maxDepth);\n const taskCount = flattenPlan(plan).length;\n logger.info(COMPONENT, `Decomposed into ${taskCount} tasks across ${maxDepth} levels`);\n\n // Phase 2: Execute\n const result = await executeHierarchicalPlan(plan, 0, baseRounds);\n\n // Phase 3: Summarize\n const summary = summarizePlan(plan);\n logger.info(COMPONENT, `Hierarchical delegation complete: ${result.completedTasks}/${result.totalTasks} succeeded`);\n\n return { result, summary };\n}\n"],"mappings":"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","names":[]}
@@ -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\n// v6.0.2 — Advisory dedup + retention windows.\n//\n// Pre-v6.0.2, `enqueueAdvisory` blindly appended every pulse decision.\n// Since `decidePulse` is stable (same activity pattern → same advisory)\n// the file accumulated hundreds of duplicate entries — 308 in production\n// at the time this fix shipped, of which only ~10 were unique. The\n// SomaAdvisoryToast widget polls this file and surfaces \"new\" entries,\n// so duplicates re-spam the user with the same suggestion every 30s.\n//\n// Fix: before appending, check whether the same (action, rationale) pair\n// was filed within `ADVISORY_DEDUP_WINDOW_MS` (12h default). If yes,\n// silently skip. ALSO prune entries older than `ADVISORY_RETENTION_MS`\n// (7 days) on every write so the file doesn't grow unbounded.\n//\n// Override either window via env (TITAN_SOMA_DEDUP_WINDOW_MS /\n// TITAN_SOMA_ADVISORY_RETENTION_MS) for testing or aggressive tuning.\n\nconst DEFAULT_DEDUP_WINDOW_MS = 12 * 60 * 60 * 1000; // 12h\nconst DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7d\n\nfunction getDedupWindowMs(): number {\n const raw = process.env.TITAN_SOMA_DEDUP_WINDOW_MS;\n const n = raw ? Number(raw) : NaN;\n return Number.isFinite(n) && n >= 0 ? n : DEFAULT_DEDUP_WINDOW_MS;\n}\n\nfunction getRetentionMs(): number {\n const raw = process.env.TITAN_SOMA_ADVISORY_RETENTION_MS;\n const n = raw ? Number(raw) : NaN;\n return Number.isFinite(n) && n >= 0 ? n : DEFAULT_RETENTION_MS;\n}\n\ninterface AdvisoryRecord {\n at: string;\n action: string;\n rationale: string;\n confidence: number;\n payload?: Record<string, unknown>;\n}\n\nfunction parseAdvisoryFile(raw: string): AdvisoryRecord[] {\n if (!raw.trim()) return [];\n const out: AdvisoryRecord[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const rec = JSON.parse(trimmed) as AdvisoryRecord;\n if (rec && typeof rec.at === 'string' && typeof rec.action === 'string') out.push(rec);\n } catch { /* skip corrupt line — never let one bad row break dedup */ }\n }\n return out;\n}\n\n/** Normalize rationale so trivial variations (whitespace, capitalization,\n * trailing punctuation) collapse to the same dedup key. */\nfunction dedupKey(action: string, rationale: string): string {\n const r = rationale.toLowerCase().replace(/\\s+/g, ' ').replace(/[.!?]+\\s*$/, '').trim();\n return `${action}::${r}`;\n}\n\nexport function enqueueAdvisory(userId: string, decision: PulseDecision, now: number = Date.now()): void {\n if (decision.action === 'nothing') return;\n const path = advisoriesPath(userId);\n try { mkdirSync(join(path, '..'), { recursive: true }); } catch { /* exists */ }\n\n // Normalize optionals up-front so dedup + persistence work with\n // concrete values (PulseDecision.rationale/confidence are optional\n // on the type, but every advisory needs a stable form on disk).\n const rationale = (decision.rationale ?? '').trim() || decision.action;\n const confidence = typeof decision.confidence === 'number' && Number.isFinite(decision.confidence) ? decision.confidence : 0.5;\n\n const dedupWindow = getDedupWindowMs();\n const retention = getRetentionMs();\n const incomingKey = dedupKey(decision.action, rationale);\n\n try {\n const existing = existsSync(path) ? parseAdvisoryFile(readFileSync(path, 'utf-8')) : [];\n\n // Prune entries older than retention so the file stays bounded.\n const retentionCutoff = now - retention;\n const retained = existing.filter(r => {\n const t = new Date(r.at).getTime();\n return Number.isFinite(t) && t >= retentionCutoff;\n });\n\n // Dedup: same (action, normalized-rationale) within the dedup window → skip.\n const dedupCutoff = now - dedupWindow;\n const isDup = retained.some(r => {\n const t = new Date(r.at).getTime();\n if (!Number.isFinite(t) || t < dedupCutoff) return false;\n return dedupKey(r.action, r.rationale) === incomingKey;\n });\n if (isDup) {\n // Common case during a steady-state behaviour pattern. Don't\n // spam the agent log either — debug level only.\n logger.debug(COMPONENT, `Skipping duplicate advisory within ${Math.round(dedupWindow / 3600_000)}h window: ${decision.action} — ${rationale.slice(0, 60)}`);\n // Rewrite the pruned file IF we actually pruned anything, so\n // the retention sweep still happens even when we're deduping.\n if (retained.length !== existing.length) {\n const body = retained.map(r => JSON.stringify(r)).join('\\n') + (retained.length > 0 ? '\\n' : '');\n writeFileSync(path, body);\n }\n return;\n }\n\n // New advisory — append.\n const fresh: AdvisoryRecord = {\n at: new Date(now).toISOString(),\n action: decision.action,\n rationale,\n confidence,\n payload: decision.payload,\n };\n const final = [...retained, fresh];\n const body = final.map(r => JSON.stringify(r)).join('\\n') + '\\n';\n writeFileSync(path, body);\n if (retained.length !== existing.length) {\n logger.info(COMPONENT, `Pruned ${existing.length - retained.length} stale advisory entries (>${Math.round(retention / 86400_000)}d old)`);\n }\n } catch (err) {\n logger.warn(COMPONENT, `Failed to enqueue advisory: ${(err as Error).message}`);\n }\n}\n\n/** Read recent advisories. Used by the agent loop to surface Soma's\n * observations in the next chat turn. Returns up to `limit` most-recent. */\nexport function readRecentAdvisories(userId: string, limit = 5): Array<{\n at: string;\n action: string;\n rationale: string;\n confidence: number;\n payload?: Record<string, unknown>;\n}> {\n const path = advisoriesPath(userId);\n if (!existsSync(path)) return [];\n try {\n const lines = readFileSync(path, 'utf-8').trim().split('\\n').filter(Boolean);\n return lines.slice(-limit).map(l => JSON.parse(l)).reverse();\n } catch {\n return [];\n }\n}\n\n// ─── Daily proactive-widget gift (v6.0) ────────────────────────────\n//\n// Tony's spec: \"I want the TITAN agent to make a customized widget for\n// its user, from info it knows about the user, every day or so if it\n// wants to.\"\n//\n// Mechanism:\n// 1. Once a day (default — 22h interval, slight jitter), the agent\n// considers gifting the user a custom widget.\n// 2. Pulls the user's Soma profile (long-term observations + baseline\n// drives), recent patterns, and active Space context.\n// 3. Composes a /api/message internal prompt asking the agent to\n// \"design and pin a widget you think the user would appreciate\n// based on what you know about them — or skip if nothing comes to\n// mind.\" Conservative bias: the agent SHOULD skip when in doubt.\n// 4. The agent (real LLM) either calls create_widget itself or\n// politely declines. Either way the decision is logged to\n// `~/.titan/users/<userId>/soma-gifts.jsonl` so we can audit.\n//\n// This is the \"TITAN gifts you a widget on its own, occasionally\"\n// behavior — different from the 5-min advisory pulse which only files\n// advisories without calling the LLM.\n\nconst DAILY_GIFT_INTERVAL_MS = 22 * 60 * 60 * 1000; // 22h, lets it drift\nconst DAILY_GIFT_MIN_GAP_MS = 18 * 60 * 60 * 1000; // never less than 18h between gifts\n\nfunction giftsPath(userId: string): string {\n const env = process.env.TITAN_HOME;\n const home = env\n ? (env.startsWith('~/') ? join(homedir(), env.slice(2)) : env)\n : join(homedir(), '.titan');\n return join(home, 'users', userId, 'soma-gifts.jsonl');\n}\n\n/** Read the most recent gift attempt timestamp (epoch ms), or 0 if none. */\nfunction lastGiftAt(userId: string): number {\n const path = giftsPath(userId);\n if (!existsSync(path)) return 0;\n try {\n const lines = readFileSync(path, 'utf-8').trim().split('\\n').filter(Boolean);\n const last = lines[lines.length - 1];\n if (!last) return 0;\n const entry = JSON.parse(last) as { at?: string };\n return entry.at ? Date.parse(entry.at) : 0;\n } catch { return 0; }\n}\n\nfunction logGiftAttempt(userId: string, payload: Record<string, unknown>): void {\n const path = giftsPath(userId);\n try { mkdirSync(join(path, '..'), { recursive: true }); } catch { /* exists */ }\n const line = JSON.stringify({ at: new Date().toISOString(), ...payload });\n try {\n const prev = existsSync(path) ? readFileSync(path, 'utf-8') : '';\n writeFileSync(path, prev + line + '\\n');\n } catch (err) {\n logger.warn(COMPONENT, `gift log write failed: ${(err as Error).message}`);\n }\n}\n\n/**\n * Decide whether to gift a custom widget today. Pure function — no side\n * effects. Returns null when we should NOT gift right now, or an object\n * with a one-paragraph brief the LLM can use to design the widget.\n */\nexport interface GiftBrief {\n /** Short rationale shown to the user when the widget lands. */\n rationale: string;\n /** Compact context the LLM sees — recent observations + patterns. */\n context: string;\n}\n\nexport function decideDailyGift(userId = 'default-user', now = Date.now(), opts?: { force?: boolean }): GiftBrief | null {\n const force = opts?.force === true;\n if (!force) {\n const since = now - lastGiftAt(userId);\n if (since < DAILY_GIFT_MIN_GAP_MS) return null;\n }\n\n // Pull what we know about the user.\n const profile = readSomaProfile(userId);\n // v6.0.3 — When force=true (manual user trigger from the SOMA panel),\n // skip the \"learned >= 3 observations\" gate. The user explicitly asked\n // for a gift; Soma should at least try, even on day-one with a thin\n // profile. The agent prompt still lets the LLM decline if nothing\n // meaningful comes to mind.\n if (!force && profile.learnedAboutUser.length < 3) {\n return null;\n }\n // Don't gift when frustrated — same throttling as advisories.\n // Manual trigger overrides this too.\n if (!force && profile.baseline.frustration > 0.7) return null;\n\n const recent = profile.learnedAboutUser.slice(-10);\n const aggs = aggregatePatterns(userId, { windowDays: 7, topN: 5 });\n const activeSpace = getActiveSpace();\n\n const context = [\n `User profile (most recent 10 observations):`,\n ...recent.map(o => ` - ${o}`),\n ``,\n `Top patterns last 7 days:`,\n ...(aggs.length === 0\n ? [' (none yet — user is new or quiet)']\n : aggs.map(a => ` - ${a.signal} ×${a.count}`)),\n ``,\n activeSpace ? `Active Space: ${activeSpace.name} (${activeSpace.widgets.length} widget(s) pinned).` : 'No active Space.',\n ].join('\\n');\n\n return {\n rationale: `Soma noticed it's been ~24h since the last surprise. Generating a widget from what's known about the user.`,\n context,\n };\n}\n\n/**\n * Actually execute the daily gift — invokes the LLM via the agent's own\n * `processMessage` flow with a brief that asks the agent to either build\n * a widget or politely decline. The agent's create_widget call (if it\n * makes one) routes through the normal widgetEmitter side-channel.\n */\nexport async function tryDailyGift(userId = 'default-user', opts?: { force?: boolean }): Promise<{ attempted: boolean; reason: string }> {\n const brief = decideDailyGift(userId, Date.now(), opts);\n if (!brief) {\n return { attempted: false, reason: 'cooldown / not enough profile / soma blocked' };\n }\n logger.info(COMPONENT, `[DailyGift] attempting for ${userId}`);\n logGiftAttempt(userId, { phase: 'start' });\n\n const prompt = [\n `You have an opportunity to gift the user a custom widget right now.`,\n `This is a SPONTANEOUS, OPTIONAL gesture — not a response to a request.`,\n `Decline if nothing meaningful comes to mind. Better to skip than to ship something generic.`,\n ``,\n `What you know about THIS user:`,\n brief.context,\n ``,\n `If you decide to build, call \\`create_widget\\` ONCE with a name + source that genuinely reflects something specific to this user (a tracker for a thing they care about, a tool for a recurring task, a dashboard for a domain they work in). Title it something they'd recognize as personal.`,\n ``,\n `If you decline, just reply with one sentence explaining why (\"nothing strong enough today\"). Do NOT make excuses, do NOT promise a future gift.`,\n ].join('\\n');\n\n try {\n // Lazy-import to avoid circular deps at module load.\n const { processMessage } = await import('./agent.js');\n const result = await processMessage(prompt, 'soma-initiative', userId, {});\n const toolCalls = (result.toolsUsed || []).join(', ');\n const widgetMade = (result.toolsUsed || []).includes('create_widget');\n logGiftAttempt(userId, {\n phase: 'done',\n widgetMade,\n toolsUsed: toolCalls,\n content: (result.content || '').slice(0, 240),\n });\n logger.info(COMPONENT, `[DailyGift] ${widgetMade ? 'built a widget' : 'declined'} (tools: ${toolCalls})`);\n return { attempted: true, reason: widgetMade ? 'widget shipped' : 'agent declined' };\n } catch (err) {\n logGiftAttempt(userId, { phase: 'error', error: (err as Error).message });\n logger.warn(COMPONENT, `[DailyGift] error: ${(err as Error).message}`);\n return { attempted: false, reason: `error: ${(err as Error).message}` };\n }\n}\n\n// ─── Driver ────────────────────────────────────────────────────────\n\nlet timerHandle: NodeJS.Timeout | null = null;\nlet dailyGiftHandle: NodeJS.Timeout | null = null;\n\n/** Start the cron-driven pulse loop. Idempotent. */\nexport function startSomaInitiative(opts?: { intervalMs?: number; userId?: string; dailyGiftIntervalMs?: number }): void {\n if (timerHandle) return;\n const interval = opts?.intervalMs ?? PULSE_INTERVAL_MS;\n const userId = opts?.userId ?? 'default-user';\n\n timerHandle = setInterval(() => {\n try {\n // Idle detection is approximate — for now we treat every tick as\n // \"fully idle\" and let `decidePulse`'s conservative confidence\n // threshold do the throttling. Once gateway-side idle tracking\n // wires in (e.g. last /api/message timestamp), this can be\n // replaced with a real `idleMs`.\n executePulse(userId, Infinity);\n } catch (err) {\n logger.warn(COMPONENT, `Pulse error: ${(err as Error).message}`);\n }\n }, interval);\n if (timerHandle.unref) timerHandle.unref();\n logger.info(COMPONENT, `Started somaInitiative loop (interval=${Math.round(interval / 1000)}s, user=${userId}).`);\n\n // v6.0 — Daily proactive-widget gift loop. Fires once every\n // ~22h with jitter; the cooldown check inside decideDailyGift also\n // prevents over-firing if interval is short. Independent timer so\n // its long cadence doesn't gum up the 5-min advisory pulse.\n const giftInterval = opts?.dailyGiftIntervalMs ?? DAILY_GIFT_INTERVAL_MS;\n dailyGiftHandle = setInterval(() => {\n void tryDailyGift(userId);\n }, giftInterval);\n if (dailyGiftHandle.unref) dailyGiftHandle.unref();\n logger.info(COMPONENT, `Started daily-gift loop (interval=${Math.round(giftInterval / 3600_000)}h, user=${userId}).`);\n}\n\nexport function stopSomaInitiative(): void {\n if (timerHandle) { clearInterval(timerHandle); timerHandle = null; }\n if (dailyGiftHandle) { clearInterval(dailyGiftHandle); dailyGiftHandle = null; }\n}\n\nexport function __resetForTests(): void {\n stopSomaInitiative();\n}\n"],"mappings":"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","names":["body"]}
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-localuser/memory/titan-v6-living-canvas.md\n * (v6.0 step 11)\n * - src/storage/patterns.ts — input signals\n * - src/storage/somaProfile.ts — drive baselines\n * - src/agent/widgetEmitter.ts — output channel\n */\nimport logger from '../utils/logger.js';\nimport { deriveSuggestions, recordSignal, aggregatePatterns } from '../storage/patterns.js';\nimport { readSomaProfile } from '../storage/somaProfile.js';\nimport { getActiveSpace } from '../storage/spaces.js';\n\nconst COMPONENT = 'SomaInitiative';\n\n/** How often the loop fires when active. 5 minutes by Tony's spec. */\nconst PULSE_INTERVAL_MS = 5 * 60 * 1000;\n\n/** Time-of-day buckets the heuristics check against (24h, local). */\nfunction timeBucket(now = new Date()): string {\n const hour = now.getHours();\n const day = now.getDay();\n const dayKind = day === 0 || day === 6 ? 'weekend' : 'weekday';\n return `${dayKind}-${hour.toString().padStart(2, '0')}`;\n}\n\n/** Single pulse — pure function of inputs, no side effects yet. */\nexport interface PulseDecision {\n /** What the loop decided to do this pulse. */\n action: 'nothing' | 'pin-widget' | 'create-space' | 'add-cron' | 'note';\n /** Human-readable explanation (used in logs + the `Soma noticed…` chip). */\n rationale?: string;\n /** Confidence the suggestion is welcome (0..1). */\n confidence?: number;\n /** Optional payload for the action — varies by kind. */\n payload?: Record<string, unknown>;\n}\n\n/**\n * Decide what (if anything) Soma should do this pulse. Pure function — no\n * side effects. The actual emission happens in `executePulse`.\n *\n * Inputs:\n * - userId — whose patterns/profile to read\n * - idleMs — milliseconds since last user activity\n * - opts.minIdleMs — only act when truly idle (default 2 min)\n */\nexport function decidePulse(\n userId = 'default-user',\n idleMs = Infinity,\n opts?: { minIdleMs?: number },\n): PulseDecision {\n const minIdle = opts?.minIdleMs ?? 2 * 60 * 1000;\n if (idleMs < minIdle) {\n return { action: 'nothing', rationale: 'user is active' };\n }\n\n // Record the time bucket as a pattern signal so the loop's own activity\n // becomes part of the user's pattern history. This is harmless and\n // helps timing-based suggestions emerge over time.\n recordSignal(userId, `time:${timeBucket()}`);\n\n const suggestions = deriveSuggestions(userId, { windowDays: 14 });\n if (suggestions.length === 0) {\n return { action: 'nothing', rationale: 'no patterns above confidence threshold' };\n }\n\n // Pick the single highest-confidence suggestion. Future iterations could\n // surface multiple, but conservative-first is the right default.\n const best = suggestions.sort((a, b) => b.confidence - a.confidence)[0];\n\n // Don't suggest the same thing repeatedly — dedup on signal in pattern\n // history. We rely on the pattern aggregation cooldown (only suggests\n // when count >= 3 in window) as the primary throttle; here we layer a\n // small extra check using the active Space's frustration baseline.\n const profile = readSomaProfile(userId);\n if (profile.baseline.frustration > 0.7) {\n // User has been frustrated. Don't pile on with proactive suggestions\n // — they'll feel like noise. Wait for the frustration to subside.\n return { action: 'nothing', rationale: 'soma frustration high — holding suggestions' };\n }\n\n return {\n action: best.kind === 'pin-widget' ? 'pin-widget'\n : best.kind === 'create-space' ? 'create-space'\n : best.kind === 'add-cron' ? 'add-cron'\n : 'note',\n rationale: best.rationale,\n confidence: best.confidence,\n payload: { signal: best.signal, activeSpaceId: getActiveSpace()?.id },\n };\n}\n\n/**\n * Fire one pulse + execute its decision. Used both by the cron driver and\n * by tests to step the loop deterministically.\n *\n * Returns the decision for telemetry / test assertions.\n */\nexport function executePulse(userId = 'default-user', idleMs = Infinity): PulseDecision {\n const d = decidePulse(userId, idleMs);\n if (d.action === 'nothing') {\n // Don't log every \"nothing happened\" tick — too noisy.\n return d;\n }\n logger.info(COMPONENT, `[Pulse] action=${d.action} confidence=${d.confidence?.toFixed(2)} rationale=\"${d.rationale}\"`);\n\n // For now the loop emits a logged advisory rather than auto-creating\n // widgets. Auto-creation requires:\n // - the gateway SSE stream to be open (otherwise the widget event\n // fires into the void)\n // - the agent to author the actual widget code, which it can't do\n // from a non-LLM context\n //\n // The right next step (v6.0+) is to enqueue these advisories so the\n // user sees them in a \"Soma noticed…\" panel; the agent's NEXT chat\n // turn can then pick them up and act. That keeps the loop's\n // side-effects observable + reversible.\n enqueueAdvisory(userId, d);\n return d;\n}\n\n// ─── Advisory queue ─────────────────────────────────────────────────\n\nimport { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nfunction advisoriesPath(userId: string): string {\n const env = process.env.TITAN_HOME;\n const home = env\n ? (env.startsWith('~/') ? join(homedir(), env.slice(2)) : env)\n : join(homedir(), '.titan');\n return join(home, 'users', userId, 'soma-advisories.jsonl');\n}\n\n// v6.0.2 — Advisory dedup + retention windows.\n//\n// Pre-v6.0.2, `enqueueAdvisory` blindly appended every pulse decision.\n// Since `decidePulse` is stable (same activity pattern → same advisory)\n// the file accumulated hundreds of duplicate entries — 308 in production\n// at the time this fix shipped, of which only ~10 were unique. The\n// SomaAdvisoryToast widget polls this file and surfaces \"new\" entries,\n// so duplicates re-spam the user with the same suggestion every 30s.\n//\n// Fix: before appending, check whether the same (action, rationale) pair\n// was filed within `ADVISORY_DEDUP_WINDOW_MS` (12h default). If yes,\n// silently skip. ALSO prune entries older than `ADVISORY_RETENTION_MS`\n// (7 days) on every write so the file doesn't grow unbounded.\n//\n// Override either window via env (TITAN_SOMA_DEDUP_WINDOW_MS /\n// TITAN_SOMA_ADVISORY_RETENTION_MS) for testing or aggressive tuning.\n\nconst DEFAULT_DEDUP_WINDOW_MS = 12 * 60 * 60 * 1000; // 12h\nconst DEFAULT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7d\n\nfunction getDedupWindowMs(): number {\n const raw = process.env.TITAN_SOMA_DEDUP_WINDOW_MS;\n const n = raw ? Number(raw) : NaN;\n return Number.isFinite(n) && n >= 0 ? n : DEFAULT_DEDUP_WINDOW_MS;\n}\n\nfunction getRetentionMs(): number {\n const raw = process.env.TITAN_SOMA_ADVISORY_RETENTION_MS;\n const n = raw ? Number(raw) : NaN;\n return Number.isFinite(n) && n >= 0 ? n : DEFAULT_RETENTION_MS;\n}\n\ninterface AdvisoryRecord {\n at: string;\n action: string;\n rationale: string;\n confidence: number;\n payload?: Record<string, unknown>;\n}\n\nfunction parseAdvisoryFile(raw: string): AdvisoryRecord[] {\n if (!raw.trim()) return [];\n const out: AdvisoryRecord[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const rec = JSON.parse(trimmed) as AdvisoryRecord;\n if (rec && typeof rec.at === 'string' && typeof rec.action === 'string') out.push(rec);\n } catch { /* skip corrupt line — never let one bad row break dedup */ }\n }\n return out;\n}\n\n/** Normalize rationale so trivial variations (whitespace, capitalization,\n * trailing punctuation) collapse to the same dedup key. */\nfunction dedupKey(action: string, rationale: string): string {\n const r = rationale.toLowerCase().replace(/\\s+/g, ' ').replace(/[.!?]+\\s*$/, '').trim();\n return `${action}::${r}`;\n}\n\nexport function enqueueAdvisory(userId: string, decision: PulseDecision, now: number = Date.now()): void {\n if (decision.action === 'nothing') return;\n const path = advisoriesPath(userId);\n try { mkdirSync(join(path, '..'), { recursive: true }); } catch { /* exists */ }\n\n // Normalize optionals up-front so dedup + persistence work with\n // concrete values (PulseDecision.rationale/confidence are optional\n // on the type, but every advisory needs a stable form on disk).\n const rationale = (decision.rationale ?? '').trim() || decision.action;\n const confidence = typeof decision.confidence === 'number' && Number.isFinite(decision.confidence) ? decision.confidence : 0.5;\n\n const dedupWindow = getDedupWindowMs();\n const retention = getRetentionMs();\n const incomingKey = dedupKey(decision.action, rationale);\n\n try {\n const existing = existsSync(path) ? parseAdvisoryFile(readFileSync(path, 'utf-8')) : [];\n\n // Prune entries older than retention so the file stays bounded.\n const retentionCutoff = now - retention;\n const retained = existing.filter(r => {\n const t = new Date(r.at).getTime();\n return Number.isFinite(t) && t >= retentionCutoff;\n });\n\n // Dedup: same (action, normalized-rationale) within the dedup window → skip.\n const dedupCutoff = now - dedupWindow;\n const isDup = retained.some(r => {\n const t = new Date(r.at).getTime();\n if (!Number.isFinite(t) || t < dedupCutoff) return false;\n return dedupKey(r.action, r.rationale) === incomingKey;\n });\n if (isDup) {\n // Common case during a steady-state behaviour pattern. Don't\n // spam the agent log either — debug level only.\n logger.debug(COMPONENT, `Skipping duplicate advisory within ${Math.round(dedupWindow / 3600_000)}h window: ${decision.action} — ${rationale.slice(0, 60)}`);\n // Rewrite the pruned file IF we actually pruned anything, so\n // the retention sweep still happens even when we're deduping.\n if (retained.length !== existing.length) {\n const body = retained.map(r => JSON.stringify(r)).join('\\n') + (retained.length > 0 ? '\\n' : '');\n writeFileSync(path, body);\n }\n return;\n }\n\n // New advisory — append.\n const fresh: AdvisoryRecord = {\n at: new Date(now).toISOString(),\n action: decision.action,\n rationale,\n confidence,\n payload: decision.payload,\n };\n const final = [...retained, fresh];\n const body = final.map(r => JSON.stringify(r)).join('\\n') + '\\n';\n writeFileSync(path, body);\n if (retained.length !== existing.length) {\n logger.info(COMPONENT, `Pruned ${existing.length - retained.length} stale advisory entries (>${Math.round(retention / 86400_000)}d old)`);\n }\n } catch (err) {\n logger.warn(COMPONENT, `Failed to enqueue advisory: ${(err as Error).message}`);\n }\n}\n\n/** Read recent advisories. Used by the agent loop to surface Soma's\n * observations in the next chat turn. Returns up to `limit` most-recent. */\nexport function readRecentAdvisories(userId: string, limit = 5): Array<{\n at: string;\n action: string;\n rationale: string;\n confidence: number;\n payload?: Record<string, unknown>;\n}> {\n const path = advisoriesPath(userId);\n if (!existsSync(path)) return [];\n try {\n const lines = readFileSync(path, 'utf-8').trim().split('\\n').filter(Boolean);\n return lines.slice(-limit).map(l => JSON.parse(l)).reverse();\n } catch {\n return [];\n }\n}\n\n// ─── Daily proactive-widget gift (v6.0) ────────────────────────────\n//\n// Tony's spec: \"I want the TITAN agent to make a customized widget for\n// its user, from info it knows about the user, every day or so if it\n// wants to.\"\n//\n// Mechanism:\n// 1. Once a day (default — 22h interval, slight jitter), the agent\n// considers gifting the user a custom widget.\n// 2. Pulls the user's Soma profile (long-term observations + baseline\n// drives), recent patterns, and active Space context.\n// 3. Composes a /api/message internal prompt asking the agent to\n// \"design and pin a widget you think the user would appreciate\n// based on what you know about them — or skip if nothing comes to\n// mind.\" Conservative bias: the agent SHOULD skip when in doubt.\n// 4. The agent (real LLM) either calls create_widget itself or\n// politely declines. Either way the decision is logged to\n// `~/.titan/users/<userId>/soma-gifts.jsonl` so we can audit.\n//\n// This is the \"TITAN gifts you a widget on its own, occasionally\"\n// behavior — different from the 5-min advisory pulse which only files\n// advisories without calling the LLM.\n\nconst DAILY_GIFT_INTERVAL_MS = 22 * 60 * 60 * 1000; // 22h, lets it drift\nconst DAILY_GIFT_MIN_GAP_MS = 18 * 60 * 60 * 1000; // never less than 18h between gifts\n\nfunction giftsPath(userId: string): string {\n const env = process.env.TITAN_HOME;\n const home = env\n ? (env.startsWith('~/') ? join(homedir(), env.slice(2)) : env)\n : join(homedir(), '.titan');\n return join(home, 'users', userId, 'soma-gifts.jsonl');\n}\n\n/** Read the most recent gift attempt timestamp (epoch ms), or 0 if none. */\nfunction lastGiftAt(userId: string): number {\n const path = giftsPath(userId);\n if (!existsSync(path)) return 0;\n try {\n const lines = readFileSync(path, 'utf-8').trim().split('\\n').filter(Boolean);\n const last = lines[lines.length - 1];\n if (!last) return 0;\n const entry = JSON.parse(last) as { at?: string };\n return entry.at ? Date.parse(entry.at) : 0;\n } catch { return 0; }\n}\n\nfunction logGiftAttempt(userId: string, payload: Record<string, unknown>): void {\n const path = giftsPath(userId);\n try { mkdirSync(join(path, '..'), { recursive: true }); } catch { /* exists */ }\n const line = JSON.stringify({ at: new Date().toISOString(), ...payload });\n try {\n const prev = existsSync(path) ? readFileSync(path, 'utf-8') : '';\n writeFileSync(path, prev + line + '\\n');\n } catch (err) {\n logger.warn(COMPONENT, `gift log write failed: ${(err as Error).message}`);\n }\n}\n\n/**\n * Decide whether to gift a custom widget today. Pure function — no side\n * effects. Returns null when we should NOT gift right now, or an object\n * with a one-paragraph brief the LLM can use to design the widget.\n */\nexport interface GiftBrief {\n /** Short rationale shown to the user when the widget lands. */\n rationale: string;\n /** Compact context the LLM sees — recent observations + patterns. */\n context: string;\n}\n\nexport function decideDailyGift(userId = 'default-user', now = Date.now(), opts?: { force?: boolean }): GiftBrief | null {\n const force = opts?.force === true;\n if (!force) {\n const since = now - lastGiftAt(userId);\n if (since < DAILY_GIFT_MIN_GAP_MS) return null;\n }\n\n // Pull what we know about the user.\n const profile = readSomaProfile(userId);\n // v6.0.3 — When force=true (manual user trigger from the SOMA panel),\n // skip the \"learned >= 3 observations\" gate. The user explicitly asked\n // for a gift; Soma should at least try, even on day-one with a thin\n // profile. The agent prompt still lets the LLM decline if nothing\n // meaningful comes to mind.\n if (!force && profile.learnedAboutUser.length < 3) {\n return null;\n }\n // Don't gift when frustrated — same throttling as advisories.\n // Manual trigger overrides this too.\n if (!force && profile.baseline.frustration > 0.7) return null;\n\n const recent = profile.learnedAboutUser.slice(-10);\n const aggs = aggregatePatterns(userId, { windowDays: 7, topN: 5 });\n const activeSpace = getActiveSpace();\n\n const context = [\n `User profile (most recent 10 observations):`,\n ...recent.map(o => ` - ${o}`),\n ``,\n `Top patterns last 7 days:`,\n ...(aggs.length === 0\n ? [' (none yet — user is new or quiet)']\n : aggs.map(a => ` - ${a.signal} ×${a.count}`)),\n ``,\n activeSpace ? `Active Space: ${activeSpace.name} (${activeSpace.widgets.length} widget(s) pinned).` : 'No active Space.',\n ].join('\\n');\n\n return {\n rationale: `Soma noticed it's been ~24h since the last surprise. Generating a widget from what's known about the user.`,\n context,\n };\n}\n\n/**\n * Actually execute the daily gift — invokes the LLM via the agent's own\n * `processMessage` flow with a brief that asks the agent to either build\n * a widget or politely decline. The agent's create_widget call (if it\n * makes one) routes through the normal widgetEmitter side-channel.\n */\nexport async function tryDailyGift(userId = 'default-user', opts?: { force?: boolean }): Promise<{ attempted: boolean; reason: string }> {\n const brief = decideDailyGift(userId, Date.now(), opts);\n if (!brief) {\n return { attempted: false, reason: 'cooldown / not enough profile / soma blocked' };\n }\n logger.info(COMPONENT, `[DailyGift] attempting for ${userId}`);\n logGiftAttempt(userId, { phase: 'start' });\n\n const prompt = [\n `You have an opportunity to gift the user a custom widget right now.`,\n `This is a SPONTANEOUS, OPTIONAL gesture — not a response to a request.`,\n `Decline if nothing meaningful comes to mind. Better to skip than to ship something generic.`,\n ``,\n `What you know about THIS user:`,\n brief.context,\n ``,\n `If you decide to build, call \\`create_widget\\` ONCE with a name + source that genuinely reflects something specific to this user (a tracker for a thing they care about, a tool for a recurring task, a dashboard for a domain they work in). Title it something they'd recognize as personal.`,\n ``,\n `If you decline, just reply with one sentence explaining why (\"nothing strong enough today\"). Do NOT make excuses, do NOT promise a future gift.`,\n ].join('\\n');\n\n try {\n // Lazy-import to avoid circular deps at module load.\n const { processMessage } = await import('./agent.js');\n const result = await processMessage(prompt, 'soma-initiative', userId, {});\n const toolCalls = (result.toolsUsed || []).join(', ');\n const widgetMade = (result.toolsUsed || []).includes('create_widget');\n logGiftAttempt(userId, {\n phase: 'done',\n widgetMade,\n toolsUsed: toolCalls,\n content: (result.content || '').slice(0, 240),\n });\n logger.info(COMPONENT, `[DailyGift] ${widgetMade ? 'built a widget' : 'declined'} (tools: ${toolCalls})`);\n return { attempted: true, reason: widgetMade ? 'widget shipped' : 'agent declined' };\n } catch (err) {\n logGiftAttempt(userId, { phase: 'error', error: (err as Error).message });\n logger.warn(COMPONENT, `[DailyGift] error: ${(err as Error).message}`);\n return { attempted: false, reason: `error: ${(err as Error).message}` };\n }\n}\n\n// ─── Driver ────────────────────────────────────────────────────────\n\nlet timerHandle: NodeJS.Timeout | null = null;\nlet dailyGiftHandle: NodeJS.Timeout | null = null;\n\n/** Start the cron-driven pulse loop. Idempotent. */\nexport function startSomaInitiative(opts?: { intervalMs?: number; userId?: string; dailyGiftIntervalMs?: number }): void {\n if (timerHandle) return;\n const interval = opts?.intervalMs ?? PULSE_INTERVAL_MS;\n const userId = opts?.userId ?? 'default-user';\n\n timerHandle = setInterval(() => {\n try {\n // Idle detection is approximate — for now we treat every tick as\n // \"fully idle\" and let `decidePulse`'s conservative confidence\n // threshold do the throttling. Once gateway-side idle tracking\n // wires in (e.g. last /api/message timestamp), this can be\n // replaced with a real `idleMs`.\n executePulse(userId, Infinity);\n } catch (err) {\n logger.warn(COMPONENT, `Pulse error: ${(err as Error).message}`);\n }\n }, interval);\n if (timerHandle.unref) timerHandle.unref();\n logger.info(COMPONENT, `Started somaInitiative loop (interval=${Math.round(interval / 1000)}s, user=${userId}).`);\n\n // v6.0 — Daily proactive-widget gift loop. Fires once every\n // ~22h with jitter; the cooldown check inside decideDailyGift also\n // prevents over-firing if interval is short. Independent timer so\n // its long cadence doesn't gum up the 5-min advisory pulse.\n const giftInterval = opts?.dailyGiftIntervalMs ?? DAILY_GIFT_INTERVAL_MS;\n dailyGiftHandle = setInterval(() => {\n void tryDailyGift(userId);\n }, giftInterval);\n if (dailyGiftHandle.unref) dailyGiftHandle.unref();\n logger.info(COMPONENT, `Started daily-gift loop (interval=${Math.round(giftInterval / 3600_000)}h, user=${userId}).`);\n}\n\nexport function stopSomaInitiative(): void {\n if (timerHandle) { clearInterval(timerHandle); timerHandle = null; }\n if (dailyGiftHandle) { clearInterval(dailyGiftHandle); dailyGiftHandle = null; }\n}\n\nexport function __resetForTests(): void {\n stopSomaInitiative();\n}\n"],"mappings":"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","names":["body"]}
@@ -778,11 +778,46 @@ function applyOutputCaps(results) {
778
778
  return { ...r, content: capped.content };
779
779
  });
780
780
  }
781
+ async function toolWouldGate(name) {
782
+ try {
783
+ if (classifyToolCall(name).decision === "gate") return true;
784
+ } catch {
785
+ }
786
+ try {
787
+ const { requiresApproval } = await import("../skills/builtin/approval_gates.js");
788
+ return requiresApproval(name);
789
+ } catch {
790
+ }
791
+ return false;
792
+ }
781
793
  async function executeTools(toolCalls, channel) {
782
794
  if (toolCalls.length <= 1) {
783
795
  const results = await Promise.all(toolCalls.map((tc) => executeTool(tc, channel)));
784
796
  return applyOutputCaps(results);
785
797
  }
798
+ const maybeGated = (await Promise.all(
799
+ toolCalls.map((tc) => toolWouldGate(tc.function.name))
800
+ )).some(Boolean);
801
+ if (maybeGated) {
802
+ const gatedResults = [];
803
+ let paused = false;
804
+ for (const tc of toolCalls) {
805
+ if (paused) {
806
+ gatedResults.push({
807
+ toolCallId: tc.id,
808
+ name: tc.function.name,
809
+ content: "Deferred: held until the human resolves the pending approval for a tool requested in the same turn. Re-issue this call after approval.",
810
+ success: false,
811
+ durationMs: 0
812
+ });
813
+ continue;
814
+ }
815
+ const r = await executeTool(tc, channel);
816
+ gatedResults.push(r);
817
+ if (r.approvalPending) paused = true;
818
+ }
819
+ return applyOutputCaps(gatedResults);
820
+ }
786
821
  const parallelCalls = toolCalls.map((tc) => {
787
822
  let args = {};
788
823
  try {