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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/agent/toolRunner.ts"],"sourcesContent":["/**\n * TITAN — Tool Runner\n * Executes tool calls from the LLM with sandboxing, timeouts, and result formatting.\n */\nimport type { ToolCall, ToolDefinition } from '../providers/base.js';\nimport {\n type ToolContract,\n ToolValidationError,\n validateToolCall,\n formatValidationError,\n} from './toolContract.js';\nimport { classifyToolCall, formatBreadcrumb } from './autoModeClassifier.js';\nimport { appendFileSync, readFileSync, existsSync } from 'fs';\nimport { TELEMETRY_EVENTS_PATH } from '../utils/constants.js';\nimport { executeToolsParallel } from './parallelTools.js';\nimport { capToolOutput } from './toolOutputCap.js';\nimport { runPreTool, runPostTool } from '../plugins/contextEngine.js';\nimport type { ContextEnginePlugin } from '../plugins/contextEngine.js';\nimport { run as runInIsolatedWorktree } from './worktreeExecutor.js';\n\n/** Tool hook plugins — set during agent initialization */\nlet toolHookPlugins: ContextEnginePlugin[] = [];\nexport function setToolHookPlugins(plugins: ContextEnginePlugin[]): void {\n toolHookPlugins = plugins;\n}\nimport logger from '../utils/logger.js';\nimport { loadConfig } from '../config/config.js';\nimport { checkAutonomy } from './autonomy.js';\nimport { isToolSkillEnabled } from '../skills/registry.js';\nimport { redactSecrets } from '../security/secretGuard.js';\nimport { scanAndRedactPII, fullExfilScan } from '../security/exfilScan.js';\nimport { scanCommand, scanURL } from '../security/preExecScan.js';\nimport { runPreToolShellHooks, runPostToolShellHooks } from '../hooks/shellHooks.js';\nimport { createCheckpoint } from '../checkpoint/manager.js';\nimport { mintActionId } from '../receipts/mint.js';\nimport { writeReceipt } from '../receipts/store.js';\n\n/** Compute a lightweight unified diff between old and new file content */\nfunction computeUnifiedDiff(filePath: string, oldContent: string, newContent: string): string {\n if (oldContent === newContent) return `// No changes to ${filePath}`;\n const oldLines = oldContent.split('\\n');\n const newLines = newContent.split('\\n');\n const header = `--- ${filePath}\\n+++ ${filePath}`;\n const hunks: string[] = [];\n let i = 0, j = 0;\n while (i < oldLines.length || j < newLines.length) {\n if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {\n i++; j++; continue;\n }\n const startI = i, startJ = j;\n const removed: string[] = [];\n const added: string[] = [];\n while (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {\n removed.push(oldLines[i++]);\n }\n while (j < newLines.length && (i >= oldLines.length || oldLines[i] !== newLines[j])) {\n added.push(newLines[j++]);\n }\n if (removed.length || added.length) {\n const ctxBefore = oldLines.slice(Math.max(0, startI - 2), startI);\n const ctxAfter = oldLines.slice(i, Math.min(oldLines.length, i + 2));\n hunks.push([\n ...ctxBefore.map(l => ` ${l}`),\n ...removed.map(l => `-${l}`),\n ...added.map(l => `+${l}`),\n ...ctxAfter.map(l => ` ${l}`),\n ].join('\\n'));\n }\n }\n const body = hunks.join('\\n---\\n');\n return `${header}\\n${body}`;\n}\nimport { getCachedToolResult, cacheToolResult } from './trajectoryCompressor.js';\nimport { classifyProviderError, FailoverReason } from '../providers/errorTaxonomy.js';\nimport { snapshotBeforeWrite } from './shadowGit.js';\nimport { captureWrite, shouldCapture } from './selfProposals.js';\nimport { getSessionGoal } from './autonomyContext.js';\n\nconst COMPONENT = 'ToolRunner';\n\n/**\n * Tools whose entire purpose is to RETURN a data: URL for the LLM\n * to embed in downstream output. These bypass `sanitizeBase64` (which\n * would strip the very thing they were called to produce) and the\n * 30KB smart-truncation (which would chop the data URL mid-base64\n * making it unembeddable).\n *\n * v6.1.0-alpha.55 added `download_image` here. Tony spotted the bug\n * the way only Tony would: \"Why doesn't TITAN download the images\n * itself?\" → we added the tool in alpha.53 → ToolRunner sanitizer\n * stripped its output → Writer never saw the data URL → hotlinked\n * external URLs instead → alpha.52 placeholder caught them →\n * appearance of \"alpha.53 doesn't work.\" It worked perfectly. We\n * were just throwing away its output two lines later.\n *\n * If you add a new tool here, also update the 30KB truncation\n * exemption below (line ~685) since both share this list.\n */\nconst DATA_URL_PRODUCING_TOOLS = new Set([\n 'download_image',\n]);\n\n/**\n * G1: Sanitize base64 image data from tool results (OpenClaw pattern).\n * Prevents token explosion when vision/screenshot tools return raw base64.\n * Replaces data URIs with a compact placeholder showing byte count.\n *\n * v6.1.0-alpha.55 — takes optional `toolName` so we can EXEMPT the\n * tools whose entire output IS a data URL (see\n * DATA_URL_PRODUCING_TOOLS above). Default behavior unchanged for\n * every other tool.\n */\nfunction sanitizeBase64(content: string, toolName?: string): string {\n if (toolName && DATA_URL_PRODUCING_TOOLS.has(toolName)) return content;\n return content.replace(\n /data:image\\/[^;]+;base64,[A-Za-z0-9+/=]{100,}/g,\n (match) => {\n const bytes = Math.ceil((match.length - match.indexOf(',') - 1) * 0.75);\n return `[image: ${(bytes / 1024).toFixed(1)}KB omitted]`;\n },\n );\n}\n\n/**\n * v6.1.0-alpha.55 — exported alias for tests + future callers. The\n * Set is the source of truth for \"tools that intentionally return a\n * data: URL for the LLM to embed downstream.\"\n */\nexport function isDataUrlProducingTool(toolName: string): boolean {\n return DATA_URL_PRODUCING_TOOLS.has(toolName);\n}\n\n/** Error classification for retry decisions */\nexport type ErrorClass = 'transient' | 'permanent' | 'timeout' | 'rate_limit';\n\n/** Classify an error to determine if retry is appropriate.\n * Delegates to the centralized error taxonomy, then maps back to ErrorClass\n * for backward compatibility with tool execution retry logic.\n */\nexport function classifyError(error: Error, _toolName: string): ErrorClass {\n const classified = classifyProviderError(error);\n switch (classified.reason) {\n case FailoverReason.TIMEOUT:\n return 'timeout';\n case FailoverReason.RATE_LIMIT:\n return 'rate_limit';\n case FailoverReason.SERVER_ERROR:\n case FailoverReason.NETWORK_ERROR:\n case FailoverReason.OVERLOADED:\n case FailoverReason.EMPTY_RESPONSE:\n return 'transient';\n default:\n return classified.retryable ? 'transient' : 'permanent';\n }\n}\n\n/** Tool execution result */\nexport interface ToolResult {\n toolCallId: string;\n name: string;\n content: string;\n success: boolean;\n durationMs: number;\n /** Number of retry attempts made (0 = first try succeeded/failed) */\n retryCount?: number;\n /** Error classification if the tool failed */\n errorClass?: ErrorClass;\n /** Inline unified diff for file-modifying tools (write_file, edit_file, apply_patch) */\n diff?: string;\n /** v5.0: True when the tool is paused waiting for human approval */\n approvalPending?: boolean;\n /** v5.0: Approval request ID when approvalPending is true */\n approvalRequestId?: string;\n}\n\n/** A registered tool handler */\nexport interface ToolHandler {\n name: string;\n description: string;\n parameters: Record<string, unknown>;\n execute: (args: Record<string, unknown>, context?: { signal?: AbortSignal }) => Promise<string>;\n /**\n * beta.15 (Phase D.3) — optional declarative contract. When set,\n * the tool runner validates incoming args against\n * `contract.input` BEFORE calling execute() and rejects malformed\n * calls with a clear, LLM-friendly error message. Skills without\n * a contract continue to work unchanged — opt-in upgrade.\n */\n contract?: ToolContract;\n}\n\n/** Global tool registry */\nconst toolRegistry: Map<string, ToolHandler> = new Map();\n\n/** Register a tool */\nexport function registerTool(handler: ToolHandler): void {\n toolRegistry.set(handler.name, handler);\n logger.debug(COMPONENT, `Registered tool: ${handler.name}`);\n}\n\n/** Unregister a tool */\nexport function unregisterTool(name: string): void {\n toolRegistry.delete(name);\n}\n\n/** Get all registered tools */\nexport function getRegisteredTools(): ToolHandler[] {\n return Array.from(toolRegistry.values());\n}\n\n/** Convert registered tools to LLM tool definitions */\nexport function getToolDefinitions(): ToolDefinition[] {\n const config = loadConfig();\n const allowed = new Set(config.security.allowedTools);\n const denied = new Set(config.security.deniedTools);\n\n return Array.from(toolRegistry.values())\n .filter((tool) => {\n if (denied.has(tool.name)) return false;\n if (allowed.size > 0 && !allowed.has(tool.name)) return false;\n if (!isToolSkillEnabled(tool.name)) return false;\n return true;\n })\n .map((tool) => ({\n type: 'function' as const,\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters,\n },\n }));\n}\n\n/** Execute a single tool call */\nfunction safeReceiptText(text: string, maxChars: number): string {\n return scanAndRedactPII(redactSecrets(text)).replace(/\\s+/g, ' ').trim().slice(0, maxChars);\n}\n\nfunction summarizeToolArguments(rawArgs: string | undefined): { summary: string; argKeys?: string[] } {\n if (!rawArgs?.trim()) return { summary: 'args=none' };\n try {\n const parsed = JSON.parse(rawArgs) as unknown;\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n const allKeys = Object.keys(parsed).sort();\n const visibleKeys = allKeys\n .slice(0, 8)\n .map((key) => safeReceiptText(key, 32))\n .filter(Boolean);\n if (visibleKeys.length === 0) return { summary: 'args=empty', argKeys: [] };\n const suffix = allKeys.length > visibleKeys.length ? ` +${allKeys.length - visibleKeys.length}` : '';\n return { summary: `args=${visibleKeys.join(',')}${suffix}`, argKeys: visibleKeys };\n }\n return { summary: `args=${Array.isArray(parsed) ? 'array' : typeof parsed}` };\n } catch {\n return { summary: 'args=invalid-json' };\n }\n}\n\nfunction writeToolCallReceipt(params: {\n actionId: string;\n toolName: string;\n status: 'ok' | 'fail';\n channel?: string;\n durationMs: number;\n argSummary: string;\n argKeys?: string[];\n error?: string;\n}): void {\n try {\n const safeError = params.error ? safeReceiptText(params.error, 200) : undefined;\n writeReceipt({\n action_id: params.actionId,\n kind: 'tool_call',\n summary: safeReceiptText(`${params.toolName} ${params.argSummary}`, 200),\n status: params.status,\n channel: params.channel,\n duration_ms: params.durationMs,\n meta: params.status === 'fail'\n ? { ...(params.argKeys ? { arg_keys: params.argKeys } : {}), ...(safeError ? { error: safeError } : {}) }\n : (params.argKeys ? { arg_keys: params.argKeys } : undefined),\n });\n } catch {\n /* receipt writes are best-effort and must not affect tool behavior */\n }\n}\n\nexport async function executeTool(toolCall: ToolCall, channel?: string): Promise<ToolResult> {\n const _aid = mintActionId();\n const _t0 = Date.now();\n const argInfo = summarizeToolArguments(toolCall.function.arguments);\n try {\n const result = await executeToolInner(toolCall, channel);\n writeToolCallReceipt({\n actionId: _aid,\n toolName: toolCall.function.name,\n status: result.success ? 'ok' : 'fail',\n channel,\n durationMs: Date.now() - _t0,\n argSummary: argInfo.summary,\n argKeys: argInfo.argKeys,\n error: result.success ? undefined : result.content,\n });\n return result;\n } catch (err) {\n writeToolCallReceipt({\n actionId: _aid,\n toolName: toolCall.function.name,\n status: 'fail',\n channel,\n durationMs: Date.now() - _t0,\n argSummary: argInfo.summary,\n argKeys: argInfo.argKeys,\n error: (err as Error).message,\n });\n throw err;\n }\n}\n\nasync function executeToolInner(toolCall: ToolCall, channel?: string): Promise<ToolResult> {\n const config = loadConfig();\n const startTime = Date.now();\n const handler = toolRegistry.get(toolCall.function.name);\n\n if (!handler) {\n // LangGraph pattern: tell the LLM which tools actually exist so it can self-correct\n const available = Array.from(toolRegistry.keys()).sort();\n const suggestions = available.filter(t => {\n const name = toolCall.function.name.toLowerCase();\n return t.toLowerCase().includes(name.slice(0, 4)) || name.includes(t.slice(0, 4));\n }).slice(0, 5);\n const hint = suggestions.length > 0\n ? `\\nDid you mean: ${suggestions.join(', ')}?`\n : `\\nAvailable tools include: ${available.slice(0, 20).join(', ')}${available.length > 20 ? ` (and ${available.length - 20} more)` : ''}`;\n return {\n toolCallId: toolCall.id,\n name: toolCall.function.name,\n content: `Error: \"${toolCall.function.name}\" is not a valid tool.${hint}\\nPlease use one of the available tools.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n\n // Check permissions\n if (config.security.deniedTools.includes(handler.name)) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Tool \"${handler.name}\" is denied by security policy`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n\n // Check if parent skill is enabled\n if (!isToolSkillEnabled(handler.name)) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Tool \"${handler.name}\" is disabled — its parent skill is turned off. Enable it in Mission Control.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n\n // Parse arguments\n let args: Record<string, unknown> = {};\n try {\n args = JSON.parse(toolCall.function.arguments);\n } catch (parseErr) {\n logger.warn('ToolRunner', `Malformed JSON args for ${handler.name}: ${(parseErr as Error).message} — raw: ${(toolCall.function.arguments || '').slice(0, 200)}`);\n // Try to salvage: if it looks like a truncated JSON, extract what we can\n const salvageMatch = (toolCall.function.arguments || '').match(/\\{[\\s\\S]*/);\n if (salvageMatch) {\n try {\n // Attempt to close the JSON and parse\n const fixed = salvageMatch[0].replace(/,?\\s*$/, '}');\n args = JSON.parse(fixed);\n logger.info('ToolRunner', `Salvaged partial JSON args for ${handler.name}`);\n } catch {\n // A5: Return error instead of executing with empty args (LangGraph pattern)\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Could not parse arguments for \"${handler.name}\". Raw: ${(toolCall.function.arguments || '').slice(0, 200)}. Please provide valid JSON arguments.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n }\n }\n\n // beta.15 (Phase D.3) — Zod contract validation. Runs BEFORE the\n // legacy required-fields check below so contract-bearing skills\n // get a precise error message. Skills without a contract fall\n // through to the legacy check unchanged.\n //\n // Contracts can be attached two ways:\n // 1. Directly on the ToolHandler (handler.contract — for new skills\n // that want a self-contained file).\n // 2. Registered in the global contract registry (for retrofitting\n // contracts onto existing skills without touching their files).\n // We check both, in that order.\n let contractToUse = handler.contract;\n if (!contractToUse) {\n try {\n const { getToolContract } = await import('./toolContract.js');\n contractToUse = getToolContract(handler.name);\n } catch { /* contract registry unavailable — skip */ }\n }\n if (contractToUse) {\n try {\n // Replace `args` with the parsed (possibly coerced) shape\n // so execute() receives Zod-cleaned arguments — string\n // numerics become numbers, undefined fields get defaults, etc.\n args = validateToolCall(contractToUse, args) as Record<string, unknown>;\n } catch (validationErr) {\n if (validationErr instanceof ToolValidationError) {\n logger.warn(\n 'ToolRunner',\n `Contract validation rejected ${handler.name}: ${validationErr.message}`,\n );\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: formatValidationError(validationErr),\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n // Unexpected non-validation error during parse — fall\n // through and let the rest of the runner handle it.\n }\n }\n\n // Schema validation: check required parameters before execution (LangGraph pattern)\n if (handler.parameters && typeof handler.parameters === 'object') {\n const schema = handler.parameters as { required?: string[]; properties?: Record<string, unknown> };\n if (schema.required && Array.isArray(schema.required)) {\n const missing = schema.required.filter(key => args[key] === undefined || args[key] === null);\n if (missing.length > 0) {\n const available = schema.properties ? Object.keys(schema.properties) : [];\n logger.warn('ToolRunner', `[SchemaValidation] ${handler.name}: missing required params: ${missing.join(', ')}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Missing required parameter(s): ${missing.join(', ')}. ` +\n `Expected parameters: ${available.join(', ')}. Please provide all required arguments.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n }\n }\n\n // v6.1.0-alpha.11 — Worktree scope (spike).\n //\n // When the current async context has a worktree scope active (see\n // src/agent/worktreeScope.ts), redirect mutating tool writes into\n // the scope's worktree directory. Lets multiple concurrent spawns\n // write the \"same\" filename without colliding. Pure-path-rewrite\n // — the tool still runs, just into a different absolute path.\n //\n // This runs BEFORE guardrails so guardrails see the rewritten\n // absolute path, not the LLM's original relative path (which\n // resolves against cwd and may falsely trigger system-path\n // protections). The self-mod scope-lock further below picks up\n // the same rewritten path; in practice worktree paths live under\n // ~/.titan/worktrees which isn't on the self-mod target list, so\n // self-mod stays a no-op for worktree-scoped writes.\n const MUTATING_TOOLS_FOR_WORKTREE = new Set(['write_file', 'edit_file', 'append_file', 'apply_patch']);\n if (MUTATING_TOOLS_FOR_WORKTREE.has(handler.name)) {\n try {\n const { getCurrentWorktreeScope } = await import('./worktreeScope.js');\n const wtScope = getCurrentWorktreeScope();\n if (wtScope) {\n const rawPath = (args.path || args.file_path || args.filePath) as string | undefined;\n if (typeof rawPath === 'string' && rawPath.length > 0) {\n if (rawPath.startsWith('/') || rawPath.startsWith('~')) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: this spawn runs in a worktree scope (${wtScope.subtaskId}). File writes must use RELATIVE paths — got \"${rawPath}\". Rewrite to something like \"output.md\" or \"src/foo.ts\" without a leading \"/\" or \"~\".`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n const { join: joinPath, dirname } = await import('path');\n const { mkdirSync } = await import('fs');\n const newPath = joinPath(wtScope.path, rawPath);\n if (args.path !== undefined) args.path = newPath;\n if (args.file_path !== undefined) args.file_path = newPath;\n if (args.filePath !== undefined) args.filePath = newPath;\n try { mkdirSync(dirname(newPath), { recursive: true }); }\n catch { /* best-effort */ }\n logger.debug(COMPONENT, `[Worktree] Redirected ${handler.name} → ${newPath} (scope: ${wtScope.goalId}/${wtScope.subtaskId})`);\n }\n }\n } catch (err) {\n logger.debug(COMPONENT, `Worktree scope check skipped: ${(err as Error).message}`);\n }\n }\n\n // Guardrails: validate tool call before execution\n try {\n const { guardToolCall } = await import('./guardrails.js');\n const guardResult = guardToolCall(handler.name, args);\n if (!guardResult.allowed) {\n logger.warn('ToolRunner', `[Guardrails] Blocked ${handler.name}: ${guardResult.reason}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Tool call blocked by guardrails — ${guardResult.reason}`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n } catch { /* guardrails unavailable — continue */ }\n\n // Read-only tool result cache (60s TTL, helper self-gates to read-only allowlist)\n const cacheArgKey = toolCall.function.arguments || '{}';\n const cachedResult = getCachedToolResult(handler.name, cacheArgKey);\n if (cachedResult !== null) {\n logger.info(COMPONENT, `[Cache HIT] ${handler.name}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: cachedResult,\n success: true,\n durationMs: Date.now() - startTime,\n };\n }\n\n logger.info(COMPONENT, `Executing tool: ${handler.name}`);\n\n // v5.0: Pre-execution scanner for dangerous commands\n if (handler.name === 'shell' || handler.name === 'code_exec') {\n const cmdArg = (args.command || args.code || args.script || '') as string;\n const scan = scanCommand(cmdArg);\n if (!scan.allowed) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Pre-execution scan blocked this command\\n${scan.warnings.join('\\n')}`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n if (scan.warnings.length > 0 && scan.level === 'warn') {\n logger.warn('ToolRunner', `Pre-exec warnings for ${handler.name}: ${scan.warnings.join('; ')}`);\n }\n }\n if (handler.name === 'browser_navigate' || handler.name === 'browser_auto_nav') {\n const urlArg = (args.url || args.target || '') as string;\n const scan = scanURL(urlArg);\n if (!scan.allowed) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Pre-execution scan blocked this URL\\n${scan.warnings.join('\\n')}`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n }\n\n // v5.0: Shell hooks — pre-tool\n const { getCurrentSessionId } = await import('./agent.js');\n const sessionId = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n const shellPre = await runPreToolShellHooks(handler.name, args, sessionId || toolCall.id, 'default', 0);\n if (!shellPre.allow) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: 'Blocked by shell hook: ' + (shellPre.reason || 'Hook denied execution'),\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n if (shellPre.modifiedArgs) args = shellPre.modifiedArgs;\n\n // Pre-tool hooks — plugins can block or modify args\n if (toolHookPlugins.length > 0) {\n const hookResult = await runPreTool(toolHookPlugins, handler.name, args);\n if (!hookResult.allow) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: 'Blocked by hook: ' + (hookResult.reason || 'Plugin denied execution'),\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n if (hookResult.modifiedArgs) args = hookResult.modifiedArgs;\n }\n\n // Autonomy gate: check if the tool is permitted under current mode\n const autonomyResult = await checkAutonomy(handler.name, args, channel);\n if (!autonomyResult.allowed) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: 'Action blocked by autonomy policy: ' + (autonomyResult.reason || 'Not permitted'),\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n\n // Shadow git checkpoint — snapshot files before mutation (fire-and-forget)\n const MUTATING_TOOLS = new Set(['write_file', 'edit_file', 'append_file', 'apply_patch']);\n\n // v4.9.0-local.7: kill-switch gate for file mutations. If the kill switch\n // is engaged, refuse write/edit/append/apply_patch so the initiative loop\n // can't keep accumulating fix-oscillations while the human hasn't resumed.\n // This closes the gap where `spawn_agent`, `autopilot`, and the pressure\n // cycle were gated but the main agent's tool path was not — meaning\n // initiative could keep rewriting the same files for hours after a kill.\n // See kill-switch.json `history` for the trigger; resume via\n // POST /api/safety/resume.\n if (MUTATING_TOOLS.has(handler.name)) {\n try {\n const { isKilled, getState } = await import('../safety/killSwitch.js');\n if (isKilled()) {\n const state = getState();\n const lastReason = state?.lastEvent?.reason ?? 'kill switch engaged';\n logger.warn(COMPONENT, `[KillSwitch] Refusing ${handler.name} — ${lastReason}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: File mutation refused — kill switch is engaged (${lastReason}). ` +\n `Resume via POST /api/safety/resume after investigating the trigger, ` +\n `then retry. Do NOT retry this tool call until resumed.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n } catch { /* kill switch module unavailable — fall through (fail-open on infra error) */ }\n }\n\n // v4.9.0-local.8: Self-mod scope lock + staging.\n //\n // Three-layer policy when the active session has a goal tagged as\n // self-modifying (see config.autonomy.selfMod.tags):\n // 1. Writes to paths OUTSIDE config.autonomy.selfMod.target are refused\n // (prevents LARPing self-improvement by writing to ~/titan-saas etc)\n // 2. When staging is enabled, writes INSIDE target are diverted to a\n // per-goal staging directory and a `self_mod_pr` approval is filed\n // 3. The original path is stored as `targetPath` on the staging entry\n // so the human sees what would land where if they approve the PR\n //\n // This is the deeper fix for the pattern observed 2026-04-18 where a\n // \"self-healing framework\" goal completed 100% by writing to an unrelated\n // Next.js app.\n let stagedRedirect: { stagedPath: string; targetPath: string } | null = null;\n if (MUTATING_TOOLS.has(handler.name)) {\n const rawFilePath = (args.path || args.file_path || args.filePath) as string | undefined;\n if (rawFilePath) {\n try {\n const { getCurrentSessionId } = await import('./agent.js');\n const { decideScope } = await import('./selfModStaging.js');\n const sid: string | null = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n const decision = decideScope(sid, rawFilePath);\n if (decision.action === 'reject') {\n logger.warn(COMPONENT, `[ScopeLock] Refusing ${handler.name}: ${decision.reason}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: ${decision.reason}\\n\\nRewrite the path to live inside the self-mod target, OR retag the goal to remove self-mod tags, OR pause the goal and create a properly-scoped one.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n if (decision.action === 'stage' && decision.stagedPath && decision.targetPath) {\n stagedRedirect = { stagedPath: decision.stagedPath, targetPath: decision.targetPath };\n // Rewrite the tool args so the handler writes to staging.\n // Preserve both `path` and `file_path` variants since\n // different tools use different field names.\n if (args.path !== undefined) args.path = decision.stagedPath;\n if (args.file_path !== undefined) args.file_path = decision.stagedPath;\n if (args.filePath !== undefined) args.filePath = decision.stagedPath;\n // Ensure the staged parent dir exists; write_file may not\n // mkdir -p on all code paths.\n try {\n const { mkdirSync } = await import('fs');\n const { dirname } = await import('path');\n mkdirSync(dirname(decision.stagedPath), { recursive: true });\n } catch { /* best-effort */ }\n logger.info(COMPONENT, `[SelfModStaging] Diverting ${handler.name} → ${decision.stagedPath} (would land at ${decision.targetPath} on approval)`);\n }\n } catch (err) {\n logger.debug(COMPONENT, `[ScopeLock] check failed (fail-open): ${(err as Error).message}`);\n }\n }\n }\n\n // v5.0: Filesystem checkpoint before destructive operations\n if (MUTATING_TOOLS.has(handler.name)) {\n const cpPaths: string[] = [];\n const cpPath = (args.path || args.file_path || args.filePath) as string;\n if (cpPath) cpPaths.push(cpPath);\n if (cpPaths.length > 0) {\n createCheckpoint(sessionId || toolCall.id, handler.name, args, cpPaths);\n }\n }\n\n if (MUTATING_TOOLS.has(handler.name)) {\n // Use the (potentially rewritten) path for shadow-git + fix-oscillation\n const filePath = (args.path || args.file_path || args.filePath) as string;\n if (filePath) {\n snapshotBeforeWrite(handler.name, filePath).catch(err =>\n logger.debug(COMPONENT, `Shadow checkpoint skipped: ${(err as Error).message}`),\n );\n // v4.9.0: fix-oscillation tracker. Same file written/edited\n // twice within 24h flags as oscillation, which feeds the\n // kill switch (3+ oscillations → kill). Best-effort — never\n // blocks the write.\n (async () => {\n try {\n const { recordFixEvent } = await import('../safety/fixOscillation.js');\n recordFixEvent({\n target: filePath,\n kind: 'file',\n detail: `${handler.name} via ${channel ?? 'unknown'}`,\n });\n } catch (err) {\n logger.debug(COMPONENT, `Fix-oscillation skipped: ${(err as Error).message}`);\n }\n })();\n }\n // v4.8.0: self-proposal capture — if this write is happening inside\n // an autonomous Soma-driven session, stash a copy for specialist\n // review. Fire-and-forget — never blocks tool execution.\n captureSelfProposalIfApplicable(handler.name, args).catch(err =>\n logger.debug(COMPONENT, `Self-proposal capture skipped: ${(err as Error).message}`),\n );\n }\n\n // Per-tool timeout lookup\n const toolTimeouts = (config.security as Record<string, unknown>).toolTimeouts as Record<string, number> | undefined;\n const baseTimeout = toolTimeouts?.[handler.name] || config.security.commandTimeout || 30000;\n\n // Retry config\n const retryConfig = (config.security as Record<string, unknown>).toolRetry as { enabled?: boolean; maxRetries?: number; backoffBaseMs?: number } | undefined;\n const retryEnabled = retryConfig?.enabled !== false;\n const maxRetries = retryConfig?.maxRetries ?? 3;\n const backoffBase = retryConfig?.backoffBaseMs ?? 1000;\n\n let lastError: Error | null = null;\n let lastErrorClass: ErrorClass = 'permanent';\n let attempt = 0;\n\n // Capture pre-execution file state for diff generation\n let preContent: string | undefined;\n let filePathForDiff: string | undefined;\n if (['write_file', 'edit_file', 'apply_patch'].includes(handler.name)) {\n const pathArg = args.path as string | undefined;\n if (pathArg) {\n filePathForDiff = pathArg;\n try {\n if (existsSync(pathArg)) preContent = readFileSync(pathArg, 'utf-8');\n } catch { /* ignore read errors */ }\n }\n }\n\n // Swarm invariants — hard safety rules (fail-open)\n try {\n const { checkInvariants } = await import('../safety/invariants.js');\n const invariant = checkInvariants(handler.name, args);\n if (!invariant.pass) {\n logger.warn(COMPONENT, `[Invariant] Blocked ${handler.name}: ${invariant.reason}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `INVARIANT_VIOLATION: ${invariant.reason}`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n } catch (err) {\n logger.warn(COMPONENT, `Invariant check failed (fail-open): ${(err as Error).message}`);\n }\n\n // beta.16 (Phase D.4) — auto-mode classifier + approval gate.\n //\n // PRECEDENCE (fixed in beta.17 after Codex P1 review):\n // 1. USER-CONFIGURED requiresApproval(handler.name) WINS FIRST.\n // If the user has explicitly added this tool to their\n // approval list, we file the approval regardless of what\n // the classifier says. The classifier never overrides\n // explicit user policy — auto-mode is a CONVENIENCE for\n // contract-declared safe tools, not a license to ignore\n // user-configured guards.\n // 2. THEN the classifier. If classifier says 'gate', file\n // approval (even if requiresApproval was false). If 'auto'\n // or 'notify', proceed past the gate; 'notify' logs a\n // breadcrumb so the autonomous action is visible.\n //\n // Beta.16 had these reversed: classifier short-circuited past\n // requiresApproval. That meant the user adding `write_file` to\n // their approval list did nothing because the classifier\n // unilaterally decided `auto` for it. Codex caught it on\n // review — P1 #1.\n const classification = classifyToolCall(handler.name);\n\n // Step 1 — user-configured approval ALWAYS runs first. The\n // user's explicit list is the source of truth.\n let userApprovalWantsGate = false;\n try {\n const { requiresApproval } = await import('../skills/builtin/approval_gates.js');\n userApprovalWantsGate = requiresApproval(handler.name);\n } catch (approvalErr) {\n // Approval gates module unavailable → log + fall through.\n // Classifier still gets to decide. Fail-open behavior preserved.\n logger.warn(COMPONENT, `Approval-config probe failed (fail-open): ${(approvalErr as Error).message}`);\n }\n\n // Step 2 — file an approval if EITHER the user's list demands it\n // OR the classifier says 'gate'. Belt + suspenders, with the user\n // explicitly first.\n if (userApprovalWantsGate || classification.decision === 'gate') {\n try {\n const { createApprovalRequest } = await import('../skills/builtin/approval_gates.js');\n const gateReason = userApprovalWantsGate\n ? 'user approval policy'\n : `classifier policy=${classification.policy} risk=${classification.riskLevel ?? 'unknown'}`;\n logger.info(COMPONENT, `[ApprovalGate] Tool \"${handler.name}\" requires human approval (${gateReason}) — filing request`);\n const request = createApprovalRequest(handler.name, args, sessionId || toolCall.id);\n if (request.status === 'pending') {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Awaiting approval: Tool \"${handler.name}\" requires human confirmation before execution. ` +\n `Request ID: ${request.id}. ` +\n `Approve with \"approve ${request.id}\" or deny with \"deny ${request.id}\".`,\n success: false,\n approvalPending: true,\n approvalRequestId: request.id,\n durationMs: Date.now() - startTime,\n };\n }\n if (request.status === 'denied') {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Tool \"${handler.name}\" was auto-denied by approval policy. ` +\n `Check approval preferences or change the request decision.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n // status === 'approved' or anything else → fall through to execute().\n } catch (approvalErr) {\n // Approval gates module unavailable → fail-open so the agent\n // doesn't deadlock. We've already warned above.\n logger.warn(COMPONENT, `Approval gate check failed (fail-open): ${(approvalErr as Error).message}`);\n }\n } else if (classification.decision === 'auto') {\n logger.debug(COMPONENT, formatBreadcrumb(handler.name, classification));\n } else if (classification.decision === 'notify') {\n // Passive notification. Future hook point for Mission Canvas\n // toasts / trajectory `note` events. Logged at info level so\n // the autonomous action is visible in titan-gateway.log + the\n // structured log pipeline.\n logger.info(COMPONENT, formatBreadcrumb(handler.name, classification));\n }\n\n const useWorktreeIsolation =\n config.security.useWorktreeIsolation === true &&\n contractToUse?.sideEffects.includes('destructive') === true;\n\n for (; attempt <= (retryEnabled ? maxRetries : 0); attempt++) {\n try {\n // On timeout retry, double the timeout\n const timeout = (attempt > 0 && lastErrorClass === 'timeout') ? baseTimeout * 2 : baseTimeout;\n\n const timeoutController = useWorktreeIsolation ? new AbortController() : undefined;\n const timeoutError = new Error(`Tool \"${handler.name}\" timed out after ${timeout}ms`);\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n let result = await Promise.race([\n useWorktreeIsolation\n ? runInIsolatedWorktree({\n spawnId: toolCall.id,\n args,\n execute: handler.execute,\n baseCwd: process.cwd(),\n signal: timeoutController?.signal,\n })\n : handler.execute(args),\n new Promise<string>((_, reject) => {\n timeoutHandle = setTimeout(() => {\n timeoutController?.abort(timeoutError);\n reject(timeoutError);\n }, timeout);\n }),\n ]).finally(() => {\n if (timeoutHandle) clearTimeout(timeoutHandle);\n });\n\n // Secret exfiltration guard — scan tool output before it leaves\n result = redactSecrets(result);\n\n // v5.0: PII redaction (privacy compliance)\n const config = loadConfig();\n if (config.security?.redactPII) {\n result = scanAndRedactPII(result);\n }\n\n // v5.0: Full exfiltration scan (layer 2-5) when configured\n if (config.security?.secretScan?.level === 'full') {\n const scan = fullExfilScan(result, 'tool_output');\n if (scan.blocked) {\n logger.warn('ToolRunner', `Exfiltration scan blocked ${handler.name}: ${scan.findings.map(f => f.type).join(', ')}`);\n }\n result = scan.redacted;\n }\n\n const durationMs = Date.now() - startTime;\n if (attempt > 0) {\n logger.info(COMPONENT, `Tool ${handler.name} succeeded on retry ${attempt} in ${durationMs}ms`);\n } else {\n logger.info(COMPONENT, `Tool ${handler.name} completed in ${durationMs}ms`);\n }\n\n // G1: Strip base64 image data before size check (prevents token\n // explosion). v6.1.0-alpha.55 — passes tool name so\n // DATA_URL_PRODUCING_TOOLS (download_image) get exempted; for\n // those the data URL IS the point of calling the tool.\n let finalContent = sanitizeBase64(result, handler.name);\n\n // Smart truncation — keep head + tail for large results (TITAN pattern).\n // v6.1.0-alpha.55 — DATA_URL_PRODUCING_TOOLS are exempted because\n // truncating mid-base64 produces an unusable data URL. The Writer\n // would then fall back to hotlinking the source URL, which is\n // exactly what Tony was seeing before alpha.55 (\"alpha.53 didn't\n // work\" — it did, this layer was killing it).\n if (finalContent.length > 30000 && !DATA_URL_PRODUCING_TOOLS.has(handler.name)) {\n const head = finalContent.slice(0, 20000);\n const tail = finalContent.slice(-5000);\n finalContent = head + '\\n\\n[... ' + (finalContent.length - 25000) + ' chars omitted ...]\\n\\n' + tail;\n logger.info(COMPONENT, `Tool ${handler.name} output truncated: ${result.length} → ${finalContent.length} chars`);\n }\n\n // v5.0: Shell hooks — post-tool\n const shellPost = await runPostToolShellHooks(handler.name, args, finalContent, sessionId || toolCall.id, 'default', 0);\n if (shellPost !== undefined) finalContent = shellPost;\n\n // Post-tool hooks — plugins can modify result\n if (toolHookPlugins.length > 0) {\n const hookResult = await runPostTool(toolHookPlugins, handler.name, args, { content: finalContent, success: true, durationMs });\n if (hookResult.modifiedContent !== undefined) finalContent = hookResult.modifiedContent;\n }\n\n // Cache the result for read-only tools (helper self-gates)\n cacheToolResult(handler.name, cacheArgKey, finalContent);\n\n // v4.9.0-local.8: if this write was diverted to staging, record\n // it in the self_mod_pr bundle. Fire-and-forget — must NOT block\n // the tool's return value or the agent loop.\n if (stagedRedirect) {\n (async () => {\n try {\n const { getCurrentSessionId } = await import('./agent.js');\n const { recordStagedWrite } = await import('./selfModStaging.js');\n const sid: string | null = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n await recordStagedWrite({\n sessionId: sid,\n toolName: handler.name,\n stagedPath: stagedRedirect!.stagedPath,\n targetPath: stagedRedirect!.targetPath,\n });\n } catch (err) {\n logger.debug(COMPONENT, `[SelfModStaging] recordStagedWrite failed: ${(err as Error).message}`);\n }\n })();\n }\n\n // Fire-and-forget telemetry + activity log\n (async () => {\n try {\n const { logActivity } = await import('../telemetry/activityLog.js');\n logActivity({ event: 'tool_call', tool: handler.name, channel: channel ?? 'unknown' });\n if (MUTATING_TOOLS.has(handler.name)) {\n logActivity({ event: 'file_edit', tool: handler.name, path: (args.path || args.file_path || args.filePath) as string | undefined });\n }\n if (handler.name === 'web_search') {\n logActivity({ event: 'web_search', query: (args.query || args.q) as string | undefined });\n }\n if (handler.name === 'web_fetch') {\n logActivity({ event: 'web_fetch', url: (args.url || args.target) as string | undefined });\n }\n if (handler.name === 'run_eval' || handler.name === 'eval_suite') {\n logActivity({ event: 'eval_run', suite: (args.suite || args.name) as string | undefined });\n }\n } catch { /* activity log non-critical */ }\n const cfg = loadConfig();\n if (cfg.telemetry?.enabled) {\n try {\n appendFileSync(TELEMETRY_EVENTS_PATH, JSON.stringify({\n event: 'tool_called',\n properties: { toolName: handler.name, success: true, durationMs, channel: channel ?? 'unknown' },\n timestamp: new Date().toISOString(),\n }) + '\\n', 'utf-8');\n } catch { /* non-critical */ }\n }\n // Remote analytics (PostHog + custom collector)\n const { trackToolCall } = await import('../analytics/featureTracker.js');\n const { getCurrentSessionId } = await import('./agent.js').catch(() => ({ getCurrentSessionId: () => null }));\n const sid = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n trackToolCall(handler.name, true, durationMs, undefined, sid ?? undefined);\n })();\n\n // Compute inline diff for file-modifying tools\n let diff: string | undefined;\n if (['write_file', 'edit_file', 'apply_patch'].includes(handler.name) && filePathForDiff && !stagedRedirect) {\n try {\n const postContent = existsSync(filePathForDiff) ? readFileSync(filePathForDiff, 'utf-8') : '';\n diff = computeUnifiedDiff(filePathForDiff, preContent ?? '', postContent);\n } catch { /* ignore diff errors */ }\n }\n\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: stagedRedirect\n ? `${finalContent}\\n\\n[SelfModStaging] Diverted to staging: ${stagedRedirect.stagedPath}. A human approval is pending before this lands at ${stagedRedirect.targetPath}.`\n : finalContent,\n success: true,\n durationMs,\n retryCount: attempt,\n diff,\n };\n } catch (error) {\n lastError = error as Error;\n lastErrorClass = classifyError(lastError, handler.name);\n\n // Don't retry permanent errors\n if (lastErrorClass === 'permanent') {\n break;\n }\n\n // Don't retry if this was the last attempt\n if (attempt >= maxRetries || !retryEnabled) {\n break;\n }\n\n // Exponential backoff: 1s, 2s, 4s (capped at 8s)\n const delay = Math.min(backoffBase * Math.pow(2, attempt), 8000);\n logger.warn(COMPONENT, `Tool ${handler.name} failed (${lastErrorClass}, attempt ${attempt + 1}/${maxRetries + 1}): ${lastError.message} — retrying in ${delay}ms`);\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n\n // All retries exhausted or permanent error\n const durationMs = Date.now() - startTime;\n const errorMsg = lastError?.message || 'Unknown error';\n const retryCount = attempt; // actual number of retries performed (matches success path)\n logger.error(COMPONENT, `Tool ${handler.name} failed (${lastErrorClass}${retryCount > 0 ? `, ${retryCount} retries` : ''}): ${errorMsg}`);\n\n // Fire-and-forget telemetry\n (async () => {\n const cfg = loadConfig();\n if (cfg.telemetry?.enabled) {\n try {\n appendFileSync(TELEMETRY_EVENTS_PATH, JSON.stringify({\n event: 'tool_called',\n properties: { toolName: handler.name, success: false, durationMs, errorClass: lastErrorClass, channel: channel ?? 'unknown' },\n timestamp: new Date().toISOString(),\n }) + '\\n', 'utf-8');\n } catch { /* non-critical */ }\n }\n // Remote analytics (PostHog + custom collector)\n const { trackToolCall } = await import('../analytics/featureTracker.js');\n const { getCurrentSessionId } = await import('./agent.js').catch(() => ({ getCurrentSessionId: () => null }));\n const sid = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n trackToolCall(handler.name, false, durationMs, lastErrorClass, sid ?? undefined);\n })();\n\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: ${errorMsg}`,\n success: false,\n durationMs,\n retryCount,\n errorClass: lastErrorClass,\n };\n}\n\n/** Execute multiple tool calls (in parallel where possible, with write-conflict detection) */\n/**\n * Apply the tool-output cap to every result. v5.8.0 — Anthropic's \"Writing\n * effective tools for AI agents\" recommends a 25k-token cap on tool results\n * with a tool-aware truncation hint. We do it here at the boundary so every\n * caller (agent loop, sub-agent, voice path) benefits without each having\n * to remember to wrap individually.\n * Reference: docs/HARNESS-PATTERNS.md, src/agent/toolOutputCap.ts\n */\nfunction applyOutputCaps(results: ToolResult[]): ToolResult[] {\n return results.map((r) => {\n const capped = capToolOutput(r.name, r.content);\n if (!capped.truncated) return r;\n // Only mutate when truncation actually happened, preserving identity\n // when content was already small.\n logger.info(COMPONENT, `[OutputCap] ${r.name} truncated ${capped.originalSize} → ${capped.content.length} chars`);\n return { ...r, content: capped.content };\n });\n}\n\nexport async function executeTools(toolCalls: ToolCall[], channel?: string): Promise<ToolResult[]> {\n // Single tool — fast path\n if (toolCalls.length <= 1) {\n const results = await Promise.all(toolCalls.map(tc => executeTool(tc, channel)));\n return applyOutputCaps(results);\n }\n\n // Multiple tools — use parallelTools engine with write-conflict detection\n const parallelCalls = toolCalls.map(tc => {\n let args: Record<string, unknown> = {};\n try { args = JSON.parse(tc.function.arguments); } catch { /* use empty */ }\n return { id: tc.id, name: tc.function.name, args };\n });\n\n const executor = async (name: string, args: Record<string, unknown>): Promise<ToolResult> => {\n // Build a synthetic ToolCall for executeTool\n const syntheticTc: ToolCall = {\n id: '',\n type: 'function',\n function: { name, arguments: JSON.stringify(args) },\n };\n return executeTool(syntheticTc, channel);\n };\n\n const parallelResults = await executeToolsParallel<ToolResult>(parallelCalls, executor);\n\n // Map back to ToolResult format with full metadata, then apply output cap.\n const mapped: ToolResult[] = parallelResults.map(pr => ({\n ...pr.content,\n toolCallId: pr.toolCallId,\n name: pr.name,\n }));\n return applyOutputCaps(mapped);\n}\n\n// ── Self-proposal capture helper (v4.8.0) ────────────────────────────────\n\n/**\n * If the current write is happening in an autonomous, Soma-driven session,\n * stash a copy of the written content for specialist review. Silent no-op\n * in all other cases (user-driven edits, non-autonomous mode, or when\n * selfMod.enabled is false in config).\n */\nasync function captureSelfProposalIfApplicable(\n toolName: string,\n args: Record<string, unknown>,\n): Promise<void> {\n // Resolve what we can from the current autonomous context\n const { getCurrentSessionId } = await import('./agent.js').catch(() => ({ getCurrentSessionId: () => null }));\n const sessionId: string | null = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n const sessionGoal = getSessionGoal(sessionId);\n const config = loadConfig();\n const autonomous = (config.autonomy?.mode === 'autonomous');\n const goalProposedBy = sessionGoal?.proposedBy ?? null;\n\n if (!shouldCapture({ toolName, autonomous, goalProposedBy })) return;\n\n const filePath = (args.path || args.file_path || args.filePath) as string | undefined;\n const content = (args.content || args.new_text || args.data) as string | undefined;\n if (!filePath || !content) return;\n\n captureWrite({\n toolName,\n filePath,\n content,\n sessionId,\n agentId: null, // filled by downstream if needed\n goalId: sessionGoal?.goalId ?? null,\n goalTitle: sessionGoal?.goalTitle ?? null,\n goalProposedBy,\n });\n}\n"],"mappings":";AAKA;AAAA,EAEI;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP,SAAS,kBAAkB,wBAAwB;AACnD,SAAS,gBAAgB,cAAc,kBAAkB;AACzD,SAAS,6BAA6B;AACtC,SAAS,4BAA4B;AACrC,SAAS,qBAAqB;AAC9B,SAAS,YAAY,mBAAmB;AAExC,SAAS,OAAO,6BAA6B;AAG7C,IAAI,kBAAyC,CAAC;AACvC,SAAS,mBAAmB,SAAsC;AACrE,oBAAkB;AACtB;AACA,OAAO,YAAY;AACnB,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,0BAA0B;AACnC,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB,qBAAqB;AAChD,SAAS,aAAa,eAAe;AACrC,SAAS,sBAAsB,6BAA6B;AAC5D,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAC7B,SAAS,oBAAoB;AAG7B,SAAS,mBAAmB,UAAkB,YAAoB,YAA4B;AAC1F,MAAI,eAAe,WAAY,QAAO,oBAAoB,QAAQ;AAClE,QAAM,WAAW,WAAW,MAAM,IAAI;AACtC,QAAM,WAAW,WAAW,MAAM,IAAI;AACtC,QAAM,SAAS,OAAO,QAAQ;AAAA,MAAS,QAAQ;AAC/C,QAAM,QAAkB,CAAC;AACzB,MAAI,IAAI,GAAG,IAAI;AACf,SAAO,IAAI,SAAS,UAAU,IAAI,SAAS,QAAQ;AAC/C,QAAI,IAAI,SAAS,UAAU,IAAI,SAAS,UAAU,SAAS,CAAC,MAAM,SAAS,CAAC,GAAG;AAC3E;AAAK;AAAK;AAAA,IACd;AACA,UAAM,SAAS,GAAG,SAAS;AAC3B,UAAM,UAAoB,CAAC;AAC3B,UAAM,QAAkB,CAAC;AACzB,WAAO,IAAI,SAAS,WAAW,KAAK,SAAS,UAAU,SAAS,CAAC,MAAM,SAAS,CAAC,IAAI;AACjF,cAAQ,KAAK,SAAS,GAAG,CAAC;AAAA,IAC9B;AACA,WAAO,IAAI,SAAS,WAAW,KAAK,SAAS,UAAU,SAAS,CAAC,MAAM,SAAS,CAAC,IAAI;AACjF,YAAM,KAAK,SAAS,GAAG,CAAC;AAAA,IAC5B;AACA,QAAI,QAAQ,UAAU,MAAM,QAAQ;AAChC,YAAM,YAAY,SAAS,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM;AAChE,YAAM,WAAW,SAAS,MAAM,GAAG,KAAK,IAAI,SAAS,QAAQ,IAAI,CAAC,CAAC;AACnE,YAAM,KAAK;AAAA,QACP,GAAG,UAAU,IAAI,OAAK,IAAI,CAAC,EAAE;AAAA,QAC7B,GAAG,QAAQ,IAAI,OAAK,IAAI,CAAC,EAAE;AAAA,QAC3B,GAAG,MAAM,IAAI,OAAK,IAAI,CAAC,EAAE;AAAA,QACzB,GAAG,SAAS,IAAI,OAAK,IAAI,CAAC,EAAE;AAAA,MAChC,EAAE,KAAK,IAAI,CAAC;AAAA,IAChB;AAAA,EACJ;AACA,QAAM,OAAO,MAAM,KAAK,SAAS;AACjC,SAAO,GAAG,MAAM;AAAA,EAAK,IAAI;AAC7B;AACA,SAAS,qBAAqB,uBAAuB;AACrD,SAAS,uBAAuB,sBAAsB;AACtD,SAAS,2BAA2B;AACpC,SAAS,cAAc,qBAAqB;AAC5C,SAAS,sBAAsB;AAE/B,MAAM,YAAY;AAoBlB,MAAM,2BAA2B,oBAAI,IAAI;AAAA,EACrC;AACJ,CAAC;AAYD,SAAS,eAAe,SAAiB,UAA2B;AAChE,MAAI,YAAY,yBAAyB,IAAI,QAAQ,EAAG,QAAO;AAC/D,SAAO,QAAQ;AAAA,IACX;AAAA,IACA,CAAC,UAAU;AACP,YAAM,QAAQ,KAAK,MAAM,MAAM,SAAS,MAAM,QAAQ,GAAG,IAAI,KAAK,IAAI;AACtE,aAAO,YAAY,QAAQ,MAAM,QAAQ,CAAC,CAAC;AAAA,IAC/C;AAAA,EACJ;AACJ;AAOO,SAAS,uBAAuB,UAA2B;AAC9D,SAAO,yBAAyB,IAAI,QAAQ;AAChD;AASO,SAAS,cAAc,OAAc,WAA+B;AACvE,QAAM,aAAa,sBAAsB,KAAK;AAC9C,UAAQ,WAAW,QAAQ;AAAA,IACvB,KAAK,eAAe;AAChB,aAAO;AAAA,IACX,KAAK,eAAe;AAChB,aAAO;AAAA,IACX,KAAK,eAAe;AAAA,IACpB,KAAK,eAAe;AAAA,IACpB,KAAK,eAAe;AAAA,IACpB,KAAK,eAAe;AAChB,aAAO;AAAA,IACX;AACI,aAAO,WAAW,YAAY,cAAc;AAAA,EACpD;AACJ;AAsCA,MAAM,eAAyC,oBAAI,IAAI;AAGhD,SAAS,aAAa,SAA4B;AACrD,eAAa,IAAI,QAAQ,MAAM,OAAO;AACtC,SAAO,MAAM,WAAW,oBAAoB,QAAQ,IAAI,EAAE;AAC9D;AAGO,SAAS,eAAe,MAAoB;AAC/C,eAAa,OAAO,IAAI;AAC5B;AAGO,SAAS,qBAAoC;AAChD,SAAO,MAAM,KAAK,aAAa,OAAO,CAAC;AAC3C;AAGO,SAAS,qBAAuC;AACnD,QAAM,SAAS,WAAW;AAC1B,QAAM,UAAU,IAAI,IAAI,OAAO,SAAS,YAAY;AACpD,QAAM,SAAS,IAAI,IAAI,OAAO,SAAS,WAAW;AAElD,SAAO,MAAM,KAAK,aAAa,OAAO,CAAC,EAClC,OAAO,CAAC,SAAS;AACd,QAAI,OAAO,IAAI,KAAK,IAAI,EAAG,QAAO;AAClC,QAAI,QAAQ,OAAO,KAAK,CAAC,QAAQ,IAAI,KAAK,IAAI,EAAG,QAAO;AACxD,QAAI,CAAC,mBAAmB,KAAK,IAAI,EAAG,QAAO;AAC3C,WAAO;AAAA,EACX,CAAC,EACA,IAAI,CAAC,UAAU;AAAA,IACZ,MAAM;AAAA,IACN,UAAU;AAAA,MACN,MAAM,KAAK;AAAA,MACX,aAAa,KAAK;AAAA,MAClB,YAAY,KAAK;AAAA,IACrB;AAAA,EACJ,EAAE;AACV;AAGA,SAAS,gBAAgB,MAAc,UAA0B;AAC7D,SAAO,iBAAiB,cAAc,IAAI,CAAC,EAAE,QAAQ,QAAQ,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ;AAC9F;AAEA,SAAS,uBAAuB,SAAsE;AAClG,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO,EAAE,SAAS,YAAY;AACpD,MAAI;AACA,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,QAAI,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,GAAG;AAChE,YAAM,UAAU,OAAO,KAAK,MAAM,EAAE,KAAK;AACzC,YAAM,cAAc,QACf,MAAM,GAAG,CAAC,EACV,IAAI,CAAC,QAAQ,gBAAgB,KAAK,EAAE,CAAC,EACrC,OAAO,OAAO;AACnB,UAAI,YAAY,WAAW,EAAG,QAAO,EAAE,SAAS,cAAc,SAAS,CAAC,EAAE;AAC1E,YAAM,SAAS,QAAQ,SAAS,YAAY,SAAS,KAAK,QAAQ,SAAS,YAAY,MAAM,KAAK;AAClG,aAAO,EAAE,SAAS,QAAQ,YAAY,KAAK,GAAG,CAAC,GAAG,MAAM,IAAI,SAAS,YAAY;AAAA,IACrF;AACA,WAAO,EAAE,SAAS,QAAQ,MAAM,QAAQ,MAAM,IAAI,UAAU,OAAO,MAAM,GAAG;AAAA,EAChF,QAAQ;AACJ,WAAO,EAAE,SAAS,oBAAoB;AAAA,EAC1C;AACJ;AAEA,SAAS,qBAAqB,QASrB;AACL,MAAI;AACA,UAAM,YAAY,OAAO,QAAQ,gBAAgB,OAAO,OAAO,GAAG,IAAI;AACtE,iBAAa;AAAA,MACT,WAAW,OAAO;AAAA,MAClB,MAAM;AAAA,MACN,SAAS,gBAAgB,GAAG,OAAO,QAAQ,IAAI,OAAO,UAAU,IAAI,GAAG;AAAA,MACvE,QAAQ,OAAO;AAAA,MACf,SAAS,OAAO;AAAA,MAChB,aAAa,OAAO;AAAA,MACpB,MAAM,OAAO,WAAW,SAClB,EAAE,GAAI,OAAO,UAAU,EAAE,UAAU,OAAO,QAAQ,IAAI,CAAC,GAAI,GAAI,YAAY,EAAE,OAAO,UAAU,IAAI,CAAC,EAAG,IACrG,OAAO,UAAU,EAAE,UAAU,OAAO,QAAQ,IAAI;AAAA,IAC3D,CAAC;AAAA,EACL,QAAQ;AAAA,EAER;AACJ;AAEA,eAAsB,YAAY,UAAoB,SAAuC;AACzF,QAAM,OAAO,aAAa;AAC1B,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,UAAU,uBAAuB,SAAS,SAAS,SAAS;AAClE,MAAI;AACA,UAAM,SAAS,MAAM,iBAAiB,UAAU,OAAO;AACvD,yBAAqB;AAAA,MACjB,UAAU;AAAA,MACV,UAAU,SAAS,SAAS;AAAA,MAC5B,QAAQ,OAAO,UAAU,OAAO;AAAA,MAChC;AAAA,MACA,YAAY,KAAK,IAAI,IAAI;AAAA,MACzB,YAAY,QAAQ;AAAA,MACpB,SAAS,QAAQ;AAAA,MACjB,OAAO,OAAO,UAAU,SAAY,OAAO;AAAA,IAC/C,CAAC;AACD,WAAO;AAAA,EACX,SAAS,KAAK;AACV,yBAAqB;AAAA,MACjB,UAAU;AAAA,MACV,UAAU,SAAS,SAAS;AAAA,MAC5B,QAAQ;AAAA,MACR;AAAA,MACA,YAAY,KAAK,IAAI,IAAI;AAAA,MACzB,YAAY,QAAQ;AAAA,MACpB,SAAS,QAAQ;AAAA,MACjB,OAAQ,IAAc;AAAA,IAC1B,CAAC;AACD,UAAM;AAAA,EACV;AACJ;AAEA,eAAe,iBAAiB,UAAoB,SAAuC;AACvF,QAAM,SAAS,WAAW;AAC1B,QAAM,YAAY,KAAK,IAAI;AAC3B,QAAM,UAAU,aAAa,IAAI,SAAS,SAAS,IAAI;AAEvD,MAAI,CAAC,SAAS;AAEV,UAAM,YAAY,MAAM,KAAK,aAAa,KAAK,CAAC,EAAE,KAAK;AACvD,UAAM,cAAc,UAAU,OAAO,OAAK;AACtC,YAAM,OAAO,SAAS,SAAS,KAAK,YAAY;AAChD,aAAO,EAAE,YAAY,EAAE,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,KAAK,SAAS,EAAE,MAAM,GAAG,CAAC,CAAC;AAAA,IACpF,CAAC,EAAE,MAAM,GAAG,CAAC;AACb,UAAM,OAAO,YAAY,SAAS,IAC5B;AAAA,gBAAmB,YAAY,KAAK,IAAI,CAAC,MACzC;AAAA,2BAA8B,UAAU,MAAM,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,GAAG,UAAU,SAAS,KAAK,SAAS,UAAU,SAAS,EAAE,WAAW,EAAE;AAC3I,WAAO;AAAA,MACH,YAAY,SAAS;AAAA,MACrB,MAAM,SAAS,SAAS;AAAA,MACxB,SAAS,WAAW,SAAS,SAAS,IAAI,yBAAyB,IAAI;AAAA;AAAA,MACvE,SAAS;AAAA,MACT,YAAY,KAAK,IAAI,IAAI;AAAA,IAC7B;AAAA,EACJ;AAGA,MAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,IAAI,GAAG;AACpD,WAAO;AAAA,MACH,YAAY,SAAS;AAAA,MACrB,MAAM,QAAQ;AAAA,MACd,SAAS,gBAAgB,QAAQ,IAAI;AAAA,MACrC,SAAS;AAAA,MACT,YAAY,KAAK,IAAI,IAAI;AAAA,IAC7B;AAAA,EACJ;AAGA,MAAI,CAAC,mBAAmB,QAAQ,IAAI,GAAG;AACnC,WAAO;AAAA,MACH,YAAY,SAAS;AAAA,MACrB,MAAM,QAAQ;AAAA,MACd,SAAS,gBAAgB,QAAQ,IAAI;AAAA,MACrC,SAAS;AAAA,MACT,YAAY,KAAK,IAAI,IAAI;AAAA,IAC7B;AAAA,EACJ;AAGA,MAAI,OAAgC,CAAC;AACrC,MAAI;AACA,WAAO,KAAK,MAAM,SAAS,SAAS,SAAS;AAAA,EACjD,SAAS,UAAU;AACf,WAAO,KAAK,cAAc,2BAA2B,QAAQ,IAAI,KAAM,SAAmB,OAAO,iBAAY,SAAS,SAAS,aAAa,IAAI,MAAM,GAAG,GAAG,CAAC,EAAE;AAE/J,UAAM,gBAAgB,SAAS,SAAS,aAAa,IAAI,MAAM,WAAW;AAC1E,QAAI,cAAc;AACd,UAAI;AAEA,cAAM,QAAQ,aAAa,CAAC,EAAE,QAAQ,UAAU,GAAG;AACnD,eAAO,KAAK,MAAM,KAAK;AACvB,eAAO,KAAK,cAAc,kCAAkC,QAAQ,IAAI,EAAE;AAAA,MAC9E,QAAQ;AAEJ,eAAO;AAAA,UACH,YAAY,SAAS;AAAA,UACrB,MAAM,QAAQ;AAAA,UACd,SAAS,yCAAyC,QAAQ,IAAI,YAAY,SAAS,SAAS,aAAa,IAAI,MAAM,GAAG,GAAG,CAAC;AAAA,UAC1H,SAAS;AAAA,UACT,YAAY,KAAK,IAAI,IAAI;AAAA,QAC7B;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AAaA,MAAI,gBAAgB,QAAQ;AAC5B,MAAI,CAAC,eAAe;AAChB,QAAI;AACA,YAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,mBAAmB;AAC5D,sBAAgB,gBAAgB,QAAQ,IAAI;AAAA,IAChD,QAAQ;AAAA,IAA6C;AAAA,EACzD;AACA,MAAI,eAAe;AACf,QAAI;AAIA,aAAO,iBAAiB,eAAe,IAAI;AAAA,IAC/C,SAAS,eAAe;AACpB,UAAI,yBAAyB,qBAAqB;AAC9C,eAAO;AAAA,UACH;AAAA,UACA,gCAAgC,QAAQ,IAAI,KAAK,cAAc,OAAO;AAAA,QAC1E;AACA,eAAO;AAAA,UACH,YAAY,SAAS;AAAA,UACrB,MAAM,QAAQ;AAAA,UACd,SAAS,sBAAsB,aAAa;AAAA,UAC5C,SAAS;AAAA,UACT,YAAY,KAAK,IAAI,IAAI;AAAA,QAC7B;AAAA,MACJ;AAAA,IAGJ;AAAA,EACJ;AAGA,MAAI,QAAQ,cAAc,OAAO,QAAQ,eAAe,UAAU;AAC9D,UAAM,SAAS,QAAQ;AACvB,QAAI,OAAO,YAAY,MAAM,QAAQ,OAAO,QAAQ,GAAG;AACnD,YAAM,UAAU,OAAO,SAAS,OAAO,SAAO,KAAK,GAAG,MAAM,UAAa,KAAK,GAAG,MAAM,IAAI;AAC3F,UAAI,QAAQ,SAAS,GAAG;AACpB,cAAM,YAAY,OAAO,aAAa,OAAO,KAAK,OAAO,UAAU,IAAI,CAAC;AACxE,eAAO,KAAK,cAAc,sBAAsB,QAAQ,IAAI,8BAA8B,QAAQ,KAAK,IAAI,CAAC,EAAE;AAC9G,eAAO;AAAA,UACH,YAAY,SAAS;AAAA,UACrB,MAAM,QAAQ;AAAA,UACd,SAAS,yCAAyC,QAAQ,KAAK,IAAI,CAAC,0BACxC,UAAU,KAAK,IAAI,CAAC;AAAA,UAChD,SAAS;AAAA,UACT,YAAY,KAAK,IAAI,IAAI;AAAA,QAC7B;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AAiBA,QAAM,8BAA8B,oBAAI,IAAI,CAAC,cAAc,aAAa,eAAe,aAAa,CAAC;AACrG,MAAI,4BAA4B,IAAI,QAAQ,IAAI,GAAG;AAC/C,QAAI;AACA,YAAM,EAAE,wBAAwB,IAAI,MAAM,OAAO,oBAAoB;AACrE,YAAM,UAAU,wBAAwB;AACxC,UAAI,SAAS;AACT,cAAM,UAAW,KAAK,QAAQ,KAAK,aAAa,KAAK;AACrD,YAAI,OAAO,YAAY,YAAY,QAAQ,SAAS,GAAG;AACnD,cAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,WAAW,GAAG,GAAG;AACpD,mBAAO;AAAA,cACH,YAAY,SAAS;AAAA,cACrB,MAAM,QAAQ;AAAA,cACd,SAAS,+CAA+C,QAAQ,SAAS,sDAAiD,OAAO;AAAA,cACjI,SAAS;AAAA,cACT,YAAY,KAAK,IAAI,IAAI;AAAA,YAC7B;AAAA,UACJ;AACA,gBAAM,EAAE,MAAM,UAAU,QAAQ,IAAI,MAAM,OAAO,MAAM;AACvD,gBAAM,EAAE,UAAU,IAAI,MAAM,OAAO,IAAI;AACvC,gBAAM,UAAU,SAAS,QAAQ,MAAM,OAAO;AAC9C,cAAI,KAAK,SAAS,OAAW,MAAK,OAAO;AACzC,cAAI,KAAK,cAAc,OAAW,MAAK,YAAY;AACnD,cAAI,KAAK,aAAa,OAAW,MAAK,WAAW;AACjD,cAAI;AAAE,sBAAU,QAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;AAAA,UAAG,QAClD;AAAA,UAAoB;AAC1B,iBAAO,MAAM,WAAW,yBAAyB,QAAQ,IAAI,WAAM,OAAO,YAAY,QAAQ,MAAM,IAAI,QAAQ,SAAS,GAAG;AAAA,QAChI;AAAA,MACJ;AAAA,IACJ,SAAS,KAAK;AACV,aAAO,MAAM,WAAW,iCAAkC,IAAc,OAAO,EAAE;AAAA,IACrF;AAAA,EACJ;AAGA,MAAI;AACA,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,iBAAiB;AACxD,UAAM,cAAc,cAAc,QAAQ,MAAM,IAAI;AACpD,QAAI,CAAC,YAAY,SAAS;AACtB,aAAO,KAAK,cAAc,wBAAwB,QAAQ,IAAI,KAAK,YAAY,MAAM,EAAE;AACvF,aAAO;AAAA,QACH,YAAY,SAAS;AAAA,QACrB,MAAM,QAAQ;AAAA,QACd,SAAS,iDAA4C,YAAY,MAAM;AAAA,QACvE,SAAS;AAAA,QACT,YAAY,KAAK,IAAI,IAAI;AAAA,MAC7B;AAAA,IACJ;AAAA,EACJ,QAAQ;AAAA,EAA0C;AAGlD,QAAM,cAAc,SAAS,SAAS,aAAa;AACnD,QAAM,eAAe,oBAAoB,QAAQ,MAAM,WAAW;AAClE,MAAI,iBAAiB,MAAM;AACvB,WAAO,KAAK,WAAW,eAAe,QAAQ,IAAI,EAAE;AACpD,WAAO;AAAA,MACH,YAAY,SAAS;AAAA,MACrB,MAAM,QAAQ;AAAA,MACd,SAAS;AAAA,MACT,SAAS;AAAA,MACT,YAAY,KAAK,IAAI,IAAI;AAAA,IAC7B;AAAA,EACJ;AAEA,SAAO,KAAK,WAAW,mBAAmB,QAAQ,IAAI,EAAE;AAGxD,MAAI,QAAQ,SAAS,WAAW,QAAQ,SAAS,aAAa;AAC1D,UAAM,SAAU,KAAK,WAAW,KAAK,QAAQ,KAAK,UAAU;AAC5D,UAAM,OAAO,YAAY,MAAM;AAC/B,QAAI,CAAC,KAAK,SAAS;AACf,aAAO;AAAA,QACH,YAAY,SAAS;AAAA,QACrB,MAAM,QAAQ;AAAA,QACd,SAAS;AAAA,EAAmD,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA,QACpF,SAAS;AAAA,QACT,YAAY,KAAK,IAAI,IAAI;AAAA,MAC7B;AAAA,IACJ;AACA,QAAI,KAAK,SAAS,SAAS,KAAK,KAAK,UAAU,QAAQ;AACnD,aAAO,KAAK,cAAc,yBAAyB,QAAQ,IAAI,KAAK,KAAK,SAAS,KAAK,IAAI,CAAC,EAAE;AAAA,IAClG;AAAA,EACJ;AACA,MAAI,QAAQ,SAAS,sBAAsB,QAAQ,SAAS,oBAAoB;AAC5E,UAAM,SAAU,KAAK,OAAO,KAAK,UAAU;AAC3C,UAAM,OAAO,QAAQ,MAAM;AAC3B,QAAI,CAAC,KAAK,SAAS;AACf,aAAO;AAAA,QACH,YAAY,SAAS;AAAA,QACrB,MAAM,QAAQ;AAAA,QACd,SAAS;AAAA,EAA+C,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA,QAChF,SAAS;AAAA,QACT,YAAY,KAAK,IAAI,IAAI;AAAA,MAC7B;AAAA,IACJ;AAAA,EACJ;AAGA,QAAM,EAAE,oBAAoB,IAAI,MAAM,OAAO,YAAY;AACzD,QAAM,YAAY,OAAO,wBAAwB,aAAa,oBAAoB,IAAI;AACtF,QAAM,WAAW,MAAM,qBAAqB,QAAQ,MAAM,MAAM,aAAa,SAAS,IAAI,WAAW,CAAC;AACtG,MAAI,CAAC,SAAS,OAAO;AACjB,WAAO;AAAA,MACH,YAAY,SAAS;AAAA,MACrB,MAAM,QAAQ;AAAA,MACd,SAAS,6BAA6B,SAAS,UAAU;AAAA,MACzD,SAAS;AAAA,MACT,YAAY,KAAK,IAAI,IAAI;AAAA,IAC7B;AAAA,EACJ;AACA,MAAI,SAAS,aAAc,QAAO,SAAS;AAG3C,MAAI,gBAAgB,SAAS,GAAG;AAC5B,UAAM,aAAa,MAAM,WAAW,iBAAiB,QAAQ,MAAM,IAAI;AACvE,QAAI,CAAC,WAAW,OAAO;AACnB,aAAO;AAAA,QACH,YAAY,SAAS;AAAA,QACrB,MAAM,QAAQ;AAAA,QACd,SAAS,uBAAuB,WAAW,UAAU;AAAA,QACrD,SAAS;AAAA,QACT,YAAY,KAAK,IAAI,IAAI;AAAA,MAC7B;AAAA,IACJ;AACA,QAAI,WAAW,aAAc,QAAO,WAAW;AAAA,EACnD;AAGA,QAAM,iBAAiB,MAAM,cAAc,QAAQ,MAAM,MAAM,OAAO;AACtE,MAAI,CAAC,eAAe,SAAS;AACzB,WAAO;AAAA,MACH,YAAY,SAAS;AAAA,MACrB,MAAM,QAAQ;AAAA,MACd,SAAS,yCAAyC,eAAe,UAAU;AAAA,MAC3E,SAAS;AAAA,MACT,YAAY,KAAK,IAAI,IAAI;AAAA,IAC7B;AAAA,EACJ;AAGA,QAAM,iBAAiB,oBAAI,IAAI,CAAC,cAAc,aAAa,eAAe,aAAa,CAAC;AAUxF,MAAI,eAAe,IAAI,QAAQ,IAAI,GAAG;AAClC,QAAI;AACA,YAAM,EAAE,UAAU,SAAS,IAAI,MAAM,OAAO,yBAAyB;AACrE,UAAI,SAAS,GAAG;AACZ,cAAM,QAAQ,SAAS;AACvB,cAAM,aAAa,OAAO,WAAW,UAAU;AAC/C,eAAO,KAAK,WAAW,yBAAyB,QAAQ,IAAI,WAAM,UAAU,EAAE;AAC9E,eAAO;AAAA,UACH,YAAY,SAAS;AAAA,UACrB,MAAM,QAAQ;AAAA,UACd,SAAS,+DAA0D,UAAU;AAAA,UAG7E,SAAS;AAAA,UACT,YAAY,KAAK,IAAI,IAAI;AAAA,QAC7B;AAAA,MACJ;AAAA,IACJ,QAAQ;AAAA,IAAiF;AAAA,EAC7F;AAgBA,MAAI,iBAAoE;AACxE,MAAI,eAAe,IAAI,QAAQ,IAAI,GAAG;AAClC,UAAM,cAAe,KAAK,QAAQ,KAAK,aAAa,KAAK;AACzD,QAAI,aAAa;AACb,UAAI;AACA,cAAM,EAAE,qBAAAA,qBAAoB,IAAI,MAAM,OAAO,YAAY;AACzD,cAAM,EAAE,YAAY,IAAI,MAAM,OAAO,qBAAqB;AAC1D,cAAM,MAAqB,OAAOA,yBAAwB,aAAaA,qBAAoB,IAAI;AAC/F,cAAM,WAAW,YAAY,KAAK,WAAW;AAC7C,YAAI,SAAS,WAAW,UAAU;AAC9B,iBAAO,KAAK,WAAW,wBAAwB,QAAQ,IAAI,KAAK,SAAS,MAAM,EAAE;AACjF,iBAAO;AAAA,YACH,YAAY,SAAS;AAAA,YACrB,MAAM,QAAQ;AAAA,YACd,SAAS,UAAU,SAAS,MAAM;AAAA;AAAA;AAAA,YAClC,SAAS;AAAA,YACT,YAAY,KAAK,IAAI,IAAI;AAAA,UAC7B;AAAA,QACJ;AACA,YAAI,SAAS,WAAW,WAAW,SAAS,cAAc,SAAS,YAAY;AAC3E,2BAAiB,EAAE,YAAY,SAAS,YAAY,YAAY,SAAS,WAAW;AAIpF,cAAI,KAAK,SAAS,OAAW,MAAK,OAAO,SAAS;AAClD,cAAI,KAAK,cAAc,OAAW,MAAK,YAAY,SAAS;AAC5D,cAAI,KAAK,aAAa,OAAW,MAAK,WAAW,SAAS;AAG1D,cAAI;AACA,kBAAM,EAAE,UAAU,IAAI,MAAM,OAAO,IAAI;AACvC,kBAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,MAAM;AACvC,sBAAU,QAAQ,SAAS,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAAA,UAC/D,QAAQ;AAAA,UAAoB;AAC5B,iBAAO,KAAK,WAAW,8BAA8B,QAAQ,IAAI,WAAM,SAAS,UAAU,mBAAmB,SAAS,UAAU,eAAe;AAAA,QACnJ;AAAA,MACJ,SAAS,KAAK;AACV,eAAO,MAAM,WAAW,yCAA0C,IAAc,OAAO,EAAE;AAAA,MAC7F;AAAA,IACJ;AAAA,EACJ;AAGA,MAAI,eAAe,IAAI,QAAQ,IAAI,GAAG;AAClC,UAAM,UAAoB,CAAC;AAC3B,UAAM,SAAU,KAAK,QAAQ,KAAK,aAAa,KAAK;AACpD,QAAI,OAAQ,SAAQ,KAAK,MAAM;AAC/B,QAAI,QAAQ,SAAS,GAAG;AACpB,uBAAiB,aAAa,SAAS,IAAI,QAAQ,MAAM,MAAM,OAAO;AAAA,IAC1E;AAAA,EACJ;AAEA,MAAI,eAAe,IAAI,QAAQ,IAAI,GAAG;AAElC,UAAM,WAAY,KAAK,QAAQ,KAAK,aAAa,KAAK;AACtD,QAAI,UAAU;AACV,0BAAoB,QAAQ,MAAM,QAAQ,EAAE;AAAA,QAAM,SAC9C,OAAO,MAAM,WAAW,8BAA+B,IAAc,OAAO,EAAE;AAAA,MAClF;AAKA,OAAC,YAAY;AACT,YAAI;AACA,gBAAM,EAAE,eAAe,IAAI,MAAM,OAAO,6BAA6B;AACrE,yBAAe;AAAA,YACX,QAAQ;AAAA,YACR,MAAM;AAAA,YACN,QAAQ,GAAG,QAAQ,IAAI,QAAQ,WAAW,SAAS;AAAA,UACvD,CAAC;AAAA,QACL,SAAS,KAAK;AACV,iBAAO,MAAM,WAAW,4BAA6B,IAAc,OAAO,EAAE;AAAA,QAChF;AAAA,MACJ,GAAG;AAAA,IACP;AAIA,oCAAgC,QAAQ,MAAM,IAAI,EAAE;AAAA,MAAM,SACtD,OAAO,MAAM,WAAW,kCAAmC,IAAc,OAAO,EAAE;AAAA,IACtF;AAAA,EACJ;AAGA,QAAM,eAAgB,OAAO,SAAqC;AAClE,QAAM,cAAc,eAAe,QAAQ,IAAI,KAAK,OAAO,SAAS,kBAAkB;AAGtF,QAAM,cAAe,OAAO,SAAqC;AACjE,QAAM,eAAe,aAAa,YAAY;AAC9C,QAAM,aAAa,aAAa,cAAc;AAC9C,QAAM,cAAc,aAAa,iBAAiB;AAElD,MAAI,YAA0B;AAC9B,MAAI,iBAA6B;AACjC,MAAI,UAAU;AAGd,MAAI;AACJ,MAAI;AACJ,MAAI,CAAC,cAAc,aAAa,aAAa,EAAE,SAAS,QAAQ,IAAI,GAAG;AACnE,UAAM,UAAU,KAAK;AACrB,QAAI,SAAS;AACT,wBAAkB;AAClB,UAAI;AACA,YAAI,WAAW,OAAO,EAAG,cAAa,aAAa,SAAS,OAAO;AAAA,MACvE,QAAQ;AAAA,MAA2B;AAAA,IACvC;AAAA,EACJ;AAGA,MAAI;AACA,UAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,yBAAyB;AAClE,UAAM,YAAY,gBAAgB,QAAQ,MAAM,IAAI;AACpD,QAAI,CAAC,UAAU,MAAM;AACjB,aAAO,KAAK,WAAW,uBAAuB,QAAQ,IAAI,KAAK,UAAU,MAAM,EAAE;AACjF,aAAO;AAAA,QACH,YAAY,SAAS;AAAA,QACrB,MAAM,QAAQ;AAAA,QACd,SAAS,wBAAwB,UAAU,MAAM;AAAA,QACjD,SAAS;AAAA,QACT,YAAY,KAAK,IAAI,IAAI;AAAA,MAC7B;AAAA,IACJ;AAAA,EACJ,SAAS,KAAK;AACV,WAAO,KAAK,WAAW,uCAAwC,IAAc,OAAO,EAAE;AAAA,EAC1F;AAsBA,QAAM,iBAAiB,iBAAiB,QAAQ,IAAI;AAIpD,MAAI,wBAAwB;AAC5B,MAAI;AACA,UAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,qCAAqC;AAC/E,4BAAwB,iBAAiB,QAAQ,IAAI;AAAA,EACzD,SAAS,aAAa;AAGlB,WAAO,KAAK,WAAW,6CAA8C,YAAsB,OAAO,EAAE;AAAA,EACxG;AAKA,MAAI,yBAAyB,eAAe,aAAa,QAAQ;AAC7D,QAAI;AACA,YAAM,EAAE,sBAAsB,IAAI,MAAM,OAAO,qCAAqC;AACpF,YAAM,aAAa,wBACb,yBACA,qBAAqB,eAAe,MAAM,SAAS,eAAe,aAAa,SAAS;AAC9F,aAAO,KAAK,WAAW,wBAAwB,QAAQ,IAAI,8BAA8B,UAAU,yBAAoB;AACvH,YAAM,UAAU,sBAAsB,QAAQ,MAAM,MAAM,aAAa,SAAS,EAAE;AAClF,UAAI,QAAQ,WAAW,WAAW;AAC9B,eAAO;AAAA,UACH,YAAY,SAAS;AAAA,UACrB,MAAM,QAAQ;AAAA,UACd,SAAS,4BAA4B,QAAQ,IAAI,+DAC9B,QAAQ,EAAE,2BACA,QAAQ,EAAE,wBAAwB,QAAQ,EAAE;AAAA,UACzE,SAAS;AAAA,UACT,iBAAiB;AAAA,UACjB,mBAAmB,QAAQ;AAAA,UAC3B,YAAY,KAAK,IAAI,IAAI;AAAA,QAC7B;AAAA,MACJ;AACA,UAAI,QAAQ,WAAW,UAAU;AAC7B,eAAO;AAAA,UACH,YAAY,SAAS;AAAA,UACrB,MAAM,QAAQ;AAAA,UACd,SAAS,gBAAgB,QAAQ,IAAI;AAAA,UAErC,SAAS;AAAA,UACT,YAAY,KAAK,IAAI,IAAI;AAAA,QAC7B;AAAA,MACJ;AAAA,IAEJ,SAAS,aAAa;AAGlB,aAAO,KAAK,WAAW,2CAA4C,YAAsB,OAAO,EAAE;AAAA,IACtG;AAAA,EACJ,WAAW,eAAe,aAAa,QAAQ;AAC3C,WAAO,MAAM,WAAW,iBAAiB,QAAQ,MAAM,cAAc,CAAC;AAAA,EAC1E,WAAW,eAAe,aAAa,UAAU;AAK7C,WAAO,KAAK,WAAW,iBAAiB,QAAQ,MAAM,cAAc,CAAC;AAAA,EACzE;AAEA,QAAM,uBACF,OAAO,SAAS,yBAAyB,QACzC,eAAe,YAAY,SAAS,aAAa,MAAM;AAE3D,SAAO,YAAY,eAAe,aAAa,IAAI,WAAW;AAC1D,QAAI;AAEA,YAAM,UAAW,UAAU,KAAK,mBAAmB,YAAa,cAAc,IAAI;AAElF,YAAM,oBAAoB,uBAAuB,IAAI,gBAAgB,IAAI;AACzE,YAAM,eAAe,IAAI,MAAM,SAAS,QAAQ,IAAI,qBAAqB,OAAO,IAAI;AACpF,UAAI;AACJ,UAAI,SAAS,MAAM,QAAQ,KAAK;AAAA,QAC5B,uBACM,sBAAsB;AAAA,UACpB,SAAS,SAAS;AAAA,UAClB;AAAA,UACA,SAAS,QAAQ;AAAA,UACjB,SAAS,QAAQ,IAAI;AAAA,UACrB,QAAQ,mBAAmB;AAAA,QAC/B,CAAC,IACC,QAAQ,QAAQ,IAAI;AAAA,QAC1B,IAAI,QAAgB,CAAC,GAAG,WAAW;AAC/B,0BAAgB,WAAW,MAAM;AAC7B,+BAAmB,MAAM,YAAY;AACrC,mBAAO,YAAY;AAAA,UACvB,GAAG,OAAO;AAAA,QACd,CAAC;AAAA,MACL,CAAC,EAAE,QAAQ,MAAM;AACb,YAAI,cAAe,cAAa,aAAa;AAAA,MACjD,CAAC;AAGD,eAAS,cAAc,MAAM;AAG7B,YAAMC,UAAS,WAAW;AAC1B,UAAIA,QAAO,UAAU,WAAW;AAC5B,iBAAS,iBAAiB,MAAM;AAAA,MACpC;AAGA,UAAIA,QAAO,UAAU,YAAY,UAAU,QAAQ;AAC/C,cAAM,OAAO,cAAc,QAAQ,aAAa;AAChD,YAAI,KAAK,SAAS;AACd,iBAAO,KAAK,cAAc,6BAA6B,QAAQ,IAAI,KAAK,KAAK,SAAS,IAAI,OAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC,EAAE;AAAA,QACvH;AACA,iBAAS,KAAK;AAAA,MAClB;AAEA,YAAMC,cAAa,KAAK,IAAI,IAAI;AAChC,UAAI,UAAU,GAAG;AACb,eAAO,KAAK,WAAW,QAAQ,QAAQ,IAAI,uBAAuB,OAAO,OAAOA,WAAU,IAAI;AAAA,MAClG,OAAO;AACH,eAAO,KAAK,WAAW,QAAQ,QAAQ,IAAI,iBAAiBA,WAAU,IAAI;AAAA,MAC9E;AAMA,UAAI,eAAe,eAAe,QAAQ,QAAQ,IAAI;AAQtD,UAAI,aAAa,SAAS,OAAS,CAAC,yBAAyB,IAAI,QAAQ,IAAI,GAAG;AAC5E,cAAM,OAAO,aAAa,MAAM,GAAG,GAAK;AACxC,cAAM,OAAO,aAAa,MAAM,IAAK;AACrC,uBAAe,OAAO,eAAe,aAAa,SAAS,QAAS,4BAA4B;AAChG,eAAO,KAAK,WAAW,QAAQ,QAAQ,IAAI,sBAAsB,OAAO,MAAM,WAAM,aAAa,MAAM,QAAQ;AAAA,MACnH;AAGA,YAAM,YAAY,MAAM,sBAAsB,QAAQ,MAAM,MAAM,cAAc,aAAa,SAAS,IAAI,WAAW,CAAC;AACtH,UAAI,cAAc,OAAW,gBAAe;AAG5C,UAAI,gBAAgB,SAAS,GAAG;AAC5B,cAAM,aAAa,MAAM,YAAY,iBAAiB,QAAQ,MAAM,MAAM,EAAE,SAAS,cAAc,SAAS,MAAM,YAAAA,YAAW,CAAC;AAC9H,YAAI,WAAW,oBAAoB,OAAW,gBAAe,WAAW;AAAA,MAC5E;AAGA,sBAAgB,QAAQ,MAAM,aAAa,YAAY;AAKvD,UAAI,gBAAgB;AAChB,SAAC,YAAY;AACT,cAAI;AACA,kBAAM,EAAE,qBAAAF,qBAAoB,IAAI,MAAM,OAAO,YAAY;AACzD,kBAAM,EAAE,kBAAkB,IAAI,MAAM,OAAO,qBAAqB;AAChE,kBAAM,MAAqB,OAAOA,yBAAwB,aAAaA,qBAAoB,IAAI;AAC/F,kBAAM,kBAAkB;AAAA,cACpB,WAAW;AAAA,cACX,UAAU,QAAQ;AAAA,cAClB,YAAY,eAAgB;AAAA,cAC5B,YAAY,eAAgB;AAAA,YAChC,CAAC;AAAA,UACL,SAAS,KAAK;AACV,mBAAO,MAAM,WAAW,8CAA+C,IAAc,OAAO,EAAE;AAAA,UAClG;AAAA,QACJ,GAAG;AAAA,MACP;AAGA,OAAC,YAAY;AACT,YAAI;AACA,gBAAM,EAAE,YAAY,IAAI,MAAM,OAAO,6BAA6B;AAClE,sBAAY,EAAE,OAAO,aAAa,MAAM,QAAQ,MAAM,SAAS,WAAW,UAAU,CAAC;AACrF,cAAI,eAAe,IAAI,QAAQ,IAAI,GAAG;AAClC,wBAAY,EAAE,OAAO,aAAa,MAAM,QAAQ,MAAM,MAAO,KAAK,QAAQ,KAAK,aAAa,KAAK,SAAgC,CAAC;AAAA,UACtI;AACA,cAAI,QAAQ,SAAS,cAAc;AAC/B,wBAAY,EAAE,OAAO,cAAc,OAAQ,KAAK,SAAS,KAAK,EAAyB,CAAC;AAAA,UAC5F;AACA,cAAI,QAAQ,SAAS,aAAa;AAC9B,wBAAY,EAAE,OAAO,aAAa,KAAM,KAAK,OAAO,KAAK,OAA8B,CAAC;AAAA,UAC5F;AACA,cAAI,QAAQ,SAAS,cAAc,QAAQ,SAAS,cAAc;AAC9D,wBAAY,EAAE,OAAO,YAAY,OAAQ,KAAK,SAAS,KAAK,KAA4B,CAAC;AAAA,UAC7F;AAAA,QACJ,QAAQ;AAAA,QAAkC;AAC1C,cAAM,MAAM,WAAW;AACvB,YAAI,IAAI,WAAW,SAAS;AACxB,cAAI;AACA,2BAAe,uBAAuB,KAAK,UAAU;AAAA,cACjD,OAAO;AAAA,cACP,YAAY,EAAE,UAAU,QAAQ,MAAM,SAAS,MAAM,YAAAE,aAAY,SAAS,WAAW,UAAU;AAAA,cAC/F,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,YACtC,CAAC,IAAI,MAAM,OAAO;AAAA,UACtB,QAAQ;AAAA,UAAqB;AAAA,QACjC;AAEA,cAAM,EAAE,cAAc,IAAI,MAAM,OAAO,gCAAgC;AACvE,cAAM,EAAE,qBAAAF,qBAAoB,IAAI,MAAM,OAAO,YAAY,EAAE,MAAM,OAAO,EAAE,qBAAqB,MAAM,KAAK,EAAE;AAC5G,cAAM,MAAM,OAAOA,yBAAwB,aAAaA,qBAAoB,IAAI;AAChF,sBAAc,QAAQ,MAAM,MAAME,aAAY,QAAW,OAAO,MAAS;AAAA,MAC7E,GAAG;AAGH,UAAI;AACJ,UAAI,CAAC,cAAc,aAAa,aAAa,EAAE,SAAS,QAAQ,IAAI,KAAK,mBAAmB,CAAC,gBAAgB;AACzG,YAAI;AACA,gBAAM,cAAc,WAAW,eAAe,IAAI,aAAa,iBAAiB,OAAO,IAAI;AAC3F,iBAAO,mBAAmB,iBAAiB,cAAc,IAAI,WAAW;AAAA,QAC5E,QAAQ;AAAA,QAA2B;AAAA,MACvC;AAEA,aAAO;AAAA,QACH,YAAY,SAAS;AAAA,QACrB,MAAM,QAAQ;AAAA,QACd,SAAS,iBACH,GAAG,YAAY;AAAA;AAAA,wCAA6C,eAAe,UAAU,sDAAsD,eAAe,UAAU,MACpK;AAAA,QACN,SAAS;AAAA,QACT,YAAAA;AAAA,QACA,YAAY;AAAA,QACZ;AAAA,MACJ;AAAA,IACJ,SAAS,OAAO;AACZ,kBAAY;AACZ,uBAAiB,cAAc,WAAW,QAAQ,IAAI;AAGtD,UAAI,mBAAmB,aAAa;AAChC;AAAA,MACJ;AAGA,UAAI,WAAW,cAAc,CAAC,cAAc;AACxC;AAAA,MACJ;AAGA,YAAM,QAAQ,KAAK,IAAI,cAAc,KAAK,IAAI,GAAG,OAAO,GAAG,GAAI;AAC/D,aAAO,KAAK,WAAW,QAAQ,QAAQ,IAAI,YAAY,cAAc,aAAa,UAAU,CAAC,IAAI,aAAa,CAAC,MAAM,UAAU,OAAO,uBAAkB,KAAK,IAAI;AACjK,YAAM,IAAI,QAAQ,aAAW,WAAW,SAAS,KAAK,CAAC;AAAA,IAC3D;AAAA,EACJ;AAGA,QAAM,aAAa,KAAK,IAAI,IAAI;AAChC,QAAM,WAAW,WAAW,WAAW;AACvC,QAAM,aAAa;AACnB,SAAO,MAAM,WAAW,QAAQ,QAAQ,IAAI,YAAY,cAAc,GAAG,aAAa,IAAI,KAAK,UAAU,aAAa,EAAE,MAAM,QAAQ,EAAE;AAGxI,GAAC,YAAY;AACT,UAAM,MAAM,WAAW;AACvB,QAAI,IAAI,WAAW,SAAS;AACxB,UAAI;AACA,uBAAe,uBAAuB,KAAK,UAAU;AAAA,UACjD,OAAO;AAAA,UACP,YAAY,EAAE,UAAU,QAAQ,MAAM,SAAS,OAAO,YAAY,YAAY,gBAAgB,SAAS,WAAW,UAAU;AAAA,UAC5H,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QACtC,CAAC,IAAI,MAAM,OAAO;AAAA,MACtB,QAAQ;AAAA,MAAqB;AAAA,IACjC;AAEA,UAAM,EAAE,cAAc,IAAI,MAAM,OAAO,gCAAgC;AACvE,UAAM,EAAE,qBAAAF,qBAAoB,IAAI,MAAM,OAAO,YAAY,EAAE,MAAM,OAAO,EAAE,qBAAqB,MAAM,KAAK,EAAE;AAC5G,UAAM,MAAM,OAAOA,yBAAwB,aAAaA,qBAAoB,IAAI;AAChF,kBAAc,QAAQ,MAAM,OAAO,YAAY,gBAAgB,OAAO,MAAS;AAAA,EACnF,GAAG;AAEH,SAAO;AAAA,IACH,YAAY,SAAS;AAAA,IACrB,MAAM,QAAQ;AAAA,IACd,SAAS,UAAU,QAAQ;AAAA,IAC3B,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA,YAAY;AAAA,EAChB;AACJ;AAWA,SAAS,gBAAgB,SAAqC;AAC1D,SAAO,QAAQ,IAAI,CAAC,MAAM;AACtB,UAAM,SAAS,cAAc,EAAE,MAAM,EAAE,OAAO;AAC9C,QAAI,CAAC,OAAO,UAAW,QAAO;AAG9B,WAAO,KAAK,WAAW,eAAe,EAAE,IAAI,cAAc,OAAO,YAAY,WAAM,OAAO,QAAQ,MAAM,QAAQ;AAChH,WAAO,EAAE,GAAG,GAAG,SAAS,OAAO,QAAQ;AAAA,EAC3C,CAAC;AACL;AAEA,eAAsB,aAAa,WAAuB,SAAyC;AAE/F,MAAI,UAAU,UAAU,GAAG;AACvB,UAAM,UAAU,MAAM,QAAQ,IAAI,UAAU,IAAI,QAAM,YAAY,IAAI,OAAO,CAAC,CAAC;AAC/E,WAAO,gBAAgB,OAAO;AAAA,EAClC;AAGA,QAAM,gBAAgB,UAAU,IAAI,QAAM;AACtC,QAAI,OAAgC,CAAC;AACrC,QAAI;AAAE,aAAO,KAAK,MAAM,GAAG,SAAS,SAAS;AAAA,IAAG,QAAQ;AAAA,IAAkB;AAC1E,WAAO,EAAE,IAAI,GAAG,IAAI,MAAM,GAAG,SAAS,MAAM,KAAK;AAAA,EACrD,CAAC;AAED,QAAM,WAAW,OAAO,MAAc,SAAuD;AAEzF,UAAM,cAAwB;AAAA,MAC1B,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,UAAU,EAAE,MAAM,WAAW,KAAK,UAAU,IAAI,EAAE;AAAA,IACtD;AACA,WAAO,YAAY,aAAa,OAAO;AAAA,EAC3C;AAEA,QAAM,kBAAkB,MAAM,qBAAiC,eAAe,QAAQ;AAGtF,QAAM,SAAuB,gBAAgB,IAAI,SAAO;AAAA,IACpD,GAAG,GAAG;AAAA,IACN,YAAY,GAAG;AAAA,IACf,MAAM,GAAG;AAAA,EACb,EAAE;AACF,SAAO,gBAAgB,MAAM;AACjC;AAUA,eAAe,gCACX,UACA,MACa;AAEb,QAAM,EAAE,oBAAoB,IAAI,MAAM,OAAO,YAAY,EAAE,MAAM,OAAO,EAAE,qBAAqB,MAAM,KAAK,EAAE;AAC5G,QAAM,YAA2B,OAAO,wBAAwB,aAAa,oBAAoB,IAAI;AACrG,QAAM,cAAc,eAAe,SAAS;AAC5C,QAAM,SAAS,WAAW;AAC1B,QAAM,aAAc,OAAO,UAAU,SAAS;AAC9C,QAAM,iBAAiB,aAAa,cAAc;AAElD,MAAI,CAAC,cAAc,EAAE,UAAU,YAAY,eAAe,CAAC,EAAG;AAE9D,QAAM,WAAY,KAAK,QAAQ,KAAK,aAAa,KAAK;AACtD,QAAM,UAAW,KAAK,WAAW,KAAK,YAAY,KAAK;AACvD,MAAI,CAAC,YAAY,CAAC,QAAS;AAE3B,eAAa;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA;AAAA,IACT,QAAQ,aAAa,UAAU;AAAA,IAC/B,WAAW,aAAa,aAAa;AAAA,IACrC;AAAA,EACJ,CAAC;AACL;","names":["getCurrentSessionId","config","durationMs"]}
1
+ {"version":3,"sources":["../../src/agent/toolRunner.ts"],"sourcesContent":["/**\n * TITAN — Tool Runner\n * Executes tool calls from the LLM with sandboxing, timeouts, and result formatting.\n */\nimport type { ToolCall, ToolDefinition } from '../providers/base.js';\nimport {\n type ToolContract,\n ToolValidationError,\n validateToolCall,\n formatValidationError,\n} from './toolContract.js';\nimport { classifyToolCall, formatBreadcrumb } from './autoModeClassifier.js';\nimport { appendFileSync, readFileSync, existsSync } from 'fs';\nimport { TELEMETRY_EVENTS_PATH } from '../utils/constants.js';\nimport { executeToolsParallel } from './parallelTools.js';\nimport { capToolOutput } from './toolOutputCap.js';\nimport { runPreTool, runPostTool } from '../plugins/contextEngine.js';\nimport type { ContextEnginePlugin } from '../plugins/contextEngine.js';\nimport { run as runInIsolatedWorktree } from './worktreeExecutor.js';\n\n/** Tool hook plugins — set during agent initialization */\nlet toolHookPlugins: ContextEnginePlugin[] = [];\nexport function setToolHookPlugins(plugins: ContextEnginePlugin[]): void {\n toolHookPlugins = plugins;\n}\nimport logger from '../utils/logger.js';\nimport { loadConfig } from '../config/config.js';\nimport { checkAutonomy } from './autonomy.js';\nimport { isToolSkillEnabled } from '../skills/registry.js';\nimport { redactSecrets } from '../security/secretGuard.js';\nimport { scanAndRedactPII, fullExfilScan } from '../security/exfilScan.js';\nimport { scanCommand, scanURL } from '../security/preExecScan.js';\nimport { runPreToolShellHooks, runPostToolShellHooks } from '../hooks/shellHooks.js';\nimport { createCheckpoint } from '../checkpoint/manager.js';\nimport { mintActionId } from '../receipts/mint.js';\nimport { writeReceipt } from '../receipts/store.js';\n\n/** Compute a lightweight unified diff between old and new file content */\nfunction computeUnifiedDiff(filePath: string, oldContent: string, newContent: string): string {\n if (oldContent === newContent) return `// No changes to ${filePath}`;\n const oldLines = oldContent.split('\\n');\n const newLines = newContent.split('\\n');\n const header = `--- ${filePath}\\n+++ ${filePath}`;\n const hunks: string[] = [];\n let i = 0, j = 0;\n while (i < oldLines.length || j < newLines.length) {\n if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {\n i++; j++; continue;\n }\n const startI = i, startJ = j;\n const removed: string[] = [];\n const added: string[] = [];\n while (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {\n removed.push(oldLines[i++]);\n }\n while (j < newLines.length && (i >= oldLines.length || oldLines[i] !== newLines[j])) {\n added.push(newLines[j++]);\n }\n if (removed.length || added.length) {\n const ctxBefore = oldLines.slice(Math.max(0, startI - 2), startI);\n const ctxAfter = oldLines.slice(i, Math.min(oldLines.length, i + 2));\n hunks.push([\n ...ctxBefore.map(l => ` ${l}`),\n ...removed.map(l => `-${l}`),\n ...added.map(l => `+${l}`),\n ...ctxAfter.map(l => ` ${l}`),\n ].join('\\n'));\n }\n }\n const body = hunks.join('\\n---\\n');\n return `${header}\\n${body}`;\n}\nimport { getCachedToolResult, cacheToolResult } from './trajectoryCompressor.js';\nimport { classifyProviderError, FailoverReason } from '../providers/errorTaxonomy.js';\nimport { snapshotBeforeWrite } from './shadowGit.js';\nimport { captureWrite, shouldCapture } from './selfProposals.js';\nimport { getSessionGoal } from './autonomyContext.js';\n\nconst COMPONENT = 'ToolRunner';\n\n/**\n * Tools whose entire purpose is to RETURN a data: URL for the LLM\n * to embed in downstream output. These bypass `sanitizeBase64` (which\n * would strip the very thing they were called to produce) and the\n * 30KB smart-truncation (which would chop the data URL mid-base64\n * making it unembeddable).\n *\n * v6.1.0-alpha.55 added `download_image` here. Tony spotted the bug\n * the way only Tony would: \"Why doesn't TITAN download the images\n * itself?\" → we added the tool in alpha.53 → ToolRunner sanitizer\n * stripped its output → Writer never saw the data URL → hotlinked\n * external URLs instead → alpha.52 placeholder caught them →\n * appearance of \"alpha.53 doesn't work.\" It worked perfectly. We\n * were just throwing away its output two lines later.\n *\n * If you add a new tool here, also update the 30KB truncation\n * exemption below (line ~685) since both share this list.\n */\nconst DATA_URL_PRODUCING_TOOLS = new Set([\n 'download_image',\n]);\n\n/**\n * G1: Sanitize base64 image data from tool results (OpenClaw pattern).\n * Prevents token explosion when vision/screenshot tools return raw base64.\n * Replaces data URIs with a compact placeholder showing byte count.\n *\n * v6.1.0-alpha.55 — takes optional `toolName` so we can EXEMPT the\n * tools whose entire output IS a data URL (see\n * DATA_URL_PRODUCING_TOOLS above). Default behavior unchanged for\n * every other tool.\n */\nfunction sanitizeBase64(content: string, toolName?: string): string {\n if (toolName && DATA_URL_PRODUCING_TOOLS.has(toolName)) return content;\n return content.replace(\n /data:image\\/[^;]+;base64,[A-Za-z0-9+/=]{100,}/g,\n (match) => {\n const bytes = Math.ceil((match.length - match.indexOf(',') - 1) * 0.75);\n return `[image: ${(bytes / 1024).toFixed(1)}KB omitted]`;\n },\n );\n}\n\n/**\n * v6.1.0-alpha.55 — exported alias for tests + future callers. The\n * Set is the source of truth for \"tools that intentionally return a\n * data: URL for the LLM to embed downstream.\"\n */\nexport function isDataUrlProducingTool(toolName: string): boolean {\n return DATA_URL_PRODUCING_TOOLS.has(toolName);\n}\n\n/** Error classification for retry decisions */\nexport type ErrorClass = 'transient' | 'permanent' | 'timeout' | 'rate_limit';\n\n/** Classify an error to determine if retry is appropriate.\n * Delegates to the centralized error taxonomy, then maps back to ErrorClass\n * for backward compatibility with tool execution retry logic.\n */\nexport function classifyError(error: Error, _toolName: string): ErrorClass {\n const classified = classifyProviderError(error);\n switch (classified.reason) {\n case FailoverReason.TIMEOUT:\n return 'timeout';\n case FailoverReason.RATE_LIMIT:\n return 'rate_limit';\n case FailoverReason.SERVER_ERROR:\n case FailoverReason.NETWORK_ERROR:\n case FailoverReason.OVERLOADED:\n case FailoverReason.EMPTY_RESPONSE:\n return 'transient';\n default:\n return classified.retryable ? 'transient' : 'permanent';\n }\n}\n\n/** Tool execution result */\nexport interface ToolResult {\n toolCallId: string;\n name: string;\n content: string;\n success: boolean;\n durationMs: number;\n /** Number of retry attempts made (0 = first try succeeded/failed) */\n retryCount?: number;\n /** Error classification if the tool failed */\n errorClass?: ErrorClass;\n /** Inline unified diff for file-modifying tools (write_file, edit_file, apply_patch) */\n diff?: string;\n /** v5.0: True when the tool is paused waiting for human approval */\n approvalPending?: boolean;\n /** v5.0: Approval request ID when approvalPending is true */\n approvalRequestId?: string;\n}\n\n/** A registered tool handler */\nexport interface ToolHandler {\n name: string;\n description: string;\n parameters: Record<string, unknown>;\n execute: (args: Record<string, unknown>, context?: { signal?: AbortSignal }) => Promise<string>;\n /**\n * beta.15 (Phase D.3) — optional declarative contract. When set,\n * the tool runner validates incoming args against\n * `contract.input` BEFORE calling execute() and rejects malformed\n * calls with a clear, LLM-friendly error message. Skills without\n * a contract continue to work unchanged — opt-in upgrade.\n */\n contract?: ToolContract;\n}\n\n/** Global tool registry */\nconst toolRegistry: Map<string, ToolHandler> = new Map();\n\n/** Register a tool */\nexport function registerTool(handler: ToolHandler): void {\n toolRegistry.set(handler.name, handler);\n logger.debug(COMPONENT, `Registered tool: ${handler.name}`);\n}\n\n/** Unregister a tool */\nexport function unregisterTool(name: string): void {\n toolRegistry.delete(name);\n}\n\n/** Get all registered tools */\nexport function getRegisteredTools(): ToolHandler[] {\n return Array.from(toolRegistry.values());\n}\n\n/** Convert registered tools to LLM tool definitions */\nexport function getToolDefinitions(): ToolDefinition[] {\n const config = loadConfig();\n const allowed = new Set(config.security.allowedTools);\n const denied = new Set(config.security.deniedTools);\n\n return Array.from(toolRegistry.values())\n .filter((tool) => {\n if (denied.has(tool.name)) return false;\n if (allowed.size > 0 && !allowed.has(tool.name)) return false;\n if (!isToolSkillEnabled(tool.name)) return false;\n return true;\n })\n .map((tool) => ({\n type: 'function' as const,\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters,\n },\n }));\n}\n\n/** Execute a single tool call */\nfunction safeReceiptText(text: string, maxChars: number): string {\n return scanAndRedactPII(redactSecrets(text)).replace(/\\s+/g, ' ').trim().slice(0, maxChars);\n}\n\nfunction summarizeToolArguments(rawArgs: string | undefined): { summary: string; argKeys?: string[] } {\n if (!rawArgs?.trim()) return { summary: 'args=none' };\n try {\n const parsed = JSON.parse(rawArgs) as unknown;\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n const allKeys = Object.keys(parsed).sort();\n const visibleKeys = allKeys\n .slice(0, 8)\n .map((key) => safeReceiptText(key, 32))\n .filter(Boolean);\n if (visibleKeys.length === 0) return { summary: 'args=empty', argKeys: [] };\n const suffix = allKeys.length > visibleKeys.length ? ` +${allKeys.length - visibleKeys.length}` : '';\n return { summary: `args=${visibleKeys.join(',')}${suffix}`, argKeys: visibleKeys };\n }\n return { summary: `args=${Array.isArray(parsed) ? 'array' : typeof parsed}` };\n } catch {\n return { summary: 'args=invalid-json' };\n }\n}\n\nfunction writeToolCallReceipt(params: {\n actionId: string;\n toolName: string;\n status: 'ok' | 'fail';\n channel?: string;\n durationMs: number;\n argSummary: string;\n argKeys?: string[];\n error?: string;\n}): void {\n try {\n const safeError = params.error ? safeReceiptText(params.error, 200) : undefined;\n writeReceipt({\n action_id: params.actionId,\n kind: 'tool_call',\n summary: safeReceiptText(`${params.toolName} ${params.argSummary}`, 200),\n status: params.status,\n channel: params.channel,\n duration_ms: params.durationMs,\n meta: params.status === 'fail'\n ? { ...(params.argKeys ? { arg_keys: params.argKeys } : {}), ...(safeError ? { error: safeError } : {}) }\n : (params.argKeys ? { arg_keys: params.argKeys } : undefined),\n });\n } catch {\n /* receipt writes are best-effort and must not affect tool behavior */\n }\n}\n\nexport async function executeTool(toolCall: ToolCall, channel?: string): Promise<ToolResult> {\n const _aid = mintActionId();\n const _t0 = Date.now();\n const argInfo = summarizeToolArguments(toolCall.function.arguments);\n try {\n const result = await executeToolInner(toolCall, channel);\n writeToolCallReceipt({\n actionId: _aid,\n toolName: toolCall.function.name,\n status: result.success ? 'ok' : 'fail',\n channel,\n durationMs: Date.now() - _t0,\n argSummary: argInfo.summary,\n argKeys: argInfo.argKeys,\n error: result.success ? undefined : result.content,\n });\n return result;\n } catch (err) {\n writeToolCallReceipt({\n actionId: _aid,\n toolName: toolCall.function.name,\n status: 'fail',\n channel,\n durationMs: Date.now() - _t0,\n argSummary: argInfo.summary,\n argKeys: argInfo.argKeys,\n error: (err as Error).message,\n });\n throw err;\n }\n}\n\nasync function executeToolInner(toolCall: ToolCall, channel?: string): Promise<ToolResult> {\n const config = loadConfig();\n const startTime = Date.now();\n const handler = toolRegistry.get(toolCall.function.name);\n\n if (!handler) {\n // LangGraph pattern: tell the LLM which tools actually exist so it can self-correct\n const available = Array.from(toolRegistry.keys()).sort();\n const suggestions = available.filter(t => {\n const name = toolCall.function.name.toLowerCase();\n return t.toLowerCase().includes(name.slice(0, 4)) || name.includes(t.slice(0, 4));\n }).slice(0, 5);\n const hint = suggestions.length > 0\n ? `\\nDid you mean: ${suggestions.join(', ')}?`\n : `\\nAvailable tools include: ${available.slice(0, 20).join(', ')}${available.length > 20 ? ` (and ${available.length - 20} more)` : ''}`;\n return {\n toolCallId: toolCall.id,\n name: toolCall.function.name,\n content: `Error: \"${toolCall.function.name}\" is not a valid tool.${hint}\\nPlease use one of the available tools.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n\n // Check permissions\n if (config.security.deniedTools.includes(handler.name)) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Tool \"${handler.name}\" is denied by security policy`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n\n // Check if parent skill is enabled\n if (!isToolSkillEnabled(handler.name)) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Tool \"${handler.name}\" is disabled — its parent skill is turned off. Enable it in Mission Control.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n\n // Parse arguments\n let args: Record<string, unknown> = {};\n try {\n args = JSON.parse(toolCall.function.arguments);\n } catch (parseErr) {\n logger.warn('ToolRunner', `Malformed JSON args for ${handler.name}: ${(parseErr as Error).message} — raw: ${(toolCall.function.arguments || '').slice(0, 200)}`);\n // Try to salvage: if it looks like a truncated JSON, extract what we can\n const salvageMatch = (toolCall.function.arguments || '').match(/\\{[\\s\\S]*/);\n if (salvageMatch) {\n try {\n // Attempt to close the JSON and parse\n const fixed = salvageMatch[0].replace(/,?\\s*$/, '}');\n args = JSON.parse(fixed);\n logger.info('ToolRunner', `Salvaged partial JSON args for ${handler.name}`);\n } catch {\n // A5: Return error instead of executing with empty args (LangGraph pattern)\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Could not parse arguments for \"${handler.name}\". Raw: ${(toolCall.function.arguments || '').slice(0, 200)}. Please provide valid JSON arguments.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n }\n }\n\n // beta.15 (Phase D.3) — Zod contract validation. Runs BEFORE the\n // legacy required-fields check below so contract-bearing skills\n // get a precise error message. Skills without a contract fall\n // through to the legacy check unchanged.\n //\n // Contracts can be attached two ways:\n // 1. Directly on the ToolHandler (handler.contract — for new skills\n // that want a self-contained file).\n // 2. Registered in the global contract registry (for retrofitting\n // contracts onto existing skills without touching their files).\n // We check both, in that order.\n let contractToUse = handler.contract;\n if (!contractToUse) {\n try {\n const { getToolContract } = await import('./toolContract.js');\n contractToUse = getToolContract(handler.name);\n } catch { /* contract registry unavailable — skip */ }\n }\n if (contractToUse) {\n try {\n // Replace `args` with the parsed (possibly coerced) shape\n // so execute() receives Zod-cleaned arguments — string\n // numerics become numbers, undefined fields get defaults, etc.\n args = validateToolCall(contractToUse, args) as Record<string, unknown>;\n } catch (validationErr) {\n if (validationErr instanceof ToolValidationError) {\n logger.warn(\n 'ToolRunner',\n `Contract validation rejected ${handler.name}: ${validationErr.message}`,\n );\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: formatValidationError(validationErr),\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n // Unexpected non-validation error during parse — fall\n // through and let the rest of the runner handle it.\n }\n }\n\n // Schema validation: check required parameters before execution (LangGraph pattern)\n if (handler.parameters && typeof handler.parameters === 'object') {\n const schema = handler.parameters as { required?: string[]; properties?: Record<string, unknown> };\n if (schema.required && Array.isArray(schema.required)) {\n const missing = schema.required.filter(key => args[key] === undefined || args[key] === null);\n if (missing.length > 0) {\n const available = schema.properties ? Object.keys(schema.properties) : [];\n logger.warn('ToolRunner', `[SchemaValidation] ${handler.name}: missing required params: ${missing.join(', ')}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Missing required parameter(s): ${missing.join(', ')}. ` +\n `Expected parameters: ${available.join(', ')}. Please provide all required arguments.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n }\n }\n\n // v6.1.0-alpha.11 — Worktree scope (spike).\n //\n // When the current async context has a worktree scope active (see\n // src/agent/worktreeScope.ts), redirect mutating tool writes into\n // the scope's worktree directory. Lets multiple concurrent spawns\n // write the \"same\" filename without colliding. Pure-path-rewrite\n // — the tool still runs, just into a different absolute path.\n //\n // This runs BEFORE guardrails so guardrails see the rewritten\n // absolute path, not the LLM's original relative path (which\n // resolves against cwd and may falsely trigger system-path\n // protections). The self-mod scope-lock further below picks up\n // the same rewritten path; in practice worktree paths live under\n // ~/.titan/worktrees which isn't on the self-mod target list, so\n // self-mod stays a no-op for worktree-scoped writes.\n const MUTATING_TOOLS_FOR_WORKTREE = new Set(['write_file', 'edit_file', 'append_file', 'apply_patch']);\n if (MUTATING_TOOLS_FOR_WORKTREE.has(handler.name)) {\n try {\n const { getCurrentWorktreeScope } = await import('./worktreeScope.js');\n const wtScope = getCurrentWorktreeScope();\n if (wtScope) {\n const rawPath = (args.path || args.file_path || args.filePath) as string | undefined;\n if (typeof rawPath === 'string' && rawPath.length > 0) {\n if (rawPath.startsWith('/') || rawPath.startsWith('~')) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: this spawn runs in a worktree scope (${wtScope.subtaskId}). File writes must use RELATIVE paths — got \"${rawPath}\". Rewrite to something like \"output.md\" or \"src/foo.ts\" without a leading \"/\" or \"~\".`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n const { join: joinPath, dirname } = await import('path');\n const { mkdirSync } = await import('fs');\n const newPath = joinPath(wtScope.path, rawPath);\n if (args.path !== undefined) args.path = newPath;\n if (args.file_path !== undefined) args.file_path = newPath;\n if (args.filePath !== undefined) args.filePath = newPath;\n try { mkdirSync(dirname(newPath), { recursive: true }); }\n catch { /* best-effort */ }\n logger.debug(COMPONENT, `[Worktree] Redirected ${handler.name} → ${newPath} (scope: ${wtScope.goalId}/${wtScope.subtaskId})`);\n }\n }\n } catch (err) {\n logger.debug(COMPONENT, `Worktree scope check skipped: ${(err as Error).message}`);\n }\n }\n\n // Guardrails: validate tool call before execution\n try {\n const { guardToolCall } = await import('./guardrails.js');\n const guardResult = guardToolCall(handler.name, args);\n if (!guardResult.allowed) {\n logger.warn('ToolRunner', `[Guardrails] Blocked ${handler.name}: ${guardResult.reason}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Tool call blocked by guardrails — ${guardResult.reason}`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n } catch { /* guardrails unavailable — continue */ }\n\n // Read-only tool result cache (60s TTL, helper self-gates to read-only allowlist)\n const cacheArgKey = toolCall.function.arguments || '{}';\n const cachedResult = getCachedToolResult(handler.name, cacheArgKey);\n if (cachedResult !== null) {\n logger.info(COMPONENT, `[Cache HIT] ${handler.name}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: cachedResult,\n success: true,\n durationMs: Date.now() - startTime,\n };\n }\n\n logger.info(COMPONENT, `Executing tool: ${handler.name}`);\n\n // v5.0: Pre-execution scanner for dangerous commands\n if (handler.name === 'shell' || handler.name === 'code_exec') {\n const cmdArg = (args.command || args.code || args.script || '') as string;\n const scan = scanCommand(cmdArg);\n if (!scan.allowed) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Pre-execution scan blocked this command\\n${scan.warnings.join('\\n')}`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n if (scan.warnings.length > 0 && scan.level === 'warn') {\n logger.warn('ToolRunner', `Pre-exec warnings for ${handler.name}: ${scan.warnings.join('; ')}`);\n }\n }\n if (handler.name === 'browser_navigate' || handler.name === 'browser_auto_nav') {\n const urlArg = (args.url || args.target || '') as string;\n const scan = scanURL(urlArg);\n if (!scan.allowed) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Pre-execution scan blocked this URL\\n${scan.warnings.join('\\n')}`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n }\n\n // v5.0: Shell hooks — pre-tool\n const { getCurrentSessionId } = await import('./agent.js');\n const sessionId = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n const shellPre = await runPreToolShellHooks(handler.name, args, sessionId || toolCall.id, 'default', 0);\n if (!shellPre.allow) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: 'Blocked by shell hook: ' + (shellPre.reason || 'Hook denied execution'),\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n if (shellPre.modifiedArgs) args = shellPre.modifiedArgs;\n\n // Pre-tool hooks — plugins can block or modify args\n if (toolHookPlugins.length > 0) {\n const hookResult = await runPreTool(toolHookPlugins, handler.name, args);\n if (!hookResult.allow) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: 'Blocked by hook: ' + (hookResult.reason || 'Plugin denied execution'),\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n if (hookResult.modifiedArgs) args = hookResult.modifiedArgs;\n }\n\n // Autonomy gate: check if the tool is permitted under current mode\n const autonomyResult = await checkAutonomy(handler.name, args, channel);\n if (!autonomyResult.allowed) {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: 'Action blocked by autonomy policy: ' + (autonomyResult.reason || 'Not permitted'),\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n\n // Shadow git checkpoint — snapshot files before mutation (fire-and-forget)\n const MUTATING_TOOLS = new Set(['write_file', 'edit_file', 'append_file', 'apply_patch']);\n\n // v4.9.0-local.7: kill-switch gate for file mutations. If the kill switch\n // is engaged, refuse write/edit/append/apply_patch so the initiative loop\n // can't keep accumulating fix-oscillations while the human hasn't resumed.\n // This closes the gap where `spawn_agent`, `autopilot`, and the pressure\n // cycle were gated but the main agent's tool path was not — meaning\n // initiative could keep rewriting the same files for hours after a kill.\n // See kill-switch.json `history` for the trigger; resume via\n // POST /api/safety/resume.\n if (MUTATING_TOOLS.has(handler.name)) {\n try {\n const { isKilled, getState } = await import('../safety/killSwitch.js');\n if (isKilled()) {\n const state = getState();\n const lastReason = state?.lastEvent?.reason ?? 'kill switch engaged';\n logger.warn(COMPONENT, `[KillSwitch] Refusing ${handler.name} — ${lastReason}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: File mutation refused — kill switch is engaged (${lastReason}). ` +\n `Resume via POST /api/safety/resume after investigating the trigger, ` +\n `then retry. Do NOT retry this tool call until resumed.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n } catch { /* kill switch module unavailable — fall through (fail-open on infra error) */ }\n }\n\n // v4.9.0-local.8: Self-mod scope lock + staging.\n //\n // Three-layer policy when the active session has a goal tagged as\n // self-modifying (see config.autonomy.selfMod.tags):\n // 1. Writes to paths OUTSIDE config.autonomy.selfMod.target are refused\n // (prevents LARPing self-improvement by writing to ~/titan-saas etc)\n // 2. When staging is enabled, writes INSIDE target are diverted to a\n // per-goal staging directory and a `self_mod_pr` approval is filed\n // 3. The original path is stored as `targetPath` on the staging entry\n // so the human sees what would land where if they approve the PR\n //\n // This is the deeper fix for the pattern observed 2026-04-18 where a\n // \"self-healing framework\" goal completed 100% by writing to an unrelated\n // Next.js app.\n let stagedRedirect: { stagedPath: string; targetPath: string } | null = null;\n if (MUTATING_TOOLS.has(handler.name)) {\n const rawFilePath = (args.path || args.file_path || args.filePath) as string | undefined;\n if (rawFilePath) {\n try {\n const { getCurrentSessionId } = await import('./agent.js');\n const { decideScope } = await import('./selfModStaging.js');\n const sid: string | null = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n const decision = decideScope(sid, rawFilePath);\n if (decision.action === 'reject') {\n logger.warn(COMPONENT, `[ScopeLock] Refusing ${handler.name}: ${decision.reason}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: ${decision.reason}\\n\\nRewrite the path to live inside the self-mod target, OR retag the goal to remove self-mod tags, OR pause the goal and create a properly-scoped one.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n if (decision.action === 'stage' && decision.stagedPath && decision.targetPath) {\n stagedRedirect = { stagedPath: decision.stagedPath, targetPath: decision.targetPath };\n // Rewrite the tool args so the handler writes to staging.\n // Preserve both `path` and `file_path` variants since\n // different tools use different field names.\n if (args.path !== undefined) args.path = decision.stagedPath;\n if (args.file_path !== undefined) args.file_path = decision.stagedPath;\n if (args.filePath !== undefined) args.filePath = decision.stagedPath;\n // Ensure the staged parent dir exists; write_file may not\n // mkdir -p on all code paths.\n try {\n const { mkdirSync } = await import('fs');\n const { dirname } = await import('path');\n mkdirSync(dirname(decision.stagedPath), { recursive: true });\n } catch { /* best-effort */ }\n logger.info(COMPONENT, `[SelfModStaging] Diverting ${handler.name} → ${decision.stagedPath} (would land at ${decision.targetPath} on approval)`);\n }\n } catch (err) {\n logger.debug(COMPONENT, `[ScopeLock] check failed (fail-open): ${(err as Error).message}`);\n }\n }\n }\n\n // v5.0: Filesystem checkpoint before destructive operations\n if (MUTATING_TOOLS.has(handler.name)) {\n const cpPaths: string[] = [];\n const cpPath = (args.path || args.file_path || args.filePath) as string;\n if (cpPath) cpPaths.push(cpPath);\n if (cpPaths.length > 0) {\n createCheckpoint(sessionId || toolCall.id, handler.name, args, cpPaths);\n }\n }\n\n if (MUTATING_TOOLS.has(handler.name)) {\n // Use the (potentially rewritten) path for shadow-git + fix-oscillation\n const filePath = (args.path || args.file_path || args.filePath) as string;\n if (filePath) {\n snapshotBeforeWrite(handler.name, filePath).catch(err =>\n logger.debug(COMPONENT, `Shadow checkpoint skipped: ${(err as Error).message}`),\n );\n // v4.9.0: fix-oscillation tracker. Same file written/edited\n // twice within 24h flags as oscillation, which feeds the\n // kill switch (3+ oscillations → kill). Best-effort — never\n // blocks the write.\n (async () => {\n try {\n const { recordFixEvent } = await import('../safety/fixOscillation.js');\n recordFixEvent({\n target: filePath,\n kind: 'file',\n detail: `${handler.name} via ${channel ?? 'unknown'}`,\n });\n } catch (err) {\n logger.debug(COMPONENT, `Fix-oscillation skipped: ${(err as Error).message}`);\n }\n })();\n }\n // v4.8.0: self-proposal capture — if this write is happening inside\n // an autonomous Soma-driven session, stash a copy for specialist\n // review. Fire-and-forget — never blocks tool execution.\n captureSelfProposalIfApplicable(handler.name, args).catch(err =>\n logger.debug(COMPONENT, `Self-proposal capture skipped: ${(err as Error).message}`),\n );\n }\n\n // Per-tool timeout lookup\n const toolTimeouts = (config.security as Record<string, unknown>).toolTimeouts as Record<string, number> | undefined;\n const baseTimeout = toolTimeouts?.[handler.name] || config.security.commandTimeout || 30000;\n\n // Retry config\n const retryConfig = (config.security as Record<string, unknown>).toolRetry as { enabled?: boolean; maxRetries?: number; backoffBaseMs?: number } | undefined;\n const retryEnabled = retryConfig?.enabled !== false;\n const maxRetries = retryConfig?.maxRetries ?? 3;\n const backoffBase = retryConfig?.backoffBaseMs ?? 1000;\n\n let lastError: Error | null = null;\n let lastErrorClass: ErrorClass = 'permanent';\n let attempt = 0;\n\n // Capture pre-execution file state for diff generation\n let preContent: string | undefined;\n let filePathForDiff: string | undefined;\n if (['write_file', 'edit_file', 'apply_patch'].includes(handler.name)) {\n const pathArg = args.path as string | undefined;\n if (pathArg) {\n filePathForDiff = pathArg;\n try {\n if (existsSync(pathArg)) preContent = readFileSync(pathArg, 'utf-8');\n } catch { /* ignore read errors */ }\n }\n }\n\n // Swarm invariants — hard safety rules (fail-open)\n try {\n const { checkInvariants } = await import('../safety/invariants.js');\n const invariant = checkInvariants(handler.name, args);\n if (!invariant.pass) {\n logger.warn(COMPONENT, `[Invariant] Blocked ${handler.name}: ${invariant.reason}`);\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `INVARIANT_VIOLATION: ${invariant.reason}`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n } catch (err) {\n logger.warn(COMPONENT, `Invariant check failed (fail-open): ${(err as Error).message}`);\n }\n\n // beta.16 (Phase D.4) — auto-mode classifier + approval gate.\n //\n // PRECEDENCE (fixed in beta.17 after Codex P1 review):\n // 1. USER-CONFIGURED requiresApproval(handler.name) WINS FIRST.\n // If the user has explicitly added this tool to their\n // approval list, we file the approval regardless of what\n // the classifier says. The classifier never overrides\n // explicit user policy — auto-mode is a CONVENIENCE for\n // contract-declared safe tools, not a license to ignore\n // user-configured guards.\n // 2. THEN the classifier. If classifier says 'gate', file\n // approval (even if requiresApproval was false). If 'auto'\n // or 'notify', proceed past the gate; 'notify' logs a\n // breadcrumb so the autonomous action is visible.\n //\n // Beta.16 had these reversed: classifier short-circuited past\n // requiresApproval. That meant the user adding `write_file` to\n // their approval list did nothing because the classifier\n // unilaterally decided `auto` for it. Codex caught it on\n // review — P1 #1.\n const classification = classifyToolCall(handler.name);\n\n // Step 1 — user-configured approval ALWAYS runs first. The\n // user's explicit list is the source of truth.\n let userApprovalWantsGate = false;\n try {\n const { requiresApproval } = await import('../skills/builtin/approval_gates.js');\n userApprovalWantsGate = requiresApproval(handler.name);\n } catch (approvalErr) {\n // Approval gates module unavailable → log + fall through.\n // Classifier still gets to decide. Fail-open behavior preserved.\n logger.warn(COMPONENT, `Approval-config probe failed (fail-open): ${(approvalErr as Error).message}`);\n }\n\n // Step 2 — file an approval if EITHER the user's list demands it\n // OR the classifier says 'gate'. Belt + suspenders, with the user\n // explicitly first.\n if (userApprovalWantsGate || classification.decision === 'gate') {\n try {\n const { createApprovalRequest } = await import('../skills/builtin/approval_gates.js');\n const gateReason = userApprovalWantsGate\n ? 'user approval policy'\n : `classifier policy=${classification.policy} risk=${classification.riskLevel ?? 'unknown'}`;\n logger.info(COMPONENT, `[ApprovalGate] Tool \"${handler.name}\" requires human approval (${gateReason}) — filing request`);\n const request = createApprovalRequest(handler.name, args, sessionId || toolCall.id);\n if (request.status === 'pending') {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Awaiting approval: Tool \"${handler.name}\" requires human confirmation before execution. ` +\n `Request ID: ${request.id}. ` +\n `Approve with \"approve ${request.id}\" or deny with \"deny ${request.id}\".`,\n success: false,\n approvalPending: true,\n approvalRequestId: request.id,\n durationMs: Date.now() - startTime,\n };\n }\n if (request.status === 'denied') {\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: Tool \"${handler.name}\" was auto-denied by approval policy. ` +\n `Check approval preferences or change the request decision.`,\n success: false,\n durationMs: Date.now() - startTime,\n };\n }\n // status === 'approved' or anything else → fall through to execute().\n } catch (approvalErr) {\n // Approval gates module unavailable → fail-open so the agent\n // doesn't deadlock. We've already warned above.\n logger.warn(COMPONENT, `Approval gate check failed (fail-open): ${(approvalErr as Error).message}`);\n }\n } else if (classification.decision === 'auto') {\n logger.debug(COMPONENT, formatBreadcrumb(handler.name, classification));\n } else if (classification.decision === 'notify') {\n // Passive notification. Future hook point for Mission Canvas\n // toasts / trajectory `note` events. Logged at info level so\n // the autonomous action is visible in titan-gateway.log + the\n // structured log pipeline.\n logger.info(COMPONENT, formatBreadcrumb(handler.name, classification));\n }\n\n const useWorktreeIsolation =\n config.security.useWorktreeIsolation === true &&\n contractToUse?.sideEffects.includes('destructive') === true;\n\n for (; attempt <= (retryEnabled ? maxRetries : 0); attempt++) {\n try {\n // On timeout retry, double the timeout\n const timeout = (attempt > 0 && lastErrorClass === 'timeout') ? baseTimeout * 2 : baseTimeout;\n\n const timeoutController = useWorktreeIsolation ? new AbortController() : undefined;\n const timeoutError = new Error(`Tool \"${handler.name}\" timed out after ${timeout}ms`);\n let timeoutHandle: ReturnType<typeof setTimeout> | undefined;\n let result = await Promise.race([\n useWorktreeIsolation\n ? runInIsolatedWorktree({\n spawnId: toolCall.id,\n args,\n execute: handler.execute,\n baseCwd: process.cwd(),\n signal: timeoutController?.signal,\n })\n : handler.execute(args),\n new Promise<string>((_, reject) => {\n timeoutHandle = setTimeout(() => {\n timeoutController?.abort(timeoutError);\n reject(timeoutError);\n }, timeout);\n }),\n ]).finally(() => {\n if (timeoutHandle) clearTimeout(timeoutHandle);\n });\n\n // Secret exfiltration guard — scan tool output before it leaves\n result = redactSecrets(result);\n\n // v5.0: PII redaction (privacy compliance)\n const config = loadConfig();\n if (config.security?.redactPII) {\n result = scanAndRedactPII(result);\n }\n\n // v5.0: Full exfiltration scan (layer 2-5) when configured\n if (config.security?.secretScan?.level === 'full') {\n const scan = fullExfilScan(result, 'tool_output');\n if (scan.blocked) {\n logger.warn('ToolRunner', `Exfiltration scan blocked ${handler.name}: ${scan.findings.map(f => f.type).join(', ')}`);\n }\n result = scan.redacted;\n }\n\n const durationMs = Date.now() - startTime;\n if (attempt > 0) {\n logger.info(COMPONENT, `Tool ${handler.name} succeeded on retry ${attempt} in ${durationMs}ms`);\n } else {\n logger.info(COMPONENT, `Tool ${handler.name} completed in ${durationMs}ms`);\n }\n\n // G1: Strip base64 image data before size check (prevents token\n // explosion). v6.1.0-alpha.55 — passes tool name so\n // DATA_URL_PRODUCING_TOOLS (download_image) get exempted; for\n // those the data URL IS the point of calling the tool.\n let finalContent = sanitizeBase64(result, handler.name);\n\n // Smart truncation — keep head + tail for large results (TITAN pattern).\n // v6.1.0-alpha.55 — DATA_URL_PRODUCING_TOOLS are exempted because\n // truncating mid-base64 produces an unusable data URL. The Writer\n // would then fall back to hotlinking the source URL, which is\n // exactly what Tony was seeing before alpha.55 (\"alpha.53 didn't\n // work\" — it did, this layer was killing it).\n if (finalContent.length > 30000 && !DATA_URL_PRODUCING_TOOLS.has(handler.name)) {\n const head = finalContent.slice(0, 20000);\n const tail = finalContent.slice(-5000);\n finalContent = head + '\\n\\n[... ' + (finalContent.length - 25000) + ' chars omitted ...]\\n\\n' + tail;\n logger.info(COMPONENT, `Tool ${handler.name} output truncated: ${result.length} → ${finalContent.length} chars`);\n }\n\n // v5.0: Shell hooks — post-tool\n const shellPost = await runPostToolShellHooks(handler.name, args, finalContent, sessionId || toolCall.id, 'default', 0);\n if (shellPost !== undefined) finalContent = shellPost;\n\n // Post-tool hooks — plugins can modify result\n if (toolHookPlugins.length > 0) {\n const hookResult = await runPostTool(toolHookPlugins, handler.name, args, { content: finalContent, success: true, durationMs });\n if (hookResult.modifiedContent !== undefined) finalContent = hookResult.modifiedContent;\n }\n\n // Cache the result for read-only tools (helper self-gates)\n cacheToolResult(handler.name, cacheArgKey, finalContent);\n\n // v4.9.0-local.8: if this write was diverted to staging, record\n // it in the self_mod_pr bundle. Fire-and-forget — must NOT block\n // the tool's return value or the agent loop.\n if (stagedRedirect) {\n (async () => {\n try {\n const { getCurrentSessionId } = await import('./agent.js');\n const { recordStagedWrite } = await import('./selfModStaging.js');\n const sid: string | null = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n await recordStagedWrite({\n sessionId: sid,\n toolName: handler.name,\n stagedPath: stagedRedirect!.stagedPath,\n targetPath: stagedRedirect!.targetPath,\n });\n } catch (err) {\n logger.debug(COMPONENT, `[SelfModStaging] recordStagedWrite failed: ${(err as Error).message}`);\n }\n })();\n }\n\n // Fire-and-forget telemetry + activity log\n (async () => {\n try {\n const { logActivity } = await import('../telemetry/activityLog.js');\n logActivity({ event: 'tool_call', tool: handler.name, channel: channel ?? 'unknown' });\n if (MUTATING_TOOLS.has(handler.name)) {\n logActivity({ event: 'file_edit', tool: handler.name, path: (args.path || args.file_path || args.filePath) as string | undefined });\n }\n if (handler.name === 'web_search') {\n logActivity({ event: 'web_search', query: (args.query || args.q) as string | undefined });\n }\n if (handler.name === 'web_fetch') {\n logActivity({ event: 'web_fetch', url: (args.url || args.target) as string | undefined });\n }\n if (handler.name === 'run_eval' || handler.name === 'eval_suite') {\n logActivity({ event: 'eval_run', suite: (args.suite || args.name) as string | undefined });\n }\n } catch { /* activity log non-critical */ }\n const cfg = loadConfig();\n if (cfg.telemetry?.enabled) {\n try {\n appendFileSync(TELEMETRY_EVENTS_PATH, JSON.stringify({\n event: 'tool_called',\n properties: { toolName: handler.name, success: true, durationMs, channel: channel ?? 'unknown' },\n timestamp: new Date().toISOString(),\n }) + '\\n', 'utf-8');\n } catch { /* non-critical */ }\n }\n // Remote analytics (PostHog + custom collector)\n const { trackToolCall } = await import('../analytics/featureTracker.js');\n const { getCurrentSessionId } = await import('./agent.js').catch(() => ({ getCurrentSessionId: () => null }));\n const sid = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n trackToolCall(handler.name, true, durationMs, undefined, sid ?? undefined);\n })();\n\n // Compute inline diff for file-modifying tools\n let diff: string | undefined;\n if (['write_file', 'edit_file', 'apply_patch'].includes(handler.name) && filePathForDiff && !stagedRedirect) {\n try {\n const postContent = existsSync(filePathForDiff) ? readFileSync(filePathForDiff, 'utf-8') : '';\n diff = computeUnifiedDiff(filePathForDiff, preContent ?? '', postContent);\n } catch { /* ignore diff errors */ }\n }\n\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: stagedRedirect\n ? `${finalContent}\\n\\n[SelfModStaging] Diverted to staging: ${stagedRedirect.stagedPath}. A human approval is pending before this lands at ${stagedRedirect.targetPath}.`\n : finalContent,\n success: true,\n durationMs,\n retryCount: attempt,\n diff,\n };\n } catch (error) {\n lastError = error as Error;\n lastErrorClass = classifyError(lastError, handler.name);\n\n // Don't retry permanent errors\n if (lastErrorClass === 'permanent') {\n break;\n }\n\n // Don't retry if this was the last attempt\n if (attempt >= maxRetries || !retryEnabled) {\n break;\n }\n\n // Exponential backoff: 1s, 2s, 4s (capped at 8s)\n const delay = Math.min(backoffBase * Math.pow(2, attempt), 8000);\n logger.warn(COMPONENT, `Tool ${handler.name} failed (${lastErrorClass}, attempt ${attempt + 1}/${maxRetries + 1}): ${lastError.message} — retrying in ${delay}ms`);\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n\n // All retries exhausted or permanent error\n const durationMs = Date.now() - startTime;\n const errorMsg = lastError?.message || 'Unknown error';\n const retryCount = attempt; // actual number of retries performed (matches success path)\n logger.error(COMPONENT, `Tool ${handler.name} failed (${lastErrorClass}${retryCount > 0 ? `, ${retryCount} retries` : ''}): ${errorMsg}`);\n\n // Fire-and-forget telemetry\n (async () => {\n const cfg = loadConfig();\n if (cfg.telemetry?.enabled) {\n try {\n appendFileSync(TELEMETRY_EVENTS_PATH, JSON.stringify({\n event: 'tool_called',\n properties: { toolName: handler.name, success: false, durationMs, errorClass: lastErrorClass, channel: channel ?? 'unknown' },\n timestamp: new Date().toISOString(),\n }) + '\\n', 'utf-8');\n } catch { /* non-critical */ }\n }\n // Remote analytics (PostHog + custom collector)\n const { trackToolCall } = await import('../analytics/featureTracker.js');\n const { getCurrentSessionId } = await import('./agent.js').catch(() => ({ getCurrentSessionId: () => null }));\n const sid = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n trackToolCall(handler.name, false, durationMs, lastErrorClass, sid ?? undefined);\n })();\n\n return {\n toolCallId: toolCall.id,\n name: handler.name,\n content: `Error: ${errorMsg}`,\n success: false,\n durationMs,\n retryCount,\n errorClass: lastErrorClass,\n };\n}\n\n/** Execute multiple tool calls (in parallel where possible, with write-conflict detection) */\n/**\n * Apply the tool-output cap to every result. v5.8.0 — Anthropic's \"Writing\n * effective tools for AI agents\" recommends a 25k-token cap on tool results\n * with a tool-aware truncation hint. We do it here at the boundary so every\n * caller (agent loop, sub-agent, voice path) benefits without each having\n * to remember to wrap individually.\n * Reference: docs/HARNESS-PATTERNS.md, src/agent/toolOutputCap.ts\n */\nfunction applyOutputCaps(results: ToolResult[]): ToolResult[] {\n return results.map((r) => {\n const capped = capToolOutput(r.name, r.content);\n if (!capped.truncated) return r;\n // Only mutate when truncation actually happened, preserving identity\n // when content was already small.\n logger.info(COMPONENT, `[OutputCap] ${r.name} truncated ${capped.originalSize} → ${capped.content.length} chars`);\n return { ...r, content: capped.content };\n });\n}\n\n/**\n * v6.5 — would this tool trip the approval gate? Mirrors the gate decision made\n * inside executeTool (classifier 'gate' OR the user's explicit approval list)\n * with no side effects. Used by executeTools to decide whether a multi-tool\n * batch must run sequentially so a gated call cannot be bypassed by its siblings.\n */\nasync function toolWouldGate(name: string): Promise<boolean> {\n try {\n if (classifyToolCall(name).decision === 'gate') return true;\n } catch { /* classifier unavailable — fall through */ }\n try {\n const { requiresApproval } = await import('../skills/builtin/approval_gates.js');\n return requiresApproval(name);\n } catch { /* approval module unavailable — fail-open probe */ }\n return false;\n}\n\nexport async function executeTools(toolCalls: ToolCall[], channel?: string): Promise<ToolResult[]> {\n // Single tool — fast path\n if (toolCalls.length <= 1) {\n const results = await Promise.all(toolCalls.map(tc => executeTool(tc, channel)));\n return applyOutputCaps(results);\n }\n\n // v6.5 — approval-gate sibling protection. The per-tool approval gate (in\n // executeTool) only defers the GATED call. Without this, ungated siblings in\n // the same assistant turn execute in parallel BEFORE the agent loop notices\n // the pending approval — so a model (or prompt injection) could co-schedule a\n // destructive gated tool (e.g. shell `rm -rf`) alongside benign calls and have\n // the benign ones run unreviewed. When the batch contains any potentially\n // gated tool, run SEQUENTIALLY and stop at the first call that actually pends\n // approval, holding the remaining siblings unexecuted. Stays correct across\n // the approve→resume cycle: an already-approved tool simply executes (not\n // pending) and the loop continues to its siblings.\n const maybeGated = (await Promise.all(\n toolCalls.map(tc => toolWouldGate(tc.function.name)),\n )).some(Boolean);\n if (maybeGated) {\n const gatedResults: ToolResult[] = [];\n let paused = false;\n for (const tc of toolCalls) {\n if (paused) {\n gatedResults.push({\n toolCallId: tc.id,\n name: tc.function.name,\n content: 'Deferred: held until the human resolves the pending approval for a tool requested in the same turn. Re-issue this call after approval.',\n success: false,\n durationMs: 0,\n });\n continue;\n }\n const r = await executeTool(tc, channel);\n gatedResults.push(r);\n if (r.approvalPending) paused = true;\n }\n return applyOutputCaps(gatedResults);\n }\n\n // Multiple tools — use parallelTools engine with write-conflict detection\n const parallelCalls = toolCalls.map(tc => {\n let args: Record<string, unknown> = {};\n try { args = JSON.parse(tc.function.arguments); } catch { /* use empty */ }\n return { id: tc.id, name: tc.function.name, args };\n });\n\n const executor = async (name: string, args: Record<string, unknown>): Promise<ToolResult> => {\n // Build a synthetic ToolCall for executeTool\n const syntheticTc: ToolCall = {\n id: '',\n type: 'function',\n function: { name, arguments: JSON.stringify(args) },\n };\n return executeTool(syntheticTc, channel);\n };\n\n const parallelResults = await executeToolsParallel<ToolResult>(parallelCalls, executor);\n\n // Map back to ToolResult format with full metadata, then apply output cap.\n const mapped: ToolResult[] = parallelResults.map(pr => ({\n ...pr.content,\n toolCallId: pr.toolCallId,\n name: pr.name,\n }));\n return applyOutputCaps(mapped);\n}\n\n// ── Self-proposal capture helper (v4.8.0) ────────────────────────────────\n\n/**\n * If the current write is happening in an autonomous, Soma-driven session,\n * stash a copy of the written content for specialist review. Silent no-op\n * in all other cases (user-driven edits, non-autonomous mode, or when\n * selfMod.enabled is false in config).\n */\nasync function captureSelfProposalIfApplicable(\n toolName: string,\n args: Record<string, unknown>,\n): Promise<void> {\n // Resolve what we can from the current autonomous context\n const { getCurrentSessionId } = await import('./agent.js').catch(() => ({ getCurrentSessionId: () => null }));\n const sessionId: string | null = typeof getCurrentSessionId === 'function' ? getCurrentSessionId() : null;\n const sessionGoal = getSessionGoal(sessionId);\n const config = loadConfig();\n const autonomous = (config.autonomy?.mode === 'autonomous');\n const goalProposedBy = sessionGoal?.proposedBy ?? null;\n\n if (!shouldCapture({ toolName, autonomous, goalProposedBy })) return;\n\n const filePath = (args.path || args.file_path || args.filePath) as string | undefined;\n const content = (args.content || args.new_text || args.data) as string | undefined;\n if (!filePath || !content) return;\n\n captureWrite({\n toolName,\n filePath,\n content,\n sessionId,\n agentId: null, // filled by downstream if needed\n goalId: sessionGoal?.goalId ?? null,\n goalTitle: sessionGoal?.goalTitle ?? null,\n goalProposedBy,\n });\n}\n"],"mappings":"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","names":["getCurrentSessionId","config","durationMs"]}
@@ -38,7 +38,11 @@ class DiscordChannel extends ChannelAdapter {
38
38
  userId: message.author.id,
39
39
  userName: message.author.username,
40
40
  content: message.content,
41
- groupId: message.guild?.id,
41
+ // v6.5 — for a guild message the reply target is the CHANNEL the
42
+ // message came from (was the GUILD id, which channels.fetch can't
43
+ // resolve — so guild replies silently failed / DM'd the sender).
44
+ // DMs (no guild) leave groupId unset so send() replies via DM.
45
+ groupId: message.guild ? message.channelId : void 0,
42
46
  timestamp: message.createdAt,
43
47
  raw: message
44
48
  };
@@ -69,15 +73,15 @@ class DiscordChannel extends ChannelAdapter {
69
73
  }
70
74
  try {
71
75
  const client = this.client;
72
- if (message.userId) {
73
- const user = await client.users.fetch(message.userId);
74
- const dm = await user.createDM();
75
- await dm.send(message.content);
76
- } else if (message.groupId) {
76
+ if (message.groupId) {
77
77
  const channel = await client.channels.fetch(message.groupId);
78
78
  if (channel?.isTextBased()) {
79
79
  await channel.send(message.content);
80
80
  }
81
+ } else if (message.userId) {
82
+ const user = await client.users.fetch(message.userId);
83
+ const dm = await user.createDM();
84
+ await dm.send(message.content);
81
85
  }
82
86
  } catch (error) {
83
87
  logger.error(COMPONENT, `Send failed: ${error.message}`);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/channels/discord.ts"],"sourcesContent":["/**\n * TITAN — Discord Channel Adapter\n * Connects to Discord using the discord.js library.\n */\nimport { ChannelAdapter, type InboundMessage, type OutboundMessage, type ChannelStatus } from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Discord';\n\nexport class DiscordChannel extends ChannelAdapter {\n readonly name = 'discord';\n readonly displayName = 'Discord';\n private connected = false;\n private client: unknown = null;\n\n async connect(): Promise<void> {\n const config = loadConfig();\n const channelConfig = config.channels.discord;\n\n if (!channelConfig.enabled) {\n logger.info(COMPONENT, 'Discord channel is disabled');\n return;\n }\n\n const token = channelConfig.token;\n if (!token) {\n logger.warn(COMPONENT, 'Discord token not configured');\n return;\n }\n\n try {\n // Dynamic import to avoid requiring discord.js when not used\n // @ts-expect-error — discord.js is an optional dependency\n const { Client, GatewayIntentBits, Events } = await import('discord.js');\n\n const client = new Client({\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.DirectMessages,\n GatewayIntentBits.MessageContent,\n ],\n });\n\n client.on(Events.MessageCreate, (message: { id: string; author: { id: string; username: string; bot: boolean } | null; content: string; guild?: { id: string }; createdAt: Date }) => {\n if (!message.author || message.author.bot) return;\n\n const inbound: InboundMessage = {\n id: message.id,\n channel: 'discord',\n userId: message.author.id,\n userName: message.author.username,\n content: message.content,\n groupId: message.guild?.id,\n timestamp: message.createdAt,\n raw: message,\n };\n\n this.emit('message', inbound);\n });\n\n client.on(Events.ClientReady, () => {\n this.connected = true;\n logger.info(COMPONENT, `Connected as ${(client.user as unknown as Record<string, string>)?.tag}`);\n });\n\n await client.login(token);\n this.client = client;\n } catch (error) {\n logger.error(COMPONENT, `Failed to connect: ${(error as Error).message}`);\n logger.info(COMPONENT, 'Install discord.js with: npm install discord.js');\n }\n }\n\n async disconnect(): Promise<void> {\n if (this.client) {\n (this.client as unknown as { destroy(): void }).destroy();\n this.connected = false;\n logger.info(COMPONENT, 'Disconnected');\n }\n }\n\n async send(message: OutboundMessage): Promise<void> {\n if (!this.client || !this.connected) {\n logger.warn(COMPONENT, 'Not connected, cannot send message');\n return;\n }\n\n try {\n const client = this.client as unknown as { users: { fetch(id: string): Promise<{ createDM(): Promise<{ send(content: string): Promise<void> }> }> }; channels: { fetch(id: string): Promise<{ isTextBased(): boolean; send(content: string): Promise<void> } | null> } };\n if (message.userId) {\n const user = await client.users.fetch(message.userId);\n const dm = await user.createDM();\n await dm.send(message.content);\n } else if (message.groupId) {\n const channel = await client.channels.fetch(message.groupId);\n if (channel?.isTextBased()) {\n await channel.send(message.content);\n }\n }\n } catch (error) {\n logger.error(COMPONENT, `Send failed: ${(error as Error).message}`);\n }\n }\n\n getStatus(): ChannelStatus {\n return {\n name: this.displayName,\n connected: this.connected,\n };\n }\n}\n"],"mappings":";AAIA,SAAS,sBAAqF;AAC9F,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AAEnB,MAAM,YAAY;AAEX,MAAM,uBAAuB,eAAe;AAAA,EACtC,OAAO;AAAA,EACP,cAAc;AAAA,EACf,YAAY;AAAA,EACZ,SAAkB;AAAA,EAE1B,MAAM,UAAyB;AAC3B,UAAM,SAAS,WAAW;AAC1B,UAAM,gBAAgB,OAAO,SAAS;AAEtC,QAAI,CAAC,cAAc,SAAS;AACxB,aAAO,KAAK,WAAW,6BAA6B;AACpD;AAAA,IACJ;AAEA,UAAM,QAAQ,cAAc;AAC5B,QAAI,CAAC,OAAO;AACR,aAAO,KAAK,WAAW,8BAA8B;AACrD;AAAA,IACJ;AAEA,QAAI;AAGA,YAAM,EAAE,QAAQ,mBAAmB,OAAO,IAAI,MAAM,OAAO,YAAY;AAEvE,YAAM,SAAS,IAAI,OAAO;AAAA,QACtB,SAAS;AAAA,UACL,kBAAkB;AAAA,UAClB,kBAAkB;AAAA,UAClB,kBAAkB;AAAA,UAClB,kBAAkB;AAAA,QACtB;AAAA,MACJ,CAAC;AAED,aAAO,GAAG,OAAO,eAAe,CAAC,YAAqJ;AAClL,YAAI,CAAC,QAAQ,UAAU,QAAQ,OAAO,IAAK;AAE3C,cAAM,UAA0B;AAAA,UAC5B,IAAI,QAAQ;AAAA,UACZ,SAAS;AAAA,UACT,QAAQ,QAAQ,OAAO;AAAA,UACvB,UAAU,QAAQ,OAAO;AAAA,UACzB,SAAS,QAAQ;AAAA,UACjB,SAAS,QAAQ,OAAO;AAAA,UACxB,WAAW,QAAQ;AAAA,UACnB,KAAK;AAAA,QACT;AAEA,aAAK,KAAK,WAAW,OAAO;AAAA,MAChC,CAAC;AAED,aAAO,GAAG,OAAO,aAAa,MAAM;AAChC,aAAK,YAAY;AACjB,eAAO,KAAK,WAAW,gBAAiB,OAAO,MAA4C,GAAG,EAAE;AAAA,MACpG,CAAC;AAED,YAAM,OAAO,MAAM,KAAK;AACxB,WAAK,SAAS;AAAA,IAClB,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,sBAAuB,MAAgB,OAAO,EAAE;AACxE,aAAO,KAAK,WAAW,iDAAiD;AAAA,IAC5E;AAAA,EACJ;AAAA,EAEA,MAAM,aAA4B;AAC9B,QAAI,KAAK,QAAQ;AACb,MAAC,KAAK,OAA0C,QAAQ;AACxD,WAAK,YAAY;AACjB,aAAO,KAAK,WAAW,cAAc;AAAA,IACzC;AAAA,EACJ;AAAA,EAEA,MAAM,KAAK,SAAyC;AAChD,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,WAAW;AACjC,aAAO,KAAK,WAAW,oCAAoC;AAC3D;AAAA,IACJ;AAEA,QAAI;AACA,YAAM,SAAS,KAAK;AACpB,UAAI,QAAQ,QAAQ;AAChB,cAAM,OAAO,MAAM,OAAO,MAAM,MAAM,QAAQ,MAAM;AACpD,cAAM,KAAK,MAAM,KAAK,SAAS;AAC/B,cAAM,GAAG,KAAK,QAAQ,OAAO;AAAA,MACjC,WAAW,QAAQ,SAAS;AACxB,cAAM,UAAU,MAAM,OAAO,SAAS,MAAM,QAAQ,OAAO;AAC3D,YAAI,SAAS,YAAY,GAAG;AACxB,gBAAM,QAAQ,KAAK,QAAQ,OAAO;AAAA,QACtC;AAAA,MACJ;AAAA,IACJ,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,gBAAiB,MAAgB,OAAO,EAAE;AAAA,IACtE;AAAA,EACJ;AAAA,EAEA,YAA2B;AACvB,WAAO;AAAA,MACH,MAAM,KAAK;AAAA,MACX,WAAW,KAAK;AAAA,IACpB;AAAA,EACJ;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/channels/discord.ts"],"sourcesContent":["/**\n * TITAN — Discord Channel Adapter\n * Connects to Discord using the discord.js library.\n */\nimport { ChannelAdapter, type InboundMessage, type OutboundMessage, type ChannelStatus } from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Discord';\n\nexport class DiscordChannel extends ChannelAdapter {\n readonly name = 'discord';\n readonly displayName = 'Discord';\n private connected = false;\n private client: unknown = null;\n\n async connect(): Promise<void> {\n const config = loadConfig();\n const channelConfig = config.channels.discord;\n\n if (!channelConfig.enabled) {\n logger.info(COMPONENT, 'Discord channel is disabled');\n return;\n }\n\n const token = channelConfig.token;\n if (!token) {\n logger.warn(COMPONENT, 'Discord token not configured');\n return;\n }\n\n try {\n // Dynamic import to avoid requiring discord.js when not used\n // @ts-expect-error — discord.js is an optional dependency\n const { Client, GatewayIntentBits, Events } = await import('discord.js');\n\n const client = new Client({\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.DirectMessages,\n GatewayIntentBits.MessageContent,\n ],\n });\n\n client.on(Events.MessageCreate, (message: { id: string; author: { id: string; username: string; bot: boolean } | null; content: string; guild?: { id: string }; channelId: string; createdAt: Date }) => {\n if (!message.author || message.author.bot) return;\n\n const inbound: InboundMessage = {\n id: message.id,\n channel: 'discord',\n userId: message.author.id,\n userName: message.author.username,\n content: message.content,\n // v6.5 — for a guild message the reply target is the CHANNEL the\n // message came from (was the GUILD id, which channels.fetch can't\n // resolve — so guild replies silently failed / DM'd the sender).\n // DMs (no guild) leave groupId unset so send() replies via DM.\n groupId: message.guild ? message.channelId : undefined,\n timestamp: message.createdAt,\n raw: message,\n };\n\n this.emit('message', inbound);\n });\n\n client.on(Events.ClientReady, () => {\n this.connected = true;\n logger.info(COMPONENT, `Connected as ${(client.user as unknown as Record<string, string>)?.tag}`);\n });\n\n await client.login(token);\n this.client = client;\n } catch (error) {\n logger.error(COMPONENT, `Failed to connect: ${(error as Error).message}`);\n logger.info(COMPONENT, 'Install discord.js with: npm install discord.js');\n }\n }\n\n async disconnect(): Promise<void> {\n if (this.client) {\n (this.client as unknown as { destroy(): void }).destroy();\n this.connected = false;\n logger.info(COMPONENT, 'Disconnected');\n }\n }\n\n async send(message: OutboundMessage): Promise<void> {\n if (!this.client || !this.connected) {\n logger.warn(COMPONENT, 'Not connected, cannot send message');\n return;\n }\n\n try {\n const client = this.client as unknown as { users: { fetch(id: string): Promise<{ createDM(): Promise<{ send(content: string): Promise<void> }> }> }; channels: { fetch(id: string): Promise<{ isTextBased(): boolean; send(content: string): Promise<void> } | null> } };\n // v6.5 — prefer the group/channel target so guild replies post in the\n // channel; only DM the user when there's no group context (a true DM).\n if (message.groupId) {\n const channel = await client.channels.fetch(message.groupId);\n if (channel?.isTextBased()) {\n await channel.send(message.content);\n }\n } else if (message.userId) {\n const user = await client.users.fetch(message.userId);\n const dm = await user.createDM();\n await dm.send(message.content);\n }\n } catch (error) {\n logger.error(COMPONENT, `Send failed: ${(error as Error).message}`);\n }\n }\n\n getStatus(): ChannelStatus {\n return {\n name: this.displayName,\n connected: this.connected,\n };\n }\n}\n"],"mappings":";AAIA,SAAS,sBAAqF;AAC9F,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AAEnB,MAAM,YAAY;AAEX,MAAM,uBAAuB,eAAe;AAAA,EACtC,OAAO;AAAA,EACP,cAAc;AAAA,EACf,YAAY;AAAA,EACZ,SAAkB;AAAA,EAE1B,MAAM,UAAyB;AAC3B,UAAM,SAAS,WAAW;AAC1B,UAAM,gBAAgB,OAAO,SAAS;AAEtC,QAAI,CAAC,cAAc,SAAS;AACxB,aAAO,KAAK,WAAW,6BAA6B;AACpD;AAAA,IACJ;AAEA,UAAM,QAAQ,cAAc;AAC5B,QAAI,CAAC,OAAO;AACR,aAAO,KAAK,WAAW,8BAA8B;AACrD;AAAA,IACJ;AAEA,QAAI;AAGA,YAAM,EAAE,QAAQ,mBAAmB,OAAO,IAAI,MAAM,OAAO,YAAY;AAEvE,YAAM,SAAS,IAAI,OAAO;AAAA,QACtB,SAAS;AAAA,UACL,kBAAkB;AAAA,UAClB,kBAAkB;AAAA,UAClB,kBAAkB;AAAA,UAClB,kBAAkB;AAAA,QACtB;AAAA,MACJ,CAAC;AAED,aAAO,GAAG,OAAO,eAAe,CAAC,YAAwK;AACrM,YAAI,CAAC,QAAQ,UAAU,QAAQ,OAAO,IAAK;AAE3C,cAAM,UAA0B;AAAA,UAC5B,IAAI,QAAQ;AAAA,UACZ,SAAS;AAAA,UACT,QAAQ,QAAQ,OAAO;AAAA,UACvB,UAAU,QAAQ,OAAO;AAAA,UACzB,SAAS,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,UAKjB,SAAS,QAAQ,QAAQ,QAAQ,YAAY;AAAA,UAC7C,WAAW,QAAQ;AAAA,UACnB,KAAK;AAAA,QACT;AAEA,aAAK,KAAK,WAAW,OAAO;AAAA,MAChC,CAAC;AAED,aAAO,GAAG,OAAO,aAAa,MAAM;AAChC,aAAK,YAAY;AACjB,eAAO,KAAK,WAAW,gBAAiB,OAAO,MAA4C,GAAG,EAAE;AAAA,MACpG,CAAC;AAED,YAAM,OAAO,MAAM,KAAK;AACxB,WAAK,SAAS;AAAA,IAClB,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,sBAAuB,MAAgB,OAAO,EAAE;AACxE,aAAO,KAAK,WAAW,iDAAiD;AAAA,IAC5E;AAAA,EACJ;AAAA,EAEA,MAAM,aAA4B;AAC9B,QAAI,KAAK,QAAQ;AACb,MAAC,KAAK,OAA0C,QAAQ;AACxD,WAAK,YAAY;AACjB,aAAO,KAAK,WAAW,cAAc;AAAA,IACzC;AAAA,EACJ;AAAA,EAEA,MAAM,KAAK,SAAyC;AAChD,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,WAAW;AACjC,aAAO,KAAK,WAAW,oCAAoC;AAC3D;AAAA,IACJ;AAEA,QAAI;AACA,YAAM,SAAS,KAAK;AAGpB,UAAI,QAAQ,SAAS;AACjB,cAAM,UAAU,MAAM,OAAO,SAAS,MAAM,QAAQ,OAAO;AAC3D,YAAI,SAAS,YAAY,GAAG;AACxB,gBAAM,QAAQ,KAAK,QAAQ,OAAO;AAAA,QACtC;AAAA,MACJ,WAAW,QAAQ,QAAQ;AACvB,cAAM,OAAO,MAAM,OAAO,MAAM,MAAM,QAAQ,MAAM;AACpD,cAAM,KAAK,MAAM,KAAK,SAAS;AAC/B,cAAM,GAAG,KAAK,QAAQ,OAAO;AAAA,MACjC;AAAA,IACJ,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,gBAAiB,MAAgB,OAAO,EAAE;AAAA,IACtE;AAAA,EACJ;AAAA,EAEA,YAA2B;AACvB,WAAO;AAAA,MACH,MAAM,KAAK;AAAA,MACX,WAAW,KAAK;AAAA,IACpB;AAAA,EACJ;AACJ;","names":[]}
@@ -20,12 +20,17 @@ class SlackChannel extends ChannelAdapter {
20
20
  return;
21
21
  }
22
22
  try {
23
+ const appToken = channelConfig.appToken;
24
+ if (!appToken) {
25
+ logger.warn(COMPONENT, "Slack Socket Mode requires an app-level token (xapp-\u2026). Set channels.slack.appToken (and token = your xoxb- bot token). Not connecting.");
26
+ return;
27
+ }
23
28
  const { App } = await import("@slack/bolt");
24
29
  const app = new App({
25
30
  token: channelConfig.token,
26
- signingSecret: channelConfig.apiKey || "",
31
+ signingSecret: channelConfig.signingSecret || void 0,
27
32
  socketMode: true,
28
- appToken: channelConfig.apiKey || ""
33
+ appToken
29
34
  });
30
35
  app.message(async ({ message }) => {
31
36
  if (message.subtype) return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/channels/slack.ts"],"sourcesContent":["/**\n * TITAN — Slack Channel Adapter\n */\nimport { ChannelAdapter, type InboundMessage, type OutboundMessage, type ChannelStatus } from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Slack';\n\nexport class SlackChannel extends ChannelAdapter {\n readonly name = 'slack';\n readonly displayName = 'Slack';\n private connected = false;\n private boltClient: unknown = null;\n\n async connect(): Promise<void> {\n const config = loadConfig();\n const channelConfig = config.channels.slack;\n if (!channelConfig.enabled) { logger.info(COMPONENT, 'Slack channel is disabled'); return; }\n if (!channelConfig.token) { logger.warn(COMPONENT, 'Slack token not configured'); return; }\n\n try {\n // Dynamic import to only load Bolt when used\n // @ts-expect-error optional peer dependency — install with: npm install @slack/bolt\n const { App } = await import('@slack/bolt');\n const app = new App({\n token: channelConfig.token,\n signingSecret: channelConfig.apiKey || '',\n socketMode: true,\n appToken: channelConfig.apiKey || '',\n });\n\n app.message(async ({ message }: { message: { subtype?: string; user?: string; ts: string; text?: string; channel?: string } }) => {\n if (message.subtype) return;\n // Attempt to resolve user display name from Slack user info\n let userName: string | undefined;\n try {\n if (app.client && message.user) {\n const info = await app.client.users.info({ user: message.user });\n const infoObj = info as Record<string, unknown>;\n const userObj = (infoObj?.user ?? {}) as Record<string, unknown>;\n const profileObj = (userObj?.profile ?? {}) as Record<string, unknown>;\n userName = (profileObj?.display_name as string)\n || (userObj?.real_name as string)\n || (userObj?.name as string);\n }\n } catch { /* Fallback: userName stays undefined */ }\n const inbound: InboundMessage = {\n id: message.ts, channel: 'slack', userId: message.user || 'unknown',\n userName,\n content: message.text || '', groupId: message.channel,\n timestamp: new Date(parseFloat(message.ts) * 1000), raw: message,\n };\n this.emit('message', inbound);\n });\n\n await app.start();\n this.boltClient = app.client;\n this.connected = true;\n logger.info(COMPONENT, 'Connected to Slack');\n } catch (error) {\n logger.error(COMPONENT, `Failed to connect: ${(error as Error).message}`);\n logger.info(COMPONENT, 'Install Bolt with: npm install @slack/bolt');\n }\n }\n\n async disconnect(): Promise<void> {\n this.connected = false;\n this.boltClient = null;\n }\n\n async send(message: OutboundMessage): Promise<void> {\n if (!this.boltClient) {\n logger.warn(COMPONENT, 'Slack not connected — cannot send message');\n return;\n }\n const channel = message.groupId || message.userId;\n if (!channel) {\n logger.warn(COMPONENT, 'No channel or userId provided for Slack message');\n return;\n }\n try {\n await (this.boltClient as { chat: { postMessage(opts: { channel: string; text: string }): Promise<void> } }).chat.postMessage({ channel, text: message.content });\n logger.debug(COMPONENT, `Sent message to ${channel}`);\n } catch (error) {\n logger.error(COMPONENT, `Failed to send message: ${(error as Error).message}`);\n }\n }\n\n getStatus(): ChannelStatus { return { name: this.displayName, connected: this.connected }; }\n}\n"],"mappings":";AAGA,SAAS,sBAAqF;AAC9F,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AAEnB,MAAM,YAAY;AAEX,MAAM,qBAAqB,eAAe;AAAA,EACpC,OAAO;AAAA,EACP,cAAc;AAAA,EACf,YAAY;AAAA,EACZ,aAAsB;AAAA,EAE9B,MAAM,UAAyB;AAC3B,UAAM,SAAS,WAAW;AAC1B,UAAM,gBAAgB,OAAO,SAAS;AACtC,QAAI,CAAC,cAAc,SAAS;AAAE,aAAO,KAAK,WAAW,2BAA2B;AAAG;AAAA,IAAQ;AAC3F,QAAI,CAAC,cAAc,OAAO;AAAE,aAAO,KAAK,WAAW,4BAA4B;AAAG;AAAA,IAAQ;AAE1F,QAAI;AAGA,YAAM,EAAE,IAAI,IAAI,MAAM,OAAO,aAAa;AAC1C,YAAM,MAAM,IAAI,IAAI;AAAA,QAChB,OAAO,cAAc;AAAA,QACrB,eAAe,cAAc,UAAU;AAAA,QACvC,YAAY;AAAA,QACZ,UAAU,cAAc,UAAU;AAAA,MACtC,CAAC;AAED,UAAI,QAAQ,OAAO,EAAE,QAAQ,MAAqG;AAC9H,YAAI,QAAQ,QAAS;AAErB,YAAI;AACJ,YAAI;AACA,cAAI,IAAI,UAAU,QAAQ,MAAM;AAC5B,kBAAM,OAAO,MAAM,IAAI,OAAO,MAAM,KAAK,EAAE,MAAM,QAAQ,KAAK,CAAC;AAC/D,kBAAM,UAAU;AAChB,kBAAM,UAAW,SAAS,QAAQ,CAAC;AACnC,kBAAM,aAAc,SAAS,WAAW,CAAC;AACzC,uBAAY,YAAY,gBAChB,SAAS,aACT,SAAS;AAAA,UACrB;AAAA,QACJ,QAAQ;AAAA,QAA2C;AACnD,cAAM,UAA0B;AAAA,UAC5B,IAAI,QAAQ;AAAA,UAAI,SAAS;AAAA,UAAS,QAAQ,QAAQ,QAAQ;AAAA,UAC1D;AAAA,UACA,SAAS,QAAQ,QAAQ;AAAA,UAAI,SAAS,QAAQ;AAAA,UAC9C,WAAW,IAAI,KAAK,WAAW,QAAQ,EAAE,IAAI,GAAI;AAAA,UAAG,KAAK;AAAA,QAC7D;AACA,aAAK,KAAK,WAAW,OAAO;AAAA,MAChC,CAAC;AAED,YAAM,IAAI,MAAM;AAChB,WAAK,aAAa,IAAI;AACtB,WAAK,YAAY;AACjB,aAAO,KAAK,WAAW,oBAAoB;AAAA,IAC/C,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,sBAAuB,MAAgB,OAAO,EAAE;AACxE,aAAO,KAAK,WAAW,4CAA4C;AAAA,IACvE;AAAA,EACJ;AAAA,EAEA,MAAM,aAA4B;AAC9B,SAAK,YAAY;AACjB,SAAK,aAAa;AAAA,EACtB;AAAA,EAEA,MAAM,KAAK,SAAyC;AAChD,QAAI,CAAC,KAAK,YAAY;AAClB,aAAO,KAAK,WAAW,gDAA2C;AAClE;AAAA,IACJ;AACA,UAAM,UAAU,QAAQ,WAAW,QAAQ;AAC3C,QAAI,CAAC,SAAS;AACV,aAAO,KAAK,WAAW,iDAAiD;AACxE;AAAA,IACJ;AACA,QAAI;AACA,YAAO,KAAK,WAAiG,KAAK,YAAY,EAAE,SAAS,MAAM,QAAQ,QAAQ,CAAC;AAChK,aAAO,MAAM,WAAW,mBAAmB,OAAO,EAAE;AAAA,IACxD,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,2BAA4B,MAAgB,OAAO,EAAE;AAAA,IACjF;AAAA,EACJ;AAAA,EAEA,YAA2B;AAAE,WAAO,EAAE,MAAM,KAAK,aAAa,WAAW,KAAK,UAAU;AAAA,EAAG;AAC/F;","names":[]}
1
+ {"version":3,"sources":["../../src/channels/slack.ts"],"sourcesContent":["/**\n * TITAN — Slack Channel Adapter\n */\nimport { ChannelAdapter, type InboundMessage, type OutboundMessage, type ChannelStatus } from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Slack';\n\nexport class SlackChannel extends ChannelAdapter {\n readonly name = 'slack';\n readonly displayName = 'Slack';\n private connected = false;\n private boltClient: unknown = null;\n\n async connect(): Promise<void> {\n const config = loadConfig();\n const channelConfig = config.channels.slack;\n if (!channelConfig.enabled) { logger.info(COMPONENT, 'Slack channel is disabled'); return; }\n if (!channelConfig.token) { logger.warn(COMPONENT, 'Slack token not configured'); return; }\n\n try {\n // v6.5 — Socket Mode needs a distinct app-level token (xapp-…). The old\n // code fed both signingSecret and appToken the single apiKey, so Bolt's\n // start() always threw and Slack never connected. Require an explicit\n // appToken and skip honestly (don't pretend) if it's missing.\n const appToken = channelConfig.appToken;\n if (!appToken) {\n logger.warn(COMPONENT, 'Slack Socket Mode requires an app-level token (xapp-…). Set channels.slack.appToken (and token = your xoxb- bot token). Not connecting.');\n return;\n }\n // Dynamic import to only load Bolt when used\n // @ts-expect-error optional peer dependency — install with: npm install @slack/bolt\n const { App } = await import('@slack/bolt');\n const app = new App({\n token: channelConfig.token,\n signingSecret: channelConfig.signingSecret || undefined,\n socketMode: true,\n appToken,\n });\n\n app.message(async ({ message }: { message: { subtype?: string; user?: string; ts: string; text?: string; channel?: string } }) => {\n if (message.subtype) return;\n // Attempt to resolve user display name from Slack user info\n let userName: string | undefined;\n try {\n if (app.client && message.user) {\n const info = await app.client.users.info({ user: message.user });\n const infoObj = info as Record<string, unknown>;\n const userObj = (infoObj?.user ?? {}) as Record<string, unknown>;\n const profileObj = (userObj?.profile ?? {}) as Record<string, unknown>;\n userName = (profileObj?.display_name as string)\n || (userObj?.real_name as string)\n || (userObj?.name as string);\n }\n } catch { /* Fallback: userName stays undefined */ }\n const inbound: InboundMessage = {\n id: message.ts, channel: 'slack', userId: message.user || 'unknown',\n userName,\n content: message.text || '', groupId: message.channel,\n timestamp: new Date(parseFloat(message.ts) * 1000), raw: message,\n };\n this.emit('message', inbound);\n });\n\n await app.start();\n this.boltClient = app.client;\n this.connected = true;\n logger.info(COMPONENT, 'Connected to Slack');\n } catch (error) {\n logger.error(COMPONENT, `Failed to connect: ${(error as Error).message}`);\n logger.info(COMPONENT, 'Install Bolt with: npm install @slack/bolt');\n }\n }\n\n async disconnect(): Promise<void> {\n this.connected = false;\n this.boltClient = null;\n }\n\n async send(message: OutboundMessage): Promise<void> {\n if (!this.boltClient) {\n logger.warn(COMPONENT, 'Slack not connected — cannot send message');\n return;\n }\n const channel = message.groupId || message.userId;\n if (!channel) {\n logger.warn(COMPONENT, 'No channel or userId provided for Slack message');\n return;\n }\n try {\n await (this.boltClient as { chat: { postMessage(opts: { channel: string; text: string }): Promise<void> } }).chat.postMessage({ channel, text: message.content });\n logger.debug(COMPONENT, `Sent message to ${channel}`);\n } catch (error) {\n logger.error(COMPONENT, `Failed to send message: ${(error as Error).message}`);\n }\n }\n\n getStatus(): ChannelStatus { return { name: this.displayName, connected: this.connected }; }\n}\n"],"mappings":";AAGA,SAAS,sBAAqF;AAC9F,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AAEnB,MAAM,YAAY;AAEX,MAAM,qBAAqB,eAAe;AAAA,EACpC,OAAO;AAAA,EACP,cAAc;AAAA,EACf,YAAY;AAAA,EACZ,aAAsB;AAAA,EAE9B,MAAM,UAAyB;AAC3B,UAAM,SAAS,WAAW;AAC1B,UAAM,gBAAgB,OAAO,SAAS;AACtC,QAAI,CAAC,cAAc,SAAS;AAAE,aAAO,KAAK,WAAW,2BAA2B;AAAG;AAAA,IAAQ;AAC3F,QAAI,CAAC,cAAc,OAAO;AAAE,aAAO,KAAK,WAAW,4BAA4B;AAAG;AAAA,IAAQ;AAE1F,QAAI;AAKA,YAAM,WAAW,cAAc;AAC/B,UAAI,CAAC,UAAU;AACX,eAAO,KAAK,WAAW,8IAAyI;AAChK;AAAA,MACJ;AAGA,YAAM,EAAE,IAAI,IAAI,MAAM,OAAO,aAAa;AAC1C,YAAM,MAAM,IAAI,IAAI;AAAA,QAChB,OAAO,cAAc;AAAA,QACrB,eAAe,cAAc,iBAAiB;AAAA,QAC9C,YAAY;AAAA,QACZ;AAAA,MACJ,CAAC;AAED,UAAI,QAAQ,OAAO,EAAE,QAAQ,MAAqG;AAC9H,YAAI,QAAQ,QAAS;AAErB,YAAI;AACJ,YAAI;AACA,cAAI,IAAI,UAAU,QAAQ,MAAM;AAC5B,kBAAM,OAAO,MAAM,IAAI,OAAO,MAAM,KAAK,EAAE,MAAM,QAAQ,KAAK,CAAC;AAC/D,kBAAM,UAAU;AAChB,kBAAM,UAAW,SAAS,QAAQ,CAAC;AACnC,kBAAM,aAAc,SAAS,WAAW,CAAC;AACzC,uBAAY,YAAY,gBAChB,SAAS,aACT,SAAS;AAAA,UACrB;AAAA,QACJ,QAAQ;AAAA,QAA2C;AACnD,cAAM,UAA0B;AAAA,UAC5B,IAAI,QAAQ;AAAA,UAAI,SAAS;AAAA,UAAS,QAAQ,QAAQ,QAAQ;AAAA,UAC1D;AAAA,UACA,SAAS,QAAQ,QAAQ;AAAA,UAAI,SAAS,QAAQ;AAAA,UAC9C,WAAW,IAAI,KAAK,WAAW,QAAQ,EAAE,IAAI,GAAI;AAAA,UAAG,KAAK;AAAA,QAC7D;AACA,aAAK,KAAK,WAAW,OAAO;AAAA,MAChC,CAAC;AAED,YAAM,IAAI,MAAM;AAChB,WAAK,aAAa,IAAI;AACtB,WAAK,YAAY;AACjB,aAAO,KAAK,WAAW,oBAAoB;AAAA,IAC/C,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,sBAAuB,MAAgB,OAAO,EAAE;AACxE,aAAO,KAAK,WAAW,4CAA4C;AAAA,IACvE;AAAA,EACJ;AAAA,EAEA,MAAM,aAA4B;AAC9B,SAAK,YAAY;AACjB,SAAK,aAAa;AAAA,EACtB;AAAA,EAEA,MAAM,KAAK,SAAyC;AAChD,QAAI,CAAC,KAAK,YAAY;AAClB,aAAO,KAAK,WAAW,gDAA2C;AAClE;AAAA,IACJ;AACA,UAAM,UAAU,QAAQ,WAAW,QAAQ;AAC3C,QAAI,CAAC,SAAS;AACV,aAAO,KAAK,WAAW,iDAAiD;AACxE;AAAA,IACJ;AACA,QAAI;AACA,YAAO,KAAK,WAAiG,KAAK,YAAY,EAAE,SAAS,MAAM,QAAQ,QAAQ,CAAC;AAChK,aAAO,MAAM,WAAW,mBAAmB,OAAO,EAAE;AAAA,IACxD,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,2BAA4B,MAAgB,OAAO,EAAE;AAAA,IACjF;AAAA,EACJ;AAAA,EAEA,YAA2B;AAAE,WAAO,EAAE,MAAM,KAAK,aAAa,WAAW,KAAK,UAAU;AAAA,EAAG;AAC/F;","names":[]}
@@ -63,7 +63,7 @@ class TelegramChannel extends ChannelAdapter {
63
63
  async send(message) {
64
64
  if (!this.bot || !this.connected) return;
65
65
  try {
66
- const chatId = message.userId || message.groupId;
66
+ const chatId = message.groupId || message.userId;
67
67
  if (chatId) {
68
68
  await this.bot.api.sendMessage(chatId, message.content, { parse_mode: "Markdown" });
69
69
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/channels/telegram.ts"],"sourcesContent":["/**\n * TITAN — Telegram Channel Adapter\n */\nimport { ChannelAdapter, type InboundMessage, type OutboundMessage, type ChannelStatus } from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Telegram';\n\nexport class TelegramChannel extends ChannelAdapter {\n readonly name = 'telegram';\n readonly displayName = 'Telegram';\n private connected = false;\n private bot: unknown = null;\n\n async connect(): Promise<void> {\n const config = loadConfig();\n const channelConfig = config.channels.telegram;\n\n if (!channelConfig.enabled) {\n logger.info(COMPONENT, 'Telegram channel is disabled');\n return;\n }\n\n const token = channelConfig.token;\n if (!token) {\n logger.warn(COMPONENT, 'Telegram token not configured');\n return;\n }\n\n try {\n // @ts-expect-error — grammy is an optional dependency\n const { Bot } = await import('grammy');\n const bot = new Bot(token);\n\n bot.catch((err: unknown) => {\n logger.error(COMPONENT, `Grammy error: ${(err as Error).message ?? err}`);\n });\n\n bot.on('message:text', (ctx: { from?: { id: number; username?: string; first_name: string }; message: { message_id: number; text: string; date: number }; chat: { type: string; id: number } }) => {\n if (!ctx.from) return;\n try {\n const inbound: InboundMessage = {\n id: String(ctx.message.message_id),\n channel: 'telegram',\n userId: String(ctx.from.id),\n userName: ctx.from.username || ctx.from.first_name,\n content: ctx.message.text,\n groupId: ctx.chat.type !== 'private' ? String(ctx.chat.id) : undefined,\n timestamp: new Date(ctx.message.date * 1000),\n raw: ctx,\n };\n this.emit('message', inbound);\n } catch (error) {\n logger.error(COMPONENT, `Message handler error: ${(error as Error).message}`);\n }\n });\n\n bot.start();\n this.bot = bot;\n this.connected = true;\n logger.info(COMPONENT, 'Connected to Telegram');\n } catch (error) {\n logger.error(COMPONENT, `Failed to connect: ${(error as Error).message}`);\n logger.info(COMPONENT, 'Install grammy with: npm install grammy');\n }\n }\n\n async disconnect(): Promise<void> {\n if (this.bot) {\n (this.bot as unknown as { stop(): void }).stop();\n this.connected = false;\n logger.info(COMPONENT, 'Disconnected');\n }\n }\n\n async send(message: OutboundMessage): Promise<void> {\n if (!this.bot || !this.connected) return;\n try {\n const chatId = message.userId || message.groupId;\n if (chatId) {\n await (this.bot as unknown as { api: { sendMessage(chatId: string, content: string, opts: Record<string, string>): Promise<void> } }).api.sendMessage(chatId, message.content, { parse_mode: 'Markdown' });\n }\n } catch (error) {\n logger.error(COMPONENT, `Send failed: ${(error as Error).message}`);\n }\n }\n\n getStatus(): ChannelStatus {\n return { name: this.displayName, connected: this.connected };\n }\n}\n"],"mappings":";AAGA,SAAS,sBAAqF;AAC9F,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AAEnB,MAAM,YAAY;AAEX,MAAM,wBAAwB,eAAe;AAAA,EACvC,OAAO;AAAA,EACP,cAAc;AAAA,EACf,YAAY;AAAA,EACZ,MAAe;AAAA,EAEvB,MAAM,UAAyB;AAC3B,UAAM,SAAS,WAAW;AAC1B,UAAM,gBAAgB,OAAO,SAAS;AAEtC,QAAI,CAAC,cAAc,SAAS;AACxB,aAAO,KAAK,WAAW,8BAA8B;AACrD;AAAA,IACJ;AAEA,UAAM,QAAQ,cAAc;AAC5B,QAAI,CAAC,OAAO;AACR,aAAO,KAAK,WAAW,+BAA+B;AACtD;AAAA,IACJ;AAEA,QAAI;AAEA,YAAM,EAAE,IAAI,IAAI,MAAM,OAAO,QAAQ;AACrC,YAAM,MAAM,IAAI,IAAI,KAAK;AAEzB,UAAI,MAAM,CAAC,QAAiB;AACxB,eAAO,MAAM,WAAW,iBAAkB,IAAc,WAAW,GAAG,EAAE;AAAA,MAC5E,CAAC;AAED,UAAI,GAAG,gBAAgB,CAAC,QAA2K;AAC/L,YAAI,CAAC,IAAI,KAAM;AACf,YAAI;AACA,gBAAM,UAA0B;AAAA,YAC5B,IAAI,OAAO,IAAI,QAAQ,UAAU;AAAA,YACjC,SAAS;AAAA,YACT,QAAQ,OAAO,IAAI,KAAK,EAAE;AAAA,YAC1B,UAAU,IAAI,KAAK,YAAY,IAAI,KAAK;AAAA,YACxC,SAAS,IAAI,QAAQ;AAAA,YACrB,SAAS,IAAI,KAAK,SAAS,YAAY,OAAO,IAAI,KAAK,EAAE,IAAI;AAAA,YAC7D,WAAW,IAAI,KAAK,IAAI,QAAQ,OAAO,GAAI;AAAA,YAC3C,KAAK;AAAA,UACT;AACA,eAAK,KAAK,WAAW,OAAO;AAAA,QAChC,SAAS,OAAO;AACZ,iBAAO,MAAM,WAAW,0BAA2B,MAAgB,OAAO,EAAE;AAAA,QAChF;AAAA,MACJ,CAAC;AAED,UAAI,MAAM;AACV,WAAK,MAAM;AACX,WAAK,YAAY;AACjB,aAAO,KAAK,WAAW,uBAAuB;AAAA,IAClD,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,sBAAuB,MAAgB,OAAO,EAAE;AACxE,aAAO,KAAK,WAAW,yCAAyC;AAAA,IACpE;AAAA,EACJ;AAAA,EAEA,MAAM,aAA4B;AAC9B,QAAI,KAAK,KAAK;AACV,MAAC,KAAK,IAAoC,KAAK;AAC/C,WAAK,YAAY;AACjB,aAAO,KAAK,WAAW,cAAc;AAAA,IACzC;AAAA,EACJ;AAAA,EAEA,MAAM,KAAK,SAAyC;AAChD,QAAI,CAAC,KAAK,OAAO,CAAC,KAAK,UAAW;AAClC,QAAI;AACA,YAAM,SAAS,QAAQ,UAAU,QAAQ;AACzC,UAAI,QAAQ;AACR,cAAO,KAAK,IAA0H,IAAI,YAAY,QAAQ,QAAQ,SAAS,EAAE,YAAY,WAAW,CAAC;AAAA,MAC7M;AAAA,IACJ,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,gBAAiB,MAAgB,OAAO,EAAE;AAAA,IACtE;AAAA,EACJ;AAAA,EAEA,YAA2B;AACvB,WAAO,EAAE,MAAM,KAAK,aAAa,WAAW,KAAK,UAAU;AAAA,EAC/D;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/channels/telegram.ts"],"sourcesContent":["/**\n * TITAN — Telegram Channel Adapter\n */\nimport { ChannelAdapter, type InboundMessage, type OutboundMessage, type ChannelStatus } from './base.js';\nimport { loadConfig } from '../config/config.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'Telegram';\n\nexport class TelegramChannel extends ChannelAdapter {\n readonly name = 'telegram';\n readonly displayName = 'Telegram';\n private connected = false;\n private bot: unknown = null;\n\n async connect(): Promise<void> {\n const config = loadConfig();\n const channelConfig = config.channels.telegram;\n\n if (!channelConfig.enabled) {\n logger.info(COMPONENT, 'Telegram channel is disabled');\n return;\n }\n\n const token = channelConfig.token;\n if (!token) {\n logger.warn(COMPONENT, 'Telegram token not configured');\n return;\n }\n\n try {\n // @ts-expect-error — grammy is an optional dependency\n const { Bot } = await import('grammy');\n const bot = new Bot(token);\n\n bot.catch((err: unknown) => {\n logger.error(COMPONENT, `Grammy error: ${(err as Error).message ?? err}`);\n });\n\n bot.on('message:text', (ctx: { from?: { id: number; username?: string; first_name: string }; message: { message_id: number; text: string; date: number }; chat: { type: string; id: number } }) => {\n if (!ctx.from) return;\n try {\n const inbound: InboundMessage = {\n id: String(ctx.message.message_id),\n channel: 'telegram',\n userId: String(ctx.from.id),\n userName: ctx.from.username || ctx.from.first_name,\n content: ctx.message.text,\n groupId: ctx.chat.type !== 'private' ? String(ctx.chat.id) : undefined,\n timestamp: new Date(ctx.message.date * 1000),\n raw: ctx,\n };\n this.emit('message', inbound);\n } catch (error) {\n logger.error(COMPONENT, `Message handler error: ${(error as Error).message}`);\n }\n });\n\n bot.start();\n this.bot = bot;\n this.connected = true;\n logger.info(COMPONENT, 'Connected to Telegram');\n } catch (error) {\n logger.error(COMPONENT, `Failed to connect: ${(error as Error).message}`);\n logger.info(COMPONENT, 'Install grammy with: npm install grammy');\n }\n }\n\n async disconnect(): Promise<void> {\n if (this.bot) {\n (this.bot as unknown as { stop(): void }).stop();\n this.connected = false;\n logger.info(COMPONENT, 'Disconnected');\n }\n }\n\n async send(message: OutboundMessage): Promise<void> {\n if (!this.bot || !this.connected) return;\n try {\n // v6.5 — prefer the group chat so a group reply posts in the group,\n // not the triggering user's private DM. For a DM, groupId is unset and\n // userId (== the private chat id) is the correct target.\n const chatId = message.groupId || message.userId;\n if (chatId) {\n await (this.bot as unknown as { api: { sendMessage(chatId: string, content: string, opts: Record<string, string>): Promise<void> } }).api.sendMessage(chatId, message.content, { parse_mode: 'Markdown' });\n }\n } catch (error) {\n logger.error(COMPONENT, `Send failed: ${(error as Error).message}`);\n }\n }\n\n getStatus(): ChannelStatus {\n return { name: this.displayName, connected: this.connected };\n }\n}\n"],"mappings":";AAGA,SAAS,sBAAqF;AAC9F,SAAS,kBAAkB;AAC3B,OAAO,YAAY;AAEnB,MAAM,YAAY;AAEX,MAAM,wBAAwB,eAAe;AAAA,EACvC,OAAO;AAAA,EACP,cAAc;AAAA,EACf,YAAY;AAAA,EACZ,MAAe;AAAA,EAEvB,MAAM,UAAyB;AAC3B,UAAM,SAAS,WAAW;AAC1B,UAAM,gBAAgB,OAAO,SAAS;AAEtC,QAAI,CAAC,cAAc,SAAS;AACxB,aAAO,KAAK,WAAW,8BAA8B;AACrD;AAAA,IACJ;AAEA,UAAM,QAAQ,cAAc;AAC5B,QAAI,CAAC,OAAO;AACR,aAAO,KAAK,WAAW,+BAA+B;AACtD;AAAA,IACJ;AAEA,QAAI;AAEA,YAAM,EAAE,IAAI,IAAI,MAAM,OAAO,QAAQ;AACrC,YAAM,MAAM,IAAI,IAAI,KAAK;AAEzB,UAAI,MAAM,CAAC,QAAiB;AACxB,eAAO,MAAM,WAAW,iBAAkB,IAAc,WAAW,GAAG,EAAE;AAAA,MAC5E,CAAC;AAED,UAAI,GAAG,gBAAgB,CAAC,QAA2K;AAC/L,YAAI,CAAC,IAAI,KAAM;AACf,YAAI;AACA,gBAAM,UAA0B;AAAA,YAC5B,IAAI,OAAO,IAAI,QAAQ,UAAU;AAAA,YACjC,SAAS;AAAA,YACT,QAAQ,OAAO,IAAI,KAAK,EAAE;AAAA,YAC1B,UAAU,IAAI,KAAK,YAAY,IAAI,KAAK;AAAA,YACxC,SAAS,IAAI,QAAQ;AAAA,YACrB,SAAS,IAAI,KAAK,SAAS,YAAY,OAAO,IAAI,KAAK,EAAE,IAAI;AAAA,YAC7D,WAAW,IAAI,KAAK,IAAI,QAAQ,OAAO,GAAI;AAAA,YAC3C,KAAK;AAAA,UACT;AACA,eAAK,KAAK,WAAW,OAAO;AAAA,QAChC,SAAS,OAAO;AACZ,iBAAO,MAAM,WAAW,0BAA2B,MAAgB,OAAO,EAAE;AAAA,QAChF;AAAA,MACJ,CAAC;AAED,UAAI,MAAM;AACV,WAAK,MAAM;AACX,WAAK,YAAY;AACjB,aAAO,KAAK,WAAW,uBAAuB;AAAA,IAClD,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,sBAAuB,MAAgB,OAAO,EAAE;AACxE,aAAO,KAAK,WAAW,yCAAyC;AAAA,IACpE;AAAA,EACJ;AAAA,EAEA,MAAM,aAA4B;AAC9B,QAAI,KAAK,KAAK;AACV,MAAC,KAAK,IAAoC,KAAK;AAC/C,WAAK,YAAY;AACjB,aAAO,KAAK,WAAW,cAAc;AAAA,IACzC;AAAA,EACJ;AAAA,EAEA,MAAM,KAAK,SAAyC;AAChD,QAAI,CAAC,KAAK,OAAO,CAAC,KAAK,UAAW;AAClC,QAAI;AAIA,YAAM,SAAS,QAAQ,WAAW,QAAQ;AAC1C,UAAI,QAAQ;AACR,cAAO,KAAK,IAA0H,IAAI,YAAY,QAAQ,QAAQ,SAAS,EAAE,YAAY,WAAW,CAAC;AAAA,MAC7M;AAAA,IACJ,SAAS,OAAO;AACZ,aAAO,MAAM,WAAW,gBAAiB,MAAgB,OAAO,EAAE;AAAA,IACtE;AAAA,EACJ;AAAA,EAEA,YAA2B;AACvB,WAAO,EAAE,MAAM,KAAK,aAAa,WAAW,KAAK,UAAU;AAAA,EAC/D;AACJ;","names":[]}
@@ -41,6 +41,12 @@ const ChannelConfigSchema = z.object({
41
41
  enabled: z.boolean().default(false),
42
42
  token: z.string().optional(),
43
43
  apiKey: z.string().optional(),
44
+ // v6.5 — Slack Socket Mode needs a distinct app-level token (xapp-…) and an
45
+ // optional signing secret. Previously both were aliased to apiKey, so Bolt
46
+ // always rejected start() and Slack never connected. Optional + ignored by
47
+ // the other adapters.
48
+ appToken: z.string().optional(),
49
+ signingSecret: z.string().optional(),
44
50
  allowFrom: z.array(z.string()).default([]),
45
51
  dmPolicy: z.enum(["pairing", "open", "closed"]).default("pairing")
46
52
  });