@mariozechner/pi-mom 0.11.2 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAMA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAe,YAAY,EAAY,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA4B/C,MAAM,WAAW,WAAW;IAC3B,GAAG,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjG,KAAK,IAAI,IAAI,CAAC;CACd;AA4TD,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,aAAa,GAAG,WAAW,CA+U3E","sourcesContent":["import { Agent, type AgentEvent, ProviderTransport } from \"@mariozechner/pi-agent-core\";\nimport { getModel } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { ChannelInfo, SlackContext, UserInfo } from \"./slack.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { createMomTools, setUploadFunction } from \"./tools/index.js\";\n\n// Hardcoded model for now\nconst model = getModel(\"anthropic\", \"claude-sonnet-4-5\");\n\n/**\n * Convert Date.now() to Slack timestamp format (seconds.microseconds)\n * Uses a monotonic counter to ensure ordering even within the same millisecond\n */\nlet lastTsMs = 0;\nlet tsCounter = 0;\n\nfunction toSlackTs(): string {\n\tconst now = Date.now();\n\tif (now === lastTsMs) {\n\t\t// Same millisecond - increment counter for sub-ms ordering\n\t\ttsCounter++;\n\t} else {\n\t\t// New millisecond - reset counter\n\t\tlastTsMs = now;\n\t\ttsCounter = 0;\n\t}\n\tconst seconds = Math.floor(now / 1000);\n\tconst micros = (now % 1000) * 1000 + tsCounter; // ms to micros + counter\n\treturn `${seconds}.${micros.toString().padStart(6, \"0\")}`;\n}\n\nexport interface AgentRunner {\n\trun(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }>;\n\tabort(): void;\n}\n\nfunction getAnthropicApiKey(): string {\n\tconst key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\tif (!key) {\n\t\tthrow new Error(\"ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY must be set\");\n\t}\n\treturn key;\n}\n\ninterface LogMessage {\n\tdate?: string;\n\tts?: string;\n\tuser?: string;\n\tuserName?: string;\n\ttext?: string;\n\tattachments?: Array<{ local: string }>;\n\tisBot?: boolean;\n}\n\nfunction getRecentMessages(channelDir: string, turnCount: number): string {\n\tconst logPath = join(channelDir, \"log.jsonl\");\n\tif (!existsSync(logPath)) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\tconst content = readFileSync(logPath, \"utf-8\");\n\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\n\tif (lines.length === 0) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\t// Parse all messages and sort by Slack timestamp\n\t// (attachment downloads can cause out-of-order logging)\n\tconst messages: LogMessage[] = [];\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tmessages.push(JSON.parse(line));\n\t\t} catch {}\n\t}\n\tmessages.sort((a, b) => {\n\t\tconst tsA = parseFloat(a.ts || \"0\");\n\t\tconst tsB = parseFloat(b.ts || \"0\");\n\t\treturn tsA - tsB;\n\t});\n\n\t// Group into \"turns\" - a turn is either:\n\t// - A single user message (isBot: false)\n\t// - A sequence of consecutive bot messages (isBot: true) coalesced into one turn\n\t// We walk backwards to get the last N turns\n\tconst turns: LogMessage[][] = [];\n\tlet currentTurn: LogMessage[] = [];\n\tlet lastWasBot: boolean | null = null;\n\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst msg = messages[i];\n\t\tconst isBot = msg.isBot === true;\n\n\t\tif (lastWasBot === null) {\n\t\t\t// First message\n\t\t\tcurrentTurn.unshift(msg);\n\t\t\tlastWasBot = isBot;\n\t\t} else if (isBot && lastWasBot) {\n\t\t\t// Consecutive bot messages - same turn\n\t\t\tcurrentTurn.unshift(msg);\n\t\t} else {\n\t\t\t// Transition - save current turn and start new one\n\t\t\tturns.unshift(currentTurn);\n\t\t\tcurrentTurn = [msg];\n\t\t\tlastWasBot = isBot;\n\n\t\t\t// Stop if we have enough turns\n\t\t\tif (turns.length >= turnCount) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Don't forget the last turn we were building\n\tif (currentTurn.length > 0 && turns.length < turnCount) {\n\t\tturns.unshift(currentTurn);\n\t}\n\n\t// Flatten turns back to messages and format as TSV\n\tconst formatted: string[] = [];\n\tfor (const turn of turns) {\n\t\tfor (const msg of turn) {\n\t\t\tconst date = (msg.date || \"\").substring(0, 19);\n\t\t\tconst user = msg.userName || msg.user || \"\";\n\t\t\tconst text = msg.text || \"\";\n\t\t\tconst attachments = (msg.attachments || []).map((a) => a.local).join(\",\");\n\t\t\tformatted.push(`${date}\\t${user}\\t${text}\\t${attachments}`);\n\t\t}\n\t}\n\n\treturn formatted.join(\"\\n\");\n}\n\nfunction getMemory(channelDir: string): string {\n\tconst parts: string[] = [];\n\n\t// Read workspace-level memory (shared across all channels)\n\tconst workspaceMemoryPath = join(channelDir, \"..\", \"MEMORY.md\");\n\tif (existsSync(workspaceMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(workspaceMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Global Workspace Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read workspace memory\", `${workspaceMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\t// Read channel-specific memory\n\tconst channelMemoryPath = join(channelDir, \"MEMORY.md\");\n\tif (existsSync(channelMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(channelMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Channel-Specific Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read channel memory\", `${channelMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\tif (parts.length === 0) {\n\t\treturn \"(no working memory yet)\";\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\tmemory: string,\n\tsandboxConfig: SandboxConfig,\n\tchannels: ChannelInfo[],\n\tusers: UserInfo[],\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\t// Format channel mappings\n\tconst channelMappings =\n\t\tchannels.length > 0 ? channels.map((c) => `${c.id}\\t#${c.name}`).join(\"\\n\") : \"(no channels loaded)\";\n\n\t// Format user mappings\n\tconst userMappings =\n\t\tusers.length > 0 ? users.map((u) => `${u.id}\\t@${u.userName}\\t${u.displayName}`).join(\"\\n\") : \"(no users loaded)\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Bash working directory: / (use cd or absolute paths)\n- Install tools with: apk add <package>\n- Your changes persist across sessions`\n\t\t: `You are running directly on the host machine.\n- Bash working directory: ${process.cwd()}\n- Be careful with system modifications`;\n\n\tconst currentDate = new Date().toISOString().split(\"T\")[0]; // YYYY-MM-DD\n\tconst currentDateTime = new Date().toISOString(); // Full ISO 8601\n\n\treturn `You are mom, a Slack bot assistant. Be concise. No emojis.\n\n## Context\n- Date: ${currentDate} (${currentDateTime})\n- You receive the last 50 conversation turns. If you need older context, search log.jsonl.\n\n## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).\n\n## Slack IDs\nChannels: ${channelMappings}\n\nUsers: ${userMappings}\n\nWhen mentioning users, use <@username> format (e.g., <@mario>).\n\n## Environment\n${envDescription}\n\n## Workspace Layout\n${workspacePath}/\n├── MEMORY.md # Global memory (all channels)\n├── skills/ # Global CLI tools you create\n└── ${channelId}/ # This channel\n ├── MEMORY.md # Channel-specific memory\n ├── log.jsonl # Full message history\n ├── attachments/ # User-shared files\n ├── scratch/ # Your working directory\n └── skills/ # Channel-specific tools\n\n## Skills (Custom CLI Tools)\nYou can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).\nStore in \\`${workspacePath}/skills/<name>/\\` or \\`${channelPath}/skills/<name>/\\`.\nEach skill needs a \\`SKILL.md\\` documenting usage. Read it before using a skill.\nList skills in global memory so you remember them.\n\n## Memory\nWrite to MEMORY.md files to persist context across conversations.\n- Global (${workspacePath}/MEMORY.md): skills, preferences, project info\n- Channel (${channelPath}/MEMORY.md): channel-specific decisions, ongoing work\nUpdate when you learn something important or when asked to remember something.\n\n### Current Memory\n${memory}\n\n## Log Queries (CRITICAL: limit output to avoid context overflow)\nFormat: \\`{\"date\":\"...\",\"ts\":\"...\",\"user\":\"...\",\"userName\":\"...\",\"text\":\"...\",\"isBot\":false}\\`\nThe log contains user messages AND your tool calls/results. Filter appropriately.\n${isDocker ? \"Install jq: apk add jq\" : \"\"}\n\n**Conversation only (excludes tool calls/results) - use for summaries:**\n\\`\\`\\`bash\n# Recent conversation (no [Tool] or [Tool Result] lines)\ngrep -v '\"text\":\"\\\\[Tool' log.jsonl | tail -30 | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Yesterday's conversation\ngrep '\"date\":\"2025-11-26' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Specific user's messages\ngrep '\"userName\":\"mario\"' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | tail -20 | jq -c '{date: .date[0:19], text}'\n\\`\\`\\`\n\n**Full details (includes tool calls) - use when you need technical context:**\n\\`\\`\\`bash\n# Raw recent entries\ntail -20 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Count all messages\nwc -l log.jsonl\n\\`\\`\\`\n\n## Tools\n- bash: Run shell commands (primary tool). Install packages as needed.\n- read: Read files\n- write: Create/overwrite files \n- edit: Surgical file edits\n- attach: Share files to Slack\n\nEach tool requires a \"label\" parameter (shown to user).\n`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen - 3) + \"...\";\n}\n\nfunction extractToolResultText(result: unknown): string {\n\t// If it's already a string, return it\n\tif (typeof result === \"string\") {\n\t\treturn result;\n\t}\n\n\t// If it's an object with content array (tool result format)\n\tif (\n\t\tresult &&\n\t\ttypeof result === \"object\" &&\n\t\t\"content\" in result &&\n\t\tArray.isArray((result as { content: unknown }).content)\n\t) {\n\t\tconst content = (result as { content: Array<{ type: string; text?: string }> }).content;\n\t\tconst textParts: string[] = [];\n\t\tfor (const part of content) {\n\t\t\tif (part.type === \"text\" && part.text) {\n\t\t\t\ttextParts.push(part.text);\n\t\t\t}\n\t\t}\n\t\tif (textParts.length > 0) {\n\t\t\treturn textParts.join(\"\\n\");\n\t\t}\n\t}\n\n\t// Fallback to JSON\n\treturn JSON.stringify(result);\n}\n\nfunction formatToolArgsForSlack(_toolName: string, args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\tlines.push(value);\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\nexport function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {\n\tlet agent: Agent | null = null;\n\tconst executor = createExecutor(sandboxConfig);\n\n\treturn {\n\t\tasync run(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }> {\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(channelDir, { recursive: true });\n\n\t\t\tconst channelId = ctx.message.channel;\n\t\t\tconst workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, \"\"));\n\t\t\tconst recentMessages = getRecentMessages(channelDir, 50);\n\t\t\tconst memory = getMemory(channelDir);\n\t\t\tconst systemPrompt = buildSystemPrompt(\n\t\t\t\tworkspacePath,\n\t\t\t\tchannelId,\n\t\t\t\tmemory,\n\t\t\t\tsandboxConfig,\n\t\t\t\tctx.channels,\n\t\t\t\tctx.users,\n\t\t\t);\n\n\t\t\t// Debug: log context sizes\n\t\t\tlog.logInfo(\n\t\t\t\t`Context sizes - system: ${systemPrompt.length} chars, messages: ${recentMessages.length} chars, memory: ${memory.length} chars`,\n\t\t\t);\n\t\t\tlog.logInfo(`Channels: ${ctx.channels.length}, Users: ${ctx.users.length}`);\n\n\t\t\t// Set up file upload function for the attach tool\n\t\t\t// For Docker, we need to translate paths back to host\n\t\t\tsetUploadFunction(async (filePath: string, title?: string) => {\n\t\t\t\tconst hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);\n\t\t\t\tawait ctx.uploadFile(hostPath, title);\n\t\t\t});\n\n\t\t\t// Create tools with executor\n\t\t\tconst tools = createMomTools(executor);\n\n\t\t\t// Create ephemeral agent\n\t\t\tagent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt,\n\t\t\t\t\tmodel,\n\t\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\t\ttools,\n\t\t\t\t},\n\t\t\t\ttransport: new ProviderTransport({\n\t\t\t\t\tgetApiKey: async () => getAnthropicApiKey(),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Create logging context\n\t\t\tconst logCtx = {\n\t\t\t\tchannelId: ctx.message.channel,\n\t\t\t\tuserName: ctx.message.userName,\n\t\t\t\tchannelName: ctx.channelName,\n\t\t\t};\n\n\t\t\t// Track pending tool calls to pair args with results and timing\n\t\t\tconst pendingTools = new Map<string, { toolName: string; args: unknown; startTime: number }>();\n\n\t\t\t// Track usage across all assistant messages in this run\n\t\t\tconst totalUsage = {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\tcost: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotal: 0,\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Track stop reason\n\t\t\tlet stopReason = \"stop\";\n\n\t\t\t// Slack message limit is 40,000 characters - split into multiple messages if needed\n\t\t\tconst SLACK_MAX_LENGTH = 40000;\n\t\t\tconst splitForSlack = (text: string): string[] => {\n\t\t\t\tif (text.length <= SLACK_MAX_LENGTH) return [text];\n\t\t\t\tconst parts: string[] = [];\n\t\t\t\tlet remaining = text;\n\t\t\t\tlet partNum = 1;\n\t\t\t\twhile (remaining.length > 0) {\n\t\t\t\t\tconst chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tremaining = remaining.substring(SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tconst suffix = remaining.length > 0 ? `\\n_(continued ${partNum}...)_` : \"\";\n\t\t\t\t\tparts.push(chunk + suffix);\n\t\t\t\t\tpartNum++;\n\t\t\t\t}\n\t\t\t\treturn parts;\n\t\t\t};\n\n\t\t\t// Promise queue to ensure ctx.respond/respondInThread calls execute in order\n\t\t\t// Handles errors gracefully by posting to thread instead of crashing\n\t\t\tconst queue = {\n\t\t\t\tchain: Promise.resolve(),\n\t\t\t\tenqueue(fn: () => Promise<void>, errorContext: string): void {\n\t\t\t\t\tthis.chain = this.chain.then(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait fn();\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\t\tlog.logWarning(`Slack API error (${errorContext})`, errMsg);\n\t\t\t\t\t\t\t// Try to post error to thread, but don't crash if that fails too\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait ctx.respondInThread(`_Error: ${errMsg}_`);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Ignore - we tried our best\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t// Enqueue a message that may need splitting\n\t\t\t\tenqueueMessage(text: string, target: \"main\" | \"thread\", errorContext: string, log = true): void {\n\t\t\t\t\tconst parts = splitForSlack(text);\n\t\t\t\t\tfor (const part of parts) {\n\t\t\t\t\t\tthis.enqueue(\n\t\t\t\t\t\t\t() => (target === \"main\" ? ctx.respond(part, log) : ctx.respondInThread(part)),\n\t\t\t\t\t\t\terrorContext,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tflush(): Promise<void> {\n\t\t\t\t\treturn this.chain;\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Subscribe to events\n\t\t\tagent.subscribe(async (event: AgentEvent) => {\n\t\t\t\tswitch (event.type) {\n\t\t\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t\t\tconst args = event.args as { label?: string };\n\t\t\t\t\t\tconst label = args.label || event.toolName;\n\n\t\t\t\t\t\t// Store args to pair with result later\n\t\t\t\t\t\tpendingTools.set(event.toolCallId, {\n\t\t\t\t\t\t\ttoolName: event.toolName,\n\t\t\t\t\t\t\targs: event.args,\n\t\t\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tlog.logToolStart(logCtx, event.toolName, label, event.args as Record<string, unknown>);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Show label in main message only\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_→ ${label}_`, false), \"tool label\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t\t\tconst resultStr = extractToolResultText(event.result);\n\t\t\t\t\t\tconst pending = pendingTools.get(event.toolCallId);\n\t\t\t\t\t\tpendingTools.delete(event.toolCallId);\n\n\t\t\t\t\t\tconst durationMs = pending ? Date.now() - pending.startTime : 0;\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tlog.logToolError(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlog.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool Result] ${event.toolName}: ${event.isError ? \"ERROR: \" : \"\"}${truncate(resultStr, 1000)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Post args + result together in thread\n\t\t\t\t\t\tconst label = pending?.args ? (pending.args as { label?: string }).label : undefined;\n\t\t\t\t\t\tconst argsFormatted = pending\n\t\t\t\t\t\t\t? formatToolArgsForSlack(event.toolName, pending.args as Record<string, unknown>)\n\t\t\t\t\t\t\t: \"(args not found)\";\n\t\t\t\t\t\tconst duration = (durationMs / 1000).toFixed(1);\n\t\t\t\t\t\tlet threadMessage = `*${event.isError ? \"✗\" : \"✓\"} ${event.toolName}*`;\n\t\t\t\t\t\tif (label) {\n\t\t\t\t\t\t\tthreadMessage += `: ${label}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthreadMessage += ` (${duration}s)\\n`;\n\n\t\t\t\t\t\tif (argsFormatted) {\n\t\t\t\t\t\t\tthreadMessage += \"```\\n\" + argsFormatted + \"\\n```\\n\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthreadMessage += \"*Result:*\\n```\\n\" + resultStr + \"\\n```\";\n\n\t\t\t\t\t\tqueue.enqueueMessage(threadMessage, \"thread\", \"tool result thread\", false);\n\n\t\t\t\t\t\t// Show brief error in main message if failed\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), \"tool error\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_update\": {\n\t\t\t\t\t\t// No longer stream to console - just track that we're streaming\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_start\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tlog.logResponseStart(logCtx);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"message_end\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tconst assistantMsg = event.message as any; // AssistantMessage type\n\n\t\t\t\t\t\t\t// Track stop reason\n\t\t\t\t\t\t\tif (assistantMsg.stopReason) {\n\t\t\t\t\t\t\t\tstopReason = assistantMsg.stopReason;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Accumulate usage\n\t\t\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\t\t\ttotalUsage.input += assistantMsg.usage.input;\n\t\t\t\t\t\t\t\ttotalUsage.output += assistantMsg.usage.output;\n\t\t\t\t\t\t\t\ttotalUsage.cacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.input += assistantMsg.usage.cost.input;\n\t\t\t\t\t\t\t\ttotalUsage.cost.output += assistantMsg.usage.cost.output;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.total += assistantMsg.usage.cost.total;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Extract thinking and text from assistant message\n\t\t\t\t\t\t\tconst content = event.message.content;\n\t\t\t\t\t\t\tconst thinkingParts: string[] = [];\n\t\t\t\t\t\t\tconst textParts: string[] = [];\n\t\t\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\t\t\tif (part.type === \"thinking\") {\n\t\t\t\t\t\t\t\t\tthinkingParts.push(part.thinking);\n\t\t\t\t\t\t\t\t} else if (part.type === \"text\") {\n\t\t\t\t\t\t\t\t\ttextParts.push(part.text);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst text = textParts.join(\"\\n\");\n\n\t\t\t\t\t\t\t// Post thinking to main message and thread\n\t\t\t\t\t\t\tfor (const thinking of thinkingParts) {\n\t\t\t\t\t\t\t\tlog.logThinking(logCtx, thinking);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"main\", \"thinking main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"thread\", \"thinking thread\", false);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Post text to main message and thread\n\t\t\t\t\t\t\tif (text.trim()) {\n\t\t\t\t\t\t\t\tlog.logResponse(logCtx, text);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"main\", \"response main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"thread\", \"response thread\", false);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Run the agent with user's message\n\t\t\t// Prepend recent messages to the user prompt (not system prompt) for better caching\n\t\t\t// The current message is already the last entry in recentMessages\n\t\t\tconst userPrompt =\n\t\t\t\t`Conversation history (last 50 turns). Respond to the last message.\\n` +\n\t\t\t\t`Format: date TAB user TAB text TAB attachments\\n\\n` +\n\t\t\t\trecentMessages;\n\t\t\t// Debug: write full context to file\n\t\t\tconst toolDefs = tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }));\n\t\t\tconst debugPrompt =\n\t\t\t\t`=== SYSTEM PROMPT (${systemPrompt.length} chars) ===\\n\\n${systemPrompt}\\n\\n` +\n\t\t\t\t`=== TOOL DEFINITIONS (${JSON.stringify(toolDefs).length} chars) ===\\n\\n${JSON.stringify(toolDefs, null, 2)}\\n\\n` +\n\t\t\t\t`=== USER PROMPT (${userPrompt.length} chars) ===\\n\\n${userPrompt}`;\n\t\t\tawait writeFile(join(channelDir, \"last_prompt.txt\"), debugPrompt, \"utf-8\");\n\n\t\t\tawait agent.prompt(userPrompt);\n\n\t\t\t// Wait for all queued respond calls to complete\n\t\t\tawait queue.flush();\n\n\t\t\t// Get final assistant message text from agent state and replace main message\n\t\t\tconst messages = agent.state.messages;\n\t\t\tconst lastAssistant = messages.filter((m) => m.role === \"assistant\").pop();\n\t\t\tconst finalText =\n\t\t\t\tlastAssistant?.content\n\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t.join(\"\\n\") || \"\";\n\t\t\tif (finalText.trim()) {\n\t\t\t\ttry {\n\t\t\t\t\t// For the main message, truncate if too long (full text is in thread)\n\t\t\t\t\tconst mainText =\n\t\t\t\t\t\tfinalText.length > SLACK_MAX_LENGTH\n\t\t\t\t\t\t\t? finalText.substring(0, SLACK_MAX_LENGTH - 50) + \"\\n\\n_(see thread for full response)_\"\n\t\t\t\t\t\t\t: finalText;\n\t\t\t\t\tawait ctx.replaceMessage(mainText);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tlog.logWarning(\"Failed to replace message with final text\", errMsg);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log usage summary if there was any usage\n\t\t\tif (totalUsage.cost.total > 0) {\n\t\t\t\tconst summary = log.logUsageSummary(logCtx, totalUsage);\n\t\t\t\tqueue.enqueue(() => ctx.respondInThread(summary), \"usage summary\");\n\t\t\t\tawait queue.flush();\n\t\t\t}\n\n\t\t\treturn { stopReason };\n\t\t},\n\n\t\tabort(): void {\n\t\t\tagent?.abort();\n\t\t},\n\t};\n}\n\n/**\n * Translate container path back to host path for file operations\n */\nfunction translateToHostPath(\n\tcontainerPath: string,\n\tchannelDir: string,\n\tworkspacePath: string,\n\tchannelId: string,\n): string {\n\tif (workspacePath === \"/workspace\") {\n\t\t// Docker mode - translate /workspace/channelId/... to host path\n\t\tconst prefix = `/workspace/${channelId}/`;\n\t\tif (containerPath.startsWith(prefix)) {\n\t\t\treturn join(channelDir, containerPath.slice(prefix.length));\n\t\t}\n\t\t// Maybe it's just /workspace/...\n\t\tif (containerPath.startsWith(\"/workspace/\")) {\n\t\t\treturn join(channelDir, \"..\", containerPath.slice(\"/workspace/\".length));\n\t\t}\n\t}\n\t// Host mode or already a host path\n\treturn containerPath;\n}\n"]}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAMA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAe,YAAY,EAAY,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA4B/C,MAAM,WAAW,WAAW;IAC3B,GAAG,CAAC,GAAG,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjG,KAAK,IAAI,IAAI,CAAC;CACd;AA4TD,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,aAAa,GAAG,WAAW,CAwV3E","sourcesContent":["import { Agent, type AgentEvent, ProviderTransport } from \"@mariozechner/pi-agent-core\";\nimport { getModel } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { ChannelInfo, SlackContext, UserInfo } from \"./slack.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { createMomTools, setUploadFunction } from \"./tools/index.js\";\n\n// Hardcoded model for now\nconst model = getModel(\"anthropic\", \"claude-sonnet-4-5\");\n\n/**\n * Convert Date.now() to Slack timestamp format (seconds.microseconds)\n * Uses a monotonic counter to ensure ordering even within the same millisecond\n */\nlet lastTsMs = 0;\nlet tsCounter = 0;\n\nfunction toSlackTs(): string {\n\tconst now = Date.now();\n\tif (now === lastTsMs) {\n\t\t// Same millisecond - increment counter for sub-ms ordering\n\t\ttsCounter++;\n\t} else {\n\t\t// New millisecond - reset counter\n\t\tlastTsMs = now;\n\t\ttsCounter = 0;\n\t}\n\tconst seconds = Math.floor(now / 1000);\n\tconst micros = (now % 1000) * 1000 + tsCounter; // ms to micros + counter\n\treturn `${seconds}.${micros.toString().padStart(6, \"0\")}`;\n}\n\nexport interface AgentRunner {\n\trun(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }>;\n\tabort(): void;\n}\n\nfunction getAnthropicApiKey(): string {\n\tconst key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\tif (!key) {\n\t\tthrow new Error(\"ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY must be set\");\n\t}\n\treturn key;\n}\n\ninterface LogMessage {\n\tdate?: string;\n\tts?: string;\n\tuser?: string;\n\tuserName?: string;\n\ttext?: string;\n\tattachments?: Array<{ local: string }>;\n\tisBot?: boolean;\n}\n\nfunction getRecentMessages(channelDir: string, turnCount: number): string {\n\tconst logPath = join(channelDir, \"log.jsonl\");\n\tif (!existsSync(logPath)) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\tconst content = readFileSync(logPath, \"utf-8\");\n\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\n\tif (lines.length === 0) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\t// Parse all messages and sort by Slack timestamp\n\t// (attachment downloads can cause out-of-order logging)\n\tconst messages: LogMessage[] = [];\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tmessages.push(JSON.parse(line));\n\t\t} catch {}\n\t}\n\tmessages.sort((a, b) => {\n\t\tconst tsA = parseFloat(a.ts || \"0\");\n\t\tconst tsB = parseFloat(b.ts || \"0\");\n\t\treturn tsA - tsB;\n\t});\n\n\t// Group into \"turns\" - a turn is either:\n\t// - A single user message (isBot: false)\n\t// - A sequence of consecutive bot messages (isBot: true) coalesced into one turn\n\t// We walk backwards to get the last N turns\n\tconst turns: LogMessage[][] = [];\n\tlet currentTurn: LogMessage[] = [];\n\tlet lastWasBot: boolean | null = null;\n\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst msg = messages[i];\n\t\tconst isBot = msg.isBot === true;\n\n\t\tif (lastWasBot === null) {\n\t\t\t// First message\n\t\t\tcurrentTurn.unshift(msg);\n\t\t\tlastWasBot = isBot;\n\t\t} else if (isBot && lastWasBot) {\n\t\t\t// Consecutive bot messages - same turn\n\t\t\tcurrentTurn.unshift(msg);\n\t\t} else {\n\t\t\t// Transition - save current turn and start new one\n\t\t\tturns.unshift(currentTurn);\n\t\t\tcurrentTurn = [msg];\n\t\t\tlastWasBot = isBot;\n\n\t\t\t// Stop if we have enough turns\n\t\t\tif (turns.length >= turnCount) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Don't forget the last turn we were building\n\tif (currentTurn.length > 0 && turns.length < turnCount) {\n\t\tturns.unshift(currentTurn);\n\t}\n\n\t// Flatten turns back to messages and format as TSV\n\tconst formatted: string[] = [];\n\tfor (const turn of turns) {\n\t\tfor (const msg of turn) {\n\t\t\tconst date = (msg.date || \"\").substring(0, 19);\n\t\t\tconst user = msg.userName || msg.user || \"\";\n\t\t\tconst text = msg.text || \"\";\n\t\t\tconst attachments = (msg.attachments || []).map((a) => a.local).join(\",\");\n\t\t\tformatted.push(`${date}\\t${user}\\t${text}\\t${attachments}`);\n\t\t}\n\t}\n\n\treturn formatted.join(\"\\n\");\n}\n\nfunction getMemory(channelDir: string): string {\n\tconst parts: string[] = [];\n\n\t// Read workspace-level memory (shared across all channels)\n\tconst workspaceMemoryPath = join(channelDir, \"..\", \"MEMORY.md\");\n\tif (existsSync(workspaceMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(workspaceMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Global Workspace Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read workspace memory\", `${workspaceMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\t// Read channel-specific memory\n\tconst channelMemoryPath = join(channelDir, \"MEMORY.md\");\n\tif (existsSync(channelMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(channelMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Channel-Specific Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read channel memory\", `${channelMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\tif (parts.length === 0) {\n\t\treturn \"(no working memory yet)\";\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\tmemory: string,\n\tsandboxConfig: SandboxConfig,\n\tchannels: ChannelInfo[],\n\tusers: UserInfo[],\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\t// Format channel mappings\n\tconst channelMappings =\n\t\tchannels.length > 0 ? channels.map((c) => `${c.id}\\t#${c.name}`).join(\"\\n\") : \"(no channels loaded)\";\n\n\t// Format user mappings\n\tconst userMappings =\n\t\tusers.length > 0 ? users.map((u) => `${u.id}\\t@${u.userName}\\t${u.displayName}`).join(\"\\n\") : \"(no users loaded)\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Bash working directory: / (use cd or absolute paths)\n- Install tools with: apk add <package>\n- Your changes persist across sessions`\n\t\t: `You are running directly on the host machine.\n- Bash working directory: ${process.cwd()}\n- Be careful with system modifications`;\n\n\tconst currentDate = new Date().toISOString().split(\"T\")[0]; // YYYY-MM-DD\n\tconst currentDateTime = new Date().toISOString(); // Full ISO 8601\n\n\treturn `You are mom, a Slack bot assistant. Be concise. No emojis.\n\n## Context\n- Date: ${currentDate} (${currentDateTime})\n- You receive the last 50 conversation turns. If you need older context, search log.jsonl.\n\n## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).\n\n## Slack IDs\nChannels: ${channelMappings}\n\nUsers: ${userMappings}\n\nWhen mentioning users, use <@username> format (e.g., <@mario>).\n\n## Environment\n${envDescription}\n\n## Workspace Layout\n${workspacePath}/\n├── MEMORY.md # Global memory (all channels)\n├── skills/ # Global CLI tools you create\n└── ${channelId}/ # This channel\n ├── MEMORY.md # Channel-specific memory\n ├── log.jsonl # Full message history\n ├── attachments/ # User-shared files\n ├── scratch/ # Your working directory\n └── skills/ # Channel-specific tools\n\n## Skills (Custom CLI Tools)\nYou can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).\nStore in \\`${workspacePath}/skills/<name>/\\` or \\`${channelPath}/skills/<name>/\\`.\nEach skill needs a \\`SKILL.md\\` documenting usage. Read it before using a skill.\nList skills in global memory so you remember them.\n\n## Memory\nWrite to MEMORY.md files to persist context across conversations.\n- Global (${workspacePath}/MEMORY.md): skills, preferences, project info\n- Channel (${channelPath}/MEMORY.md): channel-specific decisions, ongoing work\nUpdate when you learn something important or when asked to remember something.\n\n### Current Memory\n${memory}\n\n## Log Queries (CRITICAL: limit output to avoid context overflow)\nFormat: \\`{\"date\":\"...\",\"ts\":\"...\",\"user\":\"...\",\"userName\":\"...\",\"text\":\"...\",\"isBot\":false}\\`\nThe log contains user messages AND your tool calls/results. Filter appropriately.\n${isDocker ? \"Install jq: apk add jq\" : \"\"}\n\n**Conversation only (excludes tool calls/results) - use for summaries:**\n\\`\\`\\`bash\n# Recent conversation (no [Tool] or [Tool Result] lines)\ngrep -v '\"text\":\"\\\\[Tool' log.jsonl | tail -30 | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Yesterday's conversation\ngrep '\"date\":\"2025-11-26' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Specific user's messages\ngrep '\"userName\":\"mario\"' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | tail -20 | jq -c '{date: .date[0:19], text}'\n\\`\\`\\`\n\n**Full details (includes tool calls) - use when you need technical context:**\n\\`\\`\\`bash\n# Raw recent entries\ntail -20 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Count all messages\nwc -l log.jsonl\n\\`\\`\\`\n\n## Tools\n- bash: Run shell commands (primary tool). Install packages as needed.\n- read: Read files\n- write: Create/overwrite files \n- edit: Surgical file edits\n- attach: Share files to Slack\n\nEach tool requires a \"label\" parameter (shown to user).\n`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen - 3) + \"...\";\n}\n\nfunction extractToolResultText(result: unknown): string {\n\t// If it's already a string, return it\n\tif (typeof result === \"string\") {\n\t\treturn result;\n\t}\n\n\t// If it's an object with content array (tool result format)\n\tif (\n\t\tresult &&\n\t\ttypeof result === \"object\" &&\n\t\t\"content\" in result &&\n\t\tArray.isArray((result as { content: unknown }).content)\n\t) {\n\t\tconst content = (result as { content: Array<{ type: string; text?: string }> }).content;\n\t\tconst textParts: string[] = [];\n\t\tfor (const part of content) {\n\t\t\tif (part.type === \"text\" && part.text) {\n\t\t\t\ttextParts.push(part.text);\n\t\t\t}\n\t\t}\n\t\tif (textParts.length > 0) {\n\t\t\treturn textParts.join(\"\\n\");\n\t\t}\n\t}\n\n\t// Fallback to JSON\n\treturn JSON.stringify(result);\n}\n\nfunction formatToolArgsForSlack(_toolName: string, args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\tlines.push(value);\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\nexport function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {\n\tlet agent: Agent | null = null;\n\tconst executor = createExecutor(sandboxConfig);\n\n\treturn {\n\t\tasync run(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }> {\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(channelDir, { recursive: true });\n\n\t\t\tconst channelId = ctx.message.channel;\n\t\t\tconst workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, \"\"));\n\t\t\tconst recentMessagesFromLog = getRecentMessages(channelDir, 50);\n\n\t\t\t// Append the current message (may not be in log yet due to race condition\n\t\t\t// between app_mention and message events)\n\t\t\tconst currentMsgDate = new Date(parseFloat(ctx.message.ts) * 1000).toISOString().substring(0, 19);\n\t\t\tconst currentMsgUser = ctx.message.userName || ctx.message.user;\n\t\t\tconst currentMsgAttachments = ctx.message.attachments.map((a) => a.local).join(\",\");\n\t\t\tconst currentMsgLine = `${currentMsgDate}\\t${currentMsgUser}\\t${ctx.message.rawText}\\t${currentMsgAttachments}`;\n\t\t\tconst recentMessages = recentMessagesFromLog + \"\\n\" + currentMsgLine;\n\n\t\t\tconst memory = getMemory(channelDir);\n\t\t\tconst systemPrompt = buildSystemPrompt(\n\t\t\t\tworkspacePath,\n\t\t\t\tchannelId,\n\t\t\t\tmemory,\n\t\t\t\tsandboxConfig,\n\t\t\t\tctx.channels,\n\t\t\t\tctx.users,\n\t\t\t);\n\n\t\t\t// Debug: log context sizes\n\t\t\tlog.logInfo(\n\t\t\t\t`Context sizes - system: ${systemPrompt.length} chars, messages: ${recentMessages.length} chars, memory: ${memory.length} chars`,\n\t\t\t);\n\t\t\tlog.logInfo(`Channels: ${ctx.channels.length}, Users: ${ctx.users.length}`);\n\n\t\t\t// Set up file upload function for the attach tool\n\t\t\t// For Docker, we need to translate paths back to host\n\t\t\tsetUploadFunction(async (filePath: string, title?: string) => {\n\t\t\t\tconst hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);\n\t\t\t\tawait ctx.uploadFile(hostPath, title);\n\t\t\t});\n\n\t\t\t// Create tools with executor\n\t\t\tconst tools = createMomTools(executor);\n\n\t\t\t// Create ephemeral agent\n\t\t\tagent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt,\n\t\t\t\t\tmodel,\n\t\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\t\ttools,\n\t\t\t\t},\n\t\t\t\ttransport: new ProviderTransport({\n\t\t\t\t\tgetApiKey: async () => getAnthropicApiKey(),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Create logging context\n\t\t\tconst logCtx = {\n\t\t\t\tchannelId: ctx.message.channel,\n\t\t\t\tuserName: ctx.message.userName,\n\t\t\t\tchannelName: ctx.channelName,\n\t\t\t};\n\n\t\t\t// Track pending tool calls to pair args with results and timing\n\t\t\tconst pendingTools = new Map<string, { toolName: string; args: unknown; startTime: number }>();\n\n\t\t\t// Track usage across all assistant messages in this run\n\t\t\tconst totalUsage = {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\tcost: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotal: 0,\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Track stop reason\n\t\t\tlet stopReason = \"stop\";\n\n\t\t\t// Slack message limit is 40,000 characters - split into multiple messages if needed\n\t\t\tconst SLACK_MAX_LENGTH = 40000;\n\t\t\tconst splitForSlack = (text: string): string[] => {\n\t\t\t\tif (text.length <= SLACK_MAX_LENGTH) return [text];\n\t\t\t\tconst parts: string[] = [];\n\t\t\t\tlet remaining = text;\n\t\t\t\tlet partNum = 1;\n\t\t\t\twhile (remaining.length > 0) {\n\t\t\t\t\tconst chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tremaining = remaining.substring(SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tconst suffix = remaining.length > 0 ? `\\n_(continued ${partNum}...)_` : \"\";\n\t\t\t\t\tparts.push(chunk + suffix);\n\t\t\t\t\tpartNum++;\n\t\t\t\t}\n\t\t\t\treturn parts;\n\t\t\t};\n\n\t\t\t// Promise queue to ensure ctx.respond/respondInThread calls execute in order\n\t\t\t// Handles errors gracefully by posting to thread instead of crashing\n\t\t\tconst queue = {\n\t\t\t\tchain: Promise.resolve(),\n\t\t\t\tenqueue(fn: () => Promise<void>, errorContext: string): void {\n\t\t\t\t\tthis.chain = this.chain.then(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait fn();\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\t\tlog.logWarning(`Slack API error (${errorContext})`, errMsg);\n\t\t\t\t\t\t\t// Try to post error to thread, but don't crash if that fails too\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait ctx.respondInThread(`_Error: ${errMsg}_`);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Ignore - we tried our best\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t// Enqueue a message that may need splitting\n\t\t\t\tenqueueMessage(text: string, target: \"main\" | \"thread\", errorContext: string, log = true): void {\n\t\t\t\t\tconst parts = splitForSlack(text);\n\t\t\t\t\tfor (const part of parts) {\n\t\t\t\t\t\tthis.enqueue(\n\t\t\t\t\t\t\t() => (target === \"main\" ? ctx.respond(part, log) : ctx.respondInThread(part)),\n\t\t\t\t\t\t\terrorContext,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tflush(): Promise<void> {\n\t\t\t\t\treturn this.chain;\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Subscribe to events\n\t\t\tagent.subscribe(async (event: AgentEvent) => {\n\t\t\t\tswitch (event.type) {\n\t\t\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t\t\tconst args = event.args as { label?: string };\n\t\t\t\t\t\tconst label = args.label || event.toolName;\n\n\t\t\t\t\t\t// Store args to pair with result later\n\t\t\t\t\t\tpendingTools.set(event.toolCallId, {\n\t\t\t\t\t\t\ttoolName: event.toolName,\n\t\t\t\t\t\t\targs: event.args,\n\t\t\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tlog.logToolStart(logCtx, event.toolName, label, event.args as Record<string, unknown>);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Show label in main message only\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_→ ${label}_`, false), \"tool label\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t\t\tconst resultStr = extractToolResultText(event.result);\n\t\t\t\t\t\tconst pending = pendingTools.get(event.toolCallId);\n\t\t\t\t\t\tpendingTools.delete(event.toolCallId);\n\n\t\t\t\t\t\tconst durationMs = pending ? Date.now() - pending.startTime : 0;\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tlog.logToolError(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlog.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool Result] ${event.toolName}: ${event.isError ? \"ERROR: \" : \"\"}${truncate(resultStr, 1000)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Post args + result together in thread\n\t\t\t\t\t\tconst label = pending?.args ? (pending.args as { label?: string }).label : undefined;\n\t\t\t\t\t\tconst argsFormatted = pending\n\t\t\t\t\t\t\t? formatToolArgsForSlack(event.toolName, pending.args as Record<string, unknown>)\n\t\t\t\t\t\t\t: \"(args not found)\";\n\t\t\t\t\t\tconst duration = (durationMs / 1000).toFixed(1);\n\t\t\t\t\t\tlet threadMessage = `*${event.isError ? \"✗\" : \"✓\"} ${event.toolName}*`;\n\t\t\t\t\t\tif (label) {\n\t\t\t\t\t\t\tthreadMessage += `: ${label}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthreadMessage += ` (${duration}s)\\n`;\n\n\t\t\t\t\t\tif (argsFormatted) {\n\t\t\t\t\t\t\tthreadMessage += \"```\\n\" + argsFormatted + \"\\n```\\n\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthreadMessage += \"*Result:*\\n```\\n\" + resultStr + \"\\n```\";\n\n\t\t\t\t\t\tqueue.enqueueMessage(threadMessage, \"thread\", \"tool result thread\", false);\n\n\t\t\t\t\t\t// Show brief error in main message if failed\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), \"tool error\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_update\": {\n\t\t\t\t\t\t// No longer stream to console - just track that we're streaming\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_start\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tlog.logResponseStart(logCtx);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"message_end\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tconst assistantMsg = event.message as any; // AssistantMessage type\n\n\t\t\t\t\t\t\t// Track stop reason\n\t\t\t\t\t\t\tif (assistantMsg.stopReason) {\n\t\t\t\t\t\t\t\tstopReason = assistantMsg.stopReason;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Accumulate usage\n\t\t\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\t\t\ttotalUsage.input += assistantMsg.usage.input;\n\t\t\t\t\t\t\t\ttotalUsage.output += assistantMsg.usage.output;\n\t\t\t\t\t\t\t\ttotalUsage.cacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.input += assistantMsg.usage.cost.input;\n\t\t\t\t\t\t\t\ttotalUsage.cost.output += assistantMsg.usage.cost.output;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.total += assistantMsg.usage.cost.total;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Extract thinking and text from assistant message\n\t\t\t\t\t\t\tconst content = event.message.content;\n\t\t\t\t\t\t\tconst thinkingParts: string[] = [];\n\t\t\t\t\t\t\tconst textParts: string[] = [];\n\t\t\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\t\t\tif (part.type === \"thinking\") {\n\t\t\t\t\t\t\t\t\tthinkingParts.push(part.thinking);\n\t\t\t\t\t\t\t\t} else if (part.type === \"text\") {\n\t\t\t\t\t\t\t\t\ttextParts.push(part.text);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst text = textParts.join(\"\\n\");\n\n\t\t\t\t\t\t\t// Post thinking to main message and thread\n\t\t\t\t\t\t\tfor (const thinking of thinkingParts) {\n\t\t\t\t\t\t\t\tlog.logThinking(logCtx, thinking);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"main\", \"thinking main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"thread\", \"thinking thread\", false);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Post text to main message and thread\n\t\t\t\t\t\t\tif (text.trim()) {\n\t\t\t\t\t\t\t\tlog.logResponse(logCtx, text);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"main\", \"response main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"thread\", \"response thread\", false);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Run the agent with user's message\n\t\t\t// Prepend recent messages to the user prompt (not system prompt) for better caching\n\t\t\t// The current message is already the last entry in recentMessages\n\t\t\tconst userPrompt =\n\t\t\t\t`Conversation history (last 50 turns). Respond to the last message.\\n` +\n\t\t\t\t`Format: date TAB user TAB text TAB attachments\\n\\n` +\n\t\t\t\trecentMessages;\n\t\t\t// Debug: write full context to file\n\t\t\tconst toolDefs = tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }));\n\t\t\tconst debugPrompt =\n\t\t\t\t`=== SYSTEM PROMPT (${systemPrompt.length} chars) ===\\n\\n${systemPrompt}\\n\\n` +\n\t\t\t\t`=== TOOL DEFINITIONS (${JSON.stringify(toolDefs).length} chars) ===\\n\\n${JSON.stringify(toolDefs, null, 2)}\\n\\n` +\n\t\t\t\t`=== USER PROMPT (${userPrompt.length} chars) ===\\n\\n${userPrompt}`;\n\t\t\tawait writeFile(join(channelDir, \"last_prompt.txt\"), debugPrompt, \"utf-8\");\n\n\t\t\tawait agent.prompt(userPrompt);\n\n\t\t\t// Wait for all queued respond calls to complete\n\t\t\tawait queue.flush();\n\n\t\t\t// Get final assistant message text from agent state and replace main message\n\t\t\tconst messages = agent.state.messages;\n\t\t\tconst lastAssistant = messages.filter((m) => m.role === \"assistant\").pop();\n\t\t\tconst finalText =\n\t\t\t\tlastAssistant?.content\n\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t.join(\"\\n\") || \"\";\n\t\t\tif (finalText.trim()) {\n\t\t\t\ttry {\n\t\t\t\t\t// For the main message, truncate if too long (full text is in thread)\n\t\t\t\t\tconst mainText =\n\t\t\t\t\t\tfinalText.length > SLACK_MAX_LENGTH\n\t\t\t\t\t\t\t? finalText.substring(0, SLACK_MAX_LENGTH - 50) + \"\\n\\n_(see thread for full response)_\"\n\t\t\t\t\t\t\t: finalText;\n\t\t\t\t\tawait ctx.replaceMessage(mainText);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tlog.logWarning(\"Failed to replace message with final text\", errMsg);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log usage summary if there was any usage\n\t\t\tif (totalUsage.cost.total > 0) {\n\t\t\t\tconst summary = log.logUsageSummary(logCtx, totalUsage);\n\t\t\t\tqueue.enqueue(() => ctx.respondInThread(summary), \"usage summary\");\n\t\t\t\tawait queue.flush();\n\t\t\t}\n\n\t\t\treturn { stopReason };\n\t\t},\n\n\t\tabort(): void {\n\t\t\tagent?.abort();\n\t\t},\n\t};\n}\n\n/**\n * Translate container path back to host path for file operations\n */\nfunction translateToHostPath(\n\tcontainerPath: string,\n\tchannelDir: string,\n\tworkspacePath: string,\n\tchannelId: string,\n): string {\n\tif (workspacePath === \"/workspace\") {\n\t\t// Docker mode - translate /workspace/channelId/... to host path\n\t\tconst prefix = `/workspace/${channelId}/`;\n\t\tif (containerPath.startsWith(prefix)) {\n\t\t\treturn join(channelDir, containerPath.slice(prefix.length));\n\t\t}\n\t\t// Maybe it's just /workspace/...\n\t\tif (containerPath.startsWith(\"/workspace/\")) {\n\t\t\treturn join(channelDir, \"..\", containerPath.slice(\"/workspace/\".length));\n\t\t}\n\t}\n\t// Host mode or already a host path\n\treturn containerPath;\n}\n"]}
package/dist/agent.js CHANGED
@@ -308,7 +308,14 @@ export function createAgentRunner(sandboxConfig) {
308
308
  await mkdir(channelDir, { recursive: true });
309
309
  const channelId = ctx.message.channel;
310
310
  const workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, ""));
311
- const recentMessages = getRecentMessages(channelDir, 50);
311
+ const recentMessagesFromLog = getRecentMessages(channelDir, 50);
312
+ // Append the current message (may not be in log yet due to race condition
313
+ // between app_mention and message events)
314
+ const currentMsgDate = new Date(parseFloat(ctx.message.ts) * 1000).toISOString().substring(0, 19);
315
+ const currentMsgUser = ctx.message.userName || ctx.message.user;
316
+ const currentMsgAttachments = ctx.message.attachments.map((a) => a.local).join(",");
317
+ const currentMsgLine = `${currentMsgDate}\t${currentMsgUser}\t${ctx.message.rawText}\t${currentMsgAttachments}`;
318
+ const recentMessages = recentMessagesFromLog + "\n" + currentMsgLine;
312
319
  const memory = getMemory(channelDir);
313
320
  const systemPrompt = buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, ctx.channels, ctx.users);
314
321
  // Debug: log context sizes
package/dist/agent.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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","sourcesContent":["import { Agent, type AgentEvent, ProviderTransport } from \"@mariozechner/pi-agent-core\";\nimport { getModel } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { ChannelInfo, SlackContext, UserInfo } from \"./slack.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { createMomTools, setUploadFunction } from \"./tools/index.js\";\n\n// Hardcoded model for now\nconst model = getModel(\"anthropic\", \"claude-sonnet-4-5\");\n\n/**\n * Convert Date.now() to Slack timestamp format (seconds.microseconds)\n * Uses a monotonic counter to ensure ordering even within the same millisecond\n */\nlet lastTsMs = 0;\nlet tsCounter = 0;\n\nfunction toSlackTs(): string {\n\tconst now = Date.now();\n\tif (now === lastTsMs) {\n\t\t// Same millisecond - increment counter for sub-ms ordering\n\t\ttsCounter++;\n\t} else {\n\t\t// New millisecond - reset counter\n\t\tlastTsMs = now;\n\t\ttsCounter = 0;\n\t}\n\tconst seconds = Math.floor(now / 1000);\n\tconst micros = (now % 1000) * 1000 + tsCounter; // ms to micros + counter\n\treturn `${seconds}.${micros.toString().padStart(6, \"0\")}`;\n}\n\nexport interface AgentRunner {\n\trun(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }>;\n\tabort(): void;\n}\n\nfunction getAnthropicApiKey(): string {\n\tconst key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\tif (!key) {\n\t\tthrow new Error(\"ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY must be set\");\n\t}\n\treturn key;\n}\n\ninterface LogMessage {\n\tdate?: string;\n\tts?: string;\n\tuser?: string;\n\tuserName?: string;\n\ttext?: string;\n\tattachments?: Array<{ local: string }>;\n\tisBot?: boolean;\n}\n\nfunction getRecentMessages(channelDir: string, turnCount: number): string {\n\tconst logPath = join(channelDir, \"log.jsonl\");\n\tif (!existsSync(logPath)) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\tconst content = readFileSync(logPath, \"utf-8\");\n\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\n\tif (lines.length === 0) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\t// Parse all messages and sort by Slack timestamp\n\t// (attachment downloads can cause out-of-order logging)\n\tconst messages: LogMessage[] = [];\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tmessages.push(JSON.parse(line));\n\t\t} catch {}\n\t}\n\tmessages.sort((a, b) => {\n\t\tconst tsA = parseFloat(a.ts || \"0\");\n\t\tconst tsB = parseFloat(b.ts || \"0\");\n\t\treturn tsA - tsB;\n\t});\n\n\t// Group into \"turns\" - a turn is either:\n\t// - A single user message (isBot: false)\n\t// - A sequence of consecutive bot messages (isBot: true) coalesced into one turn\n\t// We walk backwards to get the last N turns\n\tconst turns: LogMessage[][] = [];\n\tlet currentTurn: LogMessage[] = [];\n\tlet lastWasBot: boolean | null = null;\n\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst msg = messages[i];\n\t\tconst isBot = msg.isBot === true;\n\n\t\tif (lastWasBot === null) {\n\t\t\t// First message\n\t\t\tcurrentTurn.unshift(msg);\n\t\t\tlastWasBot = isBot;\n\t\t} else if (isBot && lastWasBot) {\n\t\t\t// Consecutive bot messages - same turn\n\t\t\tcurrentTurn.unshift(msg);\n\t\t} else {\n\t\t\t// Transition - save current turn and start new one\n\t\t\tturns.unshift(currentTurn);\n\t\t\tcurrentTurn = [msg];\n\t\t\tlastWasBot = isBot;\n\n\t\t\t// Stop if we have enough turns\n\t\t\tif (turns.length >= turnCount) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Don't forget the last turn we were building\n\tif (currentTurn.length > 0 && turns.length < turnCount) {\n\t\tturns.unshift(currentTurn);\n\t}\n\n\t// Flatten turns back to messages and format as TSV\n\tconst formatted: string[] = [];\n\tfor (const turn of turns) {\n\t\tfor (const msg of turn) {\n\t\t\tconst date = (msg.date || \"\").substring(0, 19);\n\t\t\tconst user = msg.userName || msg.user || \"\";\n\t\t\tconst text = msg.text || \"\";\n\t\t\tconst attachments = (msg.attachments || []).map((a) => a.local).join(\",\");\n\t\t\tformatted.push(`${date}\\t${user}\\t${text}\\t${attachments}`);\n\t\t}\n\t}\n\n\treturn formatted.join(\"\\n\");\n}\n\nfunction getMemory(channelDir: string): string {\n\tconst parts: string[] = [];\n\n\t// Read workspace-level memory (shared across all channels)\n\tconst workspaceMemoryPath = join(channelDir, \"..\", \"MEMORY.md\");\n\tif (existsSync(workspaceMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(workspaceMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Global Workspace Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read workspace memory\", `${workspaceMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\t// Read channel-specific memory\n\tconst channelMemoryPath = join(channelDir, \"MEMORY.md\");\n\tif (existsSync(channelMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(channelMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Channel-Specific Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read channel memory\", `${channelMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\tif (parts.length === 0) {\n\t\treturn \"(no working memory yet)\";\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\tmemory: string,\n\tsandboxConfig: SandboxConfig,\n\tchannels: ChannelInfo[],\n\tusers: UserInfo[],\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\t// Format channel mappings\n\tconst channelMappings =\n\t\tchannels.length > 0 ? channels.map((c) => `${c.id}\\t#${c.name}`).join(\"\\n\") : \"(no channels loaded)\";\n\n\t// Format user mappings\n\tconst userMappings =\n\t\tusers.length > 0 ? users.map((u) => `${u.id}\\t@${u.userName}\\t${u.displayName}`).join(\"\\n\") : \"(no users loaded)\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Bash working directory: / (use cd or absolute paths)\n- Install tools with: apk add <package>\n- Your changes persist across sessions`\n\t\t: `You are running directly on the host machine.\n- Bash working directory: ${process.cwd()}\n- Be careful with system modifications`;\n\n\tconst currentDate = new Date().toISOString().split(\"T\")[0]; // YYYY-MM-DD\n\tconst currentDateTime = new Date().toISOString(); // Full ISO 8601\n\n\treturn `You are mom, a Slack bot assistant. Be concise. No emojis.\n\n## Context\n- Date: ${currentDate} (${currentDateTime})\n- You receive the last 50 conversation turns. If you need older context, search log.jsonl.\n\n## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).\n\n## Slack IDs\nChannels: ${channelMappings}\n\nUsers: ${userMappings}\n\nWhen mentioning users, use <@username> format (e.g., <@mario>).\n\n## Environment\n${envDescription}\n\n## Workspace Layout\n${workspacePath}/\n├── MEMORY.md # Global memory (all channels)\n├── skills/ # Global CLI tools you create\n└── ${channelId}/ # This channel\n ├── MEMORY.md # Channel-specific memory\n ├── log.jsonl # Full message history\n ├── attachments/ # User-shared files\n ├── scratch/ # Your working directory\n └── skills/ # Channel-specific tools\n\n## Skills (Custom CLI Tools)\nYou can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).\nStore in \\`${workspacePath}/skills/<name>/\\` or \\`${channelPath}/skills/<name>/\\`.\nEach skill needs a \\`SKILL.md\\` documenting usage. Read it before using a skill.\nList skills in global memory so you remember them.\n\n## Memory\nWrite to MEMORY.md files to persist context across conversations.\n- Global (${workspacePath}/MEMORY.md): skills, preferences, project info\n- Channel (${channelPath}/MEMORY.md): channel-specific decisions, ongoing work\nUpdate when you learn something important or when asked to remember something.\n\n### Current Memory\n${memory}\n\n## Log Queries (CRITICAL: limit output to avoid context overflow)\nFormat: \\`{\"date\":\"...\",\"ts\":\"...\",\"user\":\"...\",\"userName\":\"...\",\"text\":\"...\",\"isBot\":false}\\`\nThe log contains user messages AND your tool calls/results. Filter appropriately.\n${isDocker ? \"Install jq: apk add jq\" : \"\"}\n\n**Conversation only (excludes tool calls/results) - use for summaries:**\n\\`\\`\\`bash\n# Recent conversation (no [Tool] or [Tool Result] lines)\ngrep -v '\"text\":\"\\\\[Tool' log.jsonl | tail -30 | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Yesterday's conversation\ngrep '\"date\":\"2025-11-26' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Specific user's messages\ngrep '\"userName\":\"mario\"' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | tail -20 | jq -c '{date: .date[0:19], text}'\n\\`\\`\\`\n\n**Full details (includes tool calls) - use when you need technical context:**\n\\`\\`\\`bash\n# Raw recent entries\ntail -20 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Count all messages\nwc -l log.jsonl\n\\`\\`\\`\n\n## Tools\n- bash: Run shell commands (primary tool). Install packages as needed.\n- read: Read files\n- write: Create/overwrite files \n- edit: Surgical file edits\n- attach: Share files to Slack\n\nEach tool requires a \"label\" parameter (shown to user).\n`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen - 3) + \"...\";\n}\n\nfunction extractToolResultText(result: unknown): string {\n\t// If it's already a string, return it\n\tif (typeof result === \"string\") {\n\t\treturn result;\n\t}\n\n\t// If it's an object with content array (tool result format)\n\tif (\n\t\tresult &&\n\t\ttypeof result === \"object\" &&\n\t\t\"content\" in result &&\n\t\tArray.isArray((result as { content: unknown }).content)\n\t) {\n\t\tconst content = (result as { content: Array<{ type: string; text?: string }> }).content;\n\t\tconst textParts: string[] = [];\n\t\tfor (const part of content) {\n\t\t\tif (part.type === \"text\" && part.text) {\n\t\t\t\ttextParts.push(part.text);\n\t\t\t}\n\t\t}\n\t\tif (textParts.length > 0) {\n\t\t\treturn textParts.join(\"\\n\");\n\t\t}\n\t}\n\n\t// Fallback to JSON\n\treturn JSON.stringify(result);\n}\n\nfunction formatToolArgsForSlack(_toolName: string, args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\tlines.push(value);\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\nexport function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {\n\tlet agent: Agent | null = null;\n\tconst executor = createExecutor(sandboxConfig);\n\n\treturn {\n\t\tasync run(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }> {\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(channelDir, { recursive: true });\n\n\t\t\tconst channelId = ctx.message.channel;\n\t\t\tconst workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, \"\"));\n\t\t\tconst recentMessages = getRecentMessages(channelDir, 50);\n\t\t\tconst memory = getMemory(channelDir);\n\t\t\tconst systemPrompt = buildSystemPrompt(\n\t\t\t\tworkspacePath,\n\t\t\t\tchannelId,\n\t\t\t\tmemory,\n\t\t\t\tsandboxConfig,\n\t\t\t\tctx.channels,\n\t\t\t\tctx.users,\n\t\t\t);\n\n\t\t\t// Debug: log context sizes\n\t\t\tlog.logInfo(\n\t\t\t\t`Context sizes - system: ${systemPrompt.length} chars, messages: ${recentMessages.length} chars, memory: ${memory.length} chars`,\n\t\t\t);\n\t\t\tlog.logInfo(`Channels: ${ctx.channels.length}, Users: ${ctx.users.length}`);\n\n\t\t\t// Set up file upload function for the attach tool\n\t\t\t// For Docker, we need to translate paths back to host\n\t\t\tsetUploadFunction(async (filePath: string, title?: string) => {\n\t\t\t\tconst hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);\n\t\t\t\tawait ctx.uploadFile(hostPath, title);\n\t\t\t});\n\n\t\t\t// Create tools with executor\n\t\t\tconst tools = createMomTools(executor);\n\n\t\t\t// Create ephemeral agent\n\t\t\tagent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt,\n\t\t\t\t\tmodel,\n\t\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\t\ttools,\n\t\t\t\t},\n\t\t\t\ttransport: new ProviderTransport({\n\t\t\t\t\tgetApiKey: async () => getAnthropicApiKey(),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Create logging context\n\t\t\tconst logCtx = {\n\t\t\t\tchannelId: ctx.message.channel,\n\t\t\t\tuserName: ctx.message.userName,\n\t\t\t\tchannelName: ctx.channelName,\n\t\t\t};\n\n\t\t\t// Track pending tool calls to pair args with results and timing\n\t\t\tconst pendingTools = new Map<string, { toolName: string; args: unknown; startTime: number }>();\n\n\t\t\t// Track usage across all assistant messages in this run\n\t\t\tconst totalUsage = {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\tcost: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotal: 0,\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Track stop reason\n\t\t\tlet stopReason = \"stop\";\n\n\t\t\t// Slack message limit is 40,000 characters - split into multiple messages if needed\n\t\t\tconst SLACK_MAX_LENGTH = 40000;\n\t\t\tconst splitForSlack = (text: string): string[] => {\n\t\t\t\tif (text.length <= SLACK_MAX_LENGTH) return [text];\n\t\t\t\tconst parts: string[] = [];\n\t\t\t\tlet remaining = text;\n\t\t\t\tlet partNum = 1;\n\t\t\t\twhile (remaining.length > 0) {\n\t\t\t\t\tconst chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tremaining = remaining.substring(SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tconst suffix = remaining.length > 0 ? `\\n_(continued ${partNum}...)_` : \"\";\n\t\t\t\t\tparts.push(chunk + suffix);\n\t\t\t\t\tpartNum++;\n\t\t\t\t}\n\t\t\t\treturn parts;\n\t\t\t};\n\n\t\t\t// Promise queue to ensure ctx.respond/respondInThread calls execute in order\n\t\t\t// Handles errors gracefully by posting to thread instead of crashing\n\t\t\tconst queue = {\n\t\t\t\tchain: Promise.resolve(),\n\t\t\t\tenqueue(fn: () => Promise<void>, errorContext: string): void {\n\t\t\t\t\tthis.chain = this.chain.then(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait fn();\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\t\tlog.logWarning(`Slack API error (${errorContext})`, errMsg);\n\t\t\t\t\t\t\t// Try to post error to thread, but don't crash if that fails too\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait ctx.respondInThread(`_Error: ${errMsg}_`);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Ignore - we tried our best\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t// Enqueue a message that may need splitting\n\t\t\t\tenqueueMessage(text: string, target: \"main\" | \"thread\", errorContext: string, log = true): void {\n\t\t\t\t\tconst parts = splitForSlack(text);\n\t\t\t\t\tfor (const part of parts) {\n\t\t\t\t\t\tthis.enqueue(\n\t\t\t\t\t\t\t() => (target === \"main\" ? ctx.respond(part, log) : ctx.respondInThread(part)),\n\t\t\t\t\t\t\terrorContext,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tflush(): Promise<void> {\n\t\t\t\t\treturn this.chain;\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Subscribe to events\n\t\t\tagent.subscribe(async (event: AgentEvent) => {\n\t\t\t\tswitch (event.type) {\n\t\t\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t\t\tconst args = event.args as { label?: string };\n\t\t\t\t\t\tconst label = args.label || event.toolName;\n\n\t\t\t\t\t\t// Store args to pair with result later\n\t\t\t\t\t\tpendingTools.set(event.toolCallId, {\n\t\t\t\t\t\t\ttoolName: event.toolName,\n\t\t\t\t\t\t\targs: event.args,\n\t\t\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tlog.logToolStart(logCtx, event.toolName, label, event.args as Record<string, unknown>);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Show label in main message only\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_→ ${label}_`, false), \"tool label\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t\t\tconst resultStr = extractToolResultText(event.result);\n\t\t\t\t\t\tconst pending = pendingTools.get(event.toolCallId);\n\t\t\t\t\t\tpendingTools.delete(event.toolCallId);\n\n\t\t\t\t\t\tconst durationMs = pending ? Date.now() - pending.startTime : 0;\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tlog.logToolError(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlog.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool Result] ${event.toolName}: ${event.isError ? \"ERROR: \" : \"\"}${truncate(resultStr, 1000)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Post args + result together in thread\n\t\t\t\t\t\tconst label = pending?.args ? (pending.args as { label?: string }).label : undefined;\n\t\t\t\t\t\tconst argsFormatted = pending\n\t\t\t\t\t\t\t? formatToolArgsForSlack(event.toolName, pending.args as Record<string, unknown>)\n\t\t\t\t\t\t\t: \"(args not found)\";\n\t\t\t\t\t\tconst duration = (durationMs / 1000).toFixed(1);\n\t\t\t\t\t\tlet threadMessage = `*${event.isError ? \"✗\" : \"✓\"} ${event.toolName}*`;\n\t\t\t\t\t\tif (label) {\n\t\t\t\t\t\t\tthreadMessage += `: ${label}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthreadMessage += ` (${duration}s)\\n`;\n\n\t\t\t\t\t\tif (argsFormatted) {\n\t\t\t\t\t\t\tthreadMessage += \"```\\n\" + argsFormatted + \"\\n```\\n\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthreadMessage += \"*Result:*\\n```\\n\" + resultStr + \"\\n```\";\n\n\t\t\t\t\t\tqueue.enqueueMessage(threadMessage, \"thread\", \"tool result thread\", false);\n\n\t\t\t\t\t\t// Show brief error in main message if failed\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), \"tool error\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_update\": {\n\t\t\t\t\t\t// No longer stream to console - just track that we're streaming\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_start\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tlog.logResponseStart(logCtx);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"message_end\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tconst assistantMsg = event.message as any; // AssistantMessage type\n\n\t\t\t\t\t\t\t// Track stop reason\n\t\t\t\t\t\t\tif (assistantMsg.stopReason) {\n\t\t\t\t\t\t\t\tstopReason = assistantMsg.stopReason;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Accumulate usage\n\t\t\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\t\t\ttotalUsage.input += assistantMsg.usage.input;\n\t\t\t\t\t\t\t\ttotalUsage.output += assistantMsg.usage.output;\n\t\t\t\t\t\t\t\ttotalUsage.cacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.input += assistantMsg.usage.cost.input;\n\t\t\t\t\t\t\t\ttotalUsage.cost.output += assistantMsg.usage.cost.output;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.total += assistantMsg.usage.cost.total;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Extract thinking and text from assistant message\n\t\t\t\t\t\t\tconst content = event.message.content;\n\t\t\t\t\t\t\tconst thinkingParts: string[] = [];\n\t\t\t\t\t\t\tconst textParts: string[] = [];\n\t\t\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\t\t\tif (part.type === \"thinking\") {\n\t\t\t\t\t\t\t\t\tthinkingParts.push(part.thinking);\n\t\t\t\t\t\t\t\t} else if (part.type === \"text\") {\n\t\t\t\t\t\t\t\t\ttextParts.push(part.text);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst text = textParts.join(\"\\n\");\n\n\t\t\t\t\t\t\t// Post thinking to main message and thread\n\t\t\t\t\t\t\tfor (const thinking of thinkingParts) {\n\t\t\t\t\t\t\t\tlog.logThinking(logCtx, thinking);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"main\", \"thinking main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"thread\", \"thinking thread\", false);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Post text to main message and thread\n\t\t\t\t\t\t\tif (text.trim()) {\n\t\t\t\t\t\t\t\tlog.logResponse(logCtx, text);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"main\", \"response main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"thread\", \"response thread\", false);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Run the agent with user's message\n\t\t\t// Prepend recent messages to the user prompt (not system prompt) for better caching\n\t\t\t// The current message is already the last entry in recentMessages\n\t\t\tconst userPrompt =\n\t\t\t\t`Conversation history (last 50 turns). Respond to the last message.\\n` +\n\t\t\t\t`Format: date TAB user TAB text TAB attachments\\n\\n` +\n\t\t\t\trecentMessages;\n\t\t\t// Debug: write full context to file\n\t\t\tconst toolDefs = tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }));\n\t\t\tconst debugPrompt =\n\t\t\t\t`=== SYSTEM PROMPT (${systemPrompt.length} chars) ===\\n\\n${systemPrompt}\\n\\n` +\n\t\t\t\t`=== TOOL DEFINITIONS (${JSON.stringify(toolDefs).length} chars) ===\\n\\n${JSON.stringify(toolDefs, null, 2)}\\n\\n` +\n\t\t\t\t`=== USER PROMPT (${userPrompt.length} chars) ===\\n\\n${userPrompt}`;\n\t\t\tawait writeFile(join(channelDir, \"last_prompt.txt\"), debugPrompt, \"utf-8\");\n\n\t\t\tawait agent.prompt(userPrompt);\n\n\t\t\t// Wait for all queued respond calls to complete\n\t\t\tawait queue.flush();\n\n\t\t\t// Get final assistant message text from agent state and replace main message\n\t\t\tconst messages = agent.state.messages;\n\t\t\tconst lastAssistant = messages.filter((m) => m.role === \"assistant\").pop();\n\t\t\tconst finalText =\n\t\t\t\tlastAssistant?.content\n\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t.join(\"\\n\") || \"\";\n\t\t\tif (finalText.trim()) {\n\t\t\t\ttry {\n\t\t\t\t\t// For the main message, truncate if too long (full text is in thread)\n\t\t\t\t\tconst mainText =\n\t\t\t\t\t\tfinalText.length > SLACK_MAX_LENGTH\n\t\t\t\t\t\t\t? finalText.substring(0, SLACK_MAX_LENGTH - 50) + \"\\n\\n_(see thread for full response)_\"\n\t\t\t\t\t\t\t: finalText;\n\t\t\t\t\tawait ctx.replaceMessage(mainText);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tlog.logWarning(\"Failed to replace message with final text\", errMsg);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log usage summary if there was any usage\n\t\t\tif (totalUsage.cost.total > 0) {\n\t\t\t\tconst summary = log.logUsageSummary(logCtx, totalUsage);\n\t\t\t\tqueue.enqueue(() => ctx.respondInThread(summary), \"usage summary\");\n\t\t\t\tawait queue.flush();\n\t\t\t}\n\n\t\t\treturn { stopReason };\n\t\t},\n\n\t\tabort(): void {\n\t\t\tagent?.abort();\n\t\t},\n\t};\n}\n\n/**\n * Translate container path back to host path for file operations\n */\nfunction translateToHostPath(\n\tcontainerPath: string,\n\tchannelDir: string,\n\tworkspacePath: string,\n\tchannelId: string,\n): string {\n\tif (workspacePath === \"/workspace\") {\n\t\t// Docker mode - translate /workspace/channelId/... to host path\n\t\tconst prefix = `/workspace/${channelId}/`;\n\t\tif (containerPath.startsWith(prefix)) {\n\t\t\treturn join(channelDir, containerPath.slice(prefix.length));\n\t\t}\n\t\t// Maybe it's just /workspace/...\n\t\tif (containerPath.startsWith(\"/workspace/\")) {\n\t\t\treturn join(channelDir, \"..\", containerPath.slice(\"/workspace/\".length));\n\t\t}\n\t}\n\t// Host mode or already a host path\n\treturn containerPath;\n}\n"]}
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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","sourcesContent":["import { Agent, type AgentEvent, ProviderTransport } from \"@mariozechner/pi-agent-core\";\nimport { getModel } from \"@mariozechner/pi-ai\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport * as log from \"./log.js\";\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { ChannelInfo, SlackContext, UserInfo } from \"./slack.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { createMomTools, setUploadFunction } from \"./tools/index.js\";\n\n// Hardcoded model for now\nconst model = getModel(\"anthropic\", \"claude-sonnet-4-5\");\n\n/**\n * Convert Date.now() to Slack timestamp format (seconds.microseconds)\n * Uses a monotonic counter to ensure ordering even within the same millisecond\n */\nlet lastTsMs = 0;\nlet tsCounter = 0;\n\nfunction toSlackTs(): string {\n\tconst now = Date.now();\n\tif (now === lastTsMs) {\n\t\t// Same millisecond - increment counter for sub-ms ordering\n\t\ttsCounter++;\n\t} else {\n\t\t// New millisecond - reset counter\n\t\tlastTsMs = now;\n\t\ttsCounter = 0;\n\t}\n\tconst seconds = Math.floor(now / 1000);\n\tconst micros = (now % 1000) * 1000 + tsCounter; // ms to micros + counter\n\treturn `${seconds}.${micros.toString().padStart(6, \"0\")}`;\n}\n\nexport interface AgentRunner {\n\trun(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }>;\n\tabort(): void;\n}\n\nfunction getAnthropicApiKey(): string {\n\tconst key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;\n\tif (!key) {\n\t\tthrow new Error(\"ANTHROPIC_OAUTH_TOKEN or ANTHROPIC_API_KEY must be set\");\n\t}\n\treturn key;\n}\n\ninterface LogMessage {\n\tdate?: string;\n\tts?: string;\n\tuser?: string;\n\tuserName?: string;\n\ttext?: string;\n\tattachments?: Array<{ local: string }>;\n\tisBot?: boolean;\n}\n\nfunction getRecentMessages(channelDir: string, turnCount: number): string {\n\tconst logPath = join(channelDir, \"log.jsonl\");\n\tif (!existsSync(logPath)) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\tconst content = readFileSync(logPath, \"utf-8\");\n\tconst lines = content.trim().split(\"\\n\").filter(Boolean);\n\n\tif (lines.length === 0) {\n\t\treturn \"(no message history yet)\";\n\t}\n\n\t// Parse all messages and sort by Slack timestamp\n\t// (attachment downloads can cause out-of-order logging)\n\tconst messages: LogMessage[] = [];\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tmessages.push(JSON.parse(line));\n\t\t} catch {}\n\t}\n\tmessages.sort((a, b) => {\n\t\tconst tsA = parseFloat(a.ts || \"0\");\n\t\tconst tsB = parseFloat(b.ts || \"0\");\n\t\treturn tsA - tsB;\n\t});\n\n\t// Group into \"turns\" - a turn is either:\n\t// - A single user message (isBot: false)\n\t// - A sequence of consecutive bot messages (isBot: true) coalesced into one turn\n\t// We walk backwards to get the last N turns\n\tconst turns: LogMessage[][] = [];\n\tlet currentTurn: LogMessage[] = [];\n\tlet lastWasBot: boolean | null = null;\n\n\tfor (let i = messages.length - 1; i >= 0; i--) {\n\t\tconst msg = messages[i];\n\t\tconst isBot = msg.isBot === true;\n\n\t\tif (lastWasBot === null) {\n\t\t\t// First message\n\t\t\tcurrentTurn.unshift(msg);\n\t\t\tlastWasBot = isBot;\n\t\t} else if (isBot && lastWasBot) {\n\t\t\t// Consecutive bot messages - same turn\n\t\t\tcurrentTurn.unshift(msg);\n\t\t} else {\n\t\t\t// Transition - save current turn and start new one\n\t\t\tturns.unshift(currentTurn);\n\t\t\tcurrentTurn = [msg];\n\t\t\tlastWasBot = isBot;\n\n\t\t\t// Stop if we have enough turns\n\t\t\tif (turns.length >= turnCount) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Don't forget the last turn we were building\n\tif (currentTurn.length > 0 && turns.length < turnCount) {\n\t\tturns.unshift(currentTurn);\n\t}\n\n\t// Flatten turns back to messages and format as TSV\n\tconst formatted: string[] = [];\n\tfor (const turn of turns) {\n\t\tfor (const msg of turn) {\n\t\t\tconst date = (msg.date || \"\").substring(0, 19);\n\t\t\tconst user = msg.userName || msg.user || \"\";\n\t\t\tconst text = msg.text || \"\";\n\t\t\tconst attachments = (msg.attachments || []).map((a) => a.local).join(\",\");\n\t\t\tformatted.push(`${date}\\t${user}\\t${text}\\t${attachments}`);\n\t\t}\n\t}\n\n\treturn formatted.join(\"\\n\");\n}\n\nfunction getMemory(channelDir: string): string {\n\tconst parts: string[] = [];\n\n\t// Read workspace-level memory (shared across all channels)\n\tconst workspaceMemoryPath = join(channelDir, \"..\", \"MEMORY.md\");\n\tif (existsSync(workspaceMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(workspaceMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Global Workspace Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read workspace memory\", `${workspaceMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\t// Read channel-specific memory\n\tconst channelMemoryPath = join(channelDir, \"MEMORY.md\");\n\tif (existsSync(channelMemoryPath)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(channelMemoryPath, \"utf-8\").trim();\n\t\t\tif (content) {\n\t\t\t\tparts.push(\"### Channel-Specific Memory\\n\" + content);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlog.logWarning(\"Failed to read channel memory\", `${channelMemoryPath}: ${error}`);\n\t\t}\n\t}\n\n\tif (parts.length === 0) {\n\t\treturn \"(no working memory yet)\";\n\t}\n\n\treturn parts.join(\"\\n\\n\");\n}\n\nfunction buildSystemPrompt(\n\tworkspacePath: string,\n\tchannelId: string,\n\tmemory: string,\n\tsandboxConfig: SandboxConfig,\n\tchannels: ChannelInfo[],\n\tusers: UserInfo[],\n): string {\n\tconst channelPath = `${workspacePath}/${channelId}`;\n\tconst isDocker = sandboxConfig.type === \"docker\";\n\n\t// Format channel mappings\n\tconst channelMappings =\n\t\tchannels.length > 0 ? channels.map((c) => `${c.id}\\t#${c.name}`).join(\"\\n\") : \"(no channels loaded)\";\n\n\t// Format user mappings\n\tconst userMappings =\n\t\tusers.length > 0 ? users.map((u) => `${u.id}\\t@${u.userName}\\t${u.displayName}`).join(\"\\n\") : \"(no users loaded)\";\n\n\tconst envDescription = isDocker\n\t\t? `You are running inside a Docker container (Alpine Linux).\n- Bash working directory: / (use cd or absolute paths)\n- Install tools with: apk add <package>\n- Your changes persist across sessions`\n\t\t: `You are running directly on the host machine.\n- Bash working directory: ${process.cwd()}\n- Be careful with system modifications`;\n\n\tconst currentDate = new Date().toISOString().split(\"T\")[0]; // YYYY-MM-DD\n\tconst currentDateTime = new Date().toISOString(); // Full ISO 8601\n\n\treturn `You are mom, a Slack bot assistant. Be concise. No emojis.\n\n## Context\n- Date: ${currentDate} (${currentDateTime})\n- You receive the last 50 conversation turns. If you need older context, search log.jsonl.\n\n## Slack Formatting (mrkdwn, NOT Markdown)\nBold: *text*, Italic: _text_, Code: \\`code\\`, Block: \\`\\`\\`code\\`\\`\\`, Links: <url|text>\nDo NOT use **double asterisks** or [markdown](links).\n\n## Slack IDs\nChannels: ${channelMappings}\n\nUsers: ${userMappings}\n\nWhen mentioning users, use <@username> format (e.g., <@mario>).\n\n## Environment\n${envDescription}\n\n## Workspace Layout\n${workspacePath}/\n├── MEMORY.md # Global memory (all channels)\n├── skills/ # Global CLI tools you create\n└── ${channelId}/ # This channel\n ├── MEMORY.md # Channel-specific memory\n ├── log.jsonl # Full message history\n ├── attachments/ # User-shared files\n ├── scratch/ # Your working directory\n └── skills/ # Channel-specific tools\n\n## Skills (Custom CLI Tools)\nYou can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).\nStore in \\`${workspacePath}/skills/<name>/\\` or \\`${channelPath}/skills/<name>/\\`.\nEach skill needs a \\`SKILL.md\\` documenting usage. Read it before using a skill.\nList skills in global memory so you remember them.\n\n## Memory\nWrite to MEMORY.md files to persist context across conversations.\n- Global (${workspacePath}/MEMORY.md): skills, preferences, project info\n- Channel (${channelPath}/MEMORY.md): channel-specific decisions, ongoing work\nUpdate when you learn something important or when asked to remember something.\n\n### Current Memory\n${memory}\n\n## Log Queries (CRITICAL: limit output to avoid context overflow)\nFormat: \\`{\"date\":\"...\",\"ts\":\"...\",\"user\":\"...\",\"userName\":\"...\",\"text\":\"...\",\"isBot\":false}\\`\nThe log contains user messages AND your tool calls/results. Filter appropriately.\n${isDocker ? \"Install jq: apk add jq\" : \"\"}\n\n**Conversation only (excludes tool calls/results) - use for summaries:**\n\\`\\`\\`bash\n# Recent conversation (no [Tool] or [Tool Result] lines)\ngrep -v '\"text\":\"\\\\[Tool' log.jsonl | tail -30 | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Yesterday's conversation\ngrep '\"date\":\"2025-11-26' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Specific user's messages\ngrep '\"userName\":\"mario\"' log.jsonl | grep -v '\"text\":\"\\\\[Tool' | tail -20 | jq -c '{date: .date[0:19], text}'\n\\`\\`\\`\n\n**Full details (includes tool calls) - use when you need technical context:**\n\\`\\`\\`bash\n# Raw recent entries\ntail -20 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'\n\n# Count all messages\nwc -l log.jsonl\n\\`\\`\\`\n\n## Tools\n- bash: Run shell commands (primary tool). Install packages as needed.\n- read: Read files\n- write: Create/overwrite files \n- edit: Surgical file edits\n- attach: Share files to Slack\n\nEach tool requires a \"label\" parameter (shown to user).\n`;\n}\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn text.substring(0, maxLen - 3) + \"...\";\n}\n\nfunction extractToolResultText(result: unknown): string {\n\t// If it's already a string, return it\n\tif (typeof result === \"string\") {\n\t\treturn result;\n\t}\n\n\t// If it's an object with content array (tool result format)\n\tif (\n\t\tresult &&\n\t\ttypeof result === \"object\" &&\n\t\t\"content\" in result &&\n\t\tArray.isArray((result as { content: unknown }).content)\n\t) {\n\t\tconst content = (result as { content: Array<{ type: string; text?: string }> }).content;\n\t\tconst textParts: string[] = [];\n\t\tfor (const part of content) {\n\t\t\tif (part.type === \"text\" && part.text) {\n\t\t\t\ttextParts.push(part.text);\n\t\t\t}\n\t\t}\n\t\tif (textParts.length > 0) {\n\t\t\treturn textParts.join(\"\\n\");\n\t\t}\n\t}\n\n\t// Fallback to JSON\n\treturn JSON.stringify(result);\n}\n\nfunction formatToolArgsForSlack(_toolName: string, args: Record<string, unknown>): string {\n\tconst lines: string[] = [];\n\n\tfor (const [key, value] of Object.entries(args)) {\n\t\t// Skip the label - it's already shown\n\t\tif (key === \"label\") continue;\n\n\t\t// For read tool, format path with offset/limit\n\t\tif (key === \"path\" && typeof value === \"string\") {\n\t\t\tconst offset = args.offset as number | undefined;\n\t\t\tconst limit = args.limit as number | undefined;\n\t\t\tif (offset !== undefined && limit !== undefined) {\n\t\t\t\tlines.push(`${value}:${offset}-${offset + limit}`);\n\t\t\t} else {\n\t\t\t\tlines.push(value);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Skip offset/limit since we already handled them\n\t\tif (key === \"offset\" || key === \"limit\") continue;\n\n\t\t// For other values, format them\n\t\tif (typeof value === \"string\") {\n\t\t\tlines.push(value);\n\t\t} else {\n\t\t\tlines.push(JSON.stringify(value));\n\t\t}\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n\nexport function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {\n\tlet agent: Agent | null = null;\n\tconst executor = createExecutor(sandboxConfig);\n\n\treturn {\n\t\tasync run(ctx: SlackContext, channelDir: string, store: ChannelStore): Promise<{ stopReason: string }> {\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(channelDir, { recursive: true });\n\n\t\t\tconst channelId = ctx.message.channel;\n\t\t\tconst workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, \"\"));\n\t\t\tconst recentMessagesFromLog = getRecentMessages(channelDir, 50);\n\n\t\t\t// Append the current message (may not be in log yet due to race condition\n\t\t\t// between app_mention and message events)\n\t\t\tconst currentMsgDate = new Date(parseFloat(ctx.message.ts) * 1000).toISOString().substring(0, 19);\n\t\t\tconst currentMsgUser = ctx.message.userName || ctx.message.user;\n\t\t\tconst currentMsgAttachments = ctx.message.attachments.map((a) => a.local).join(\",\");\n\t\t\tconst currentMsgLine = `${currentMsgDate}\\t${currentMsgUser}\\t${ctx.message.rawText}\\t${currentMsgAttachments}`;\n\t\t\tconst recentMessages = recentMessagesFromLog + \"\\n\" + currentMsgLine;\n\n\t\t\tconst memory = getMemory(channelDir);\n\t\t\tconst systemPrompt = buildSystemPrompt(\n\t\t\t\tworkspacePath,\n\t\t\t\tchannelId,\n\t\t\t\tmemory,\n\t\t\t\tsandboxConfig,\n\t\t\t\tctx.channels,\n\t\t\t\tctx.users,\n\t\t\t);\n\n\t\t\t// Debug: log context sizes\n\t\t\tlog.logInfo(\n\t\t\t\t`Context sizes - system: ${systemPrompt.length} chars, messages: ${recentMessages.length} chars, memory: ${memory.length} chars`,\n\t\t\t);\n\t\t\tlog.logInfo(`Channels: ${ctx.channels.length}, Users: ${ctx.users.length}`);\n\n\t\t\t// Set up file upload function for the attach tool\n\t\t\t// For Docker, we need to translate paths back to host\n\t\t\tsetUploadFunction(async (filePath: string, title?: string) => {\n\t\t\t\tconst hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);\n\t\t\t\tawait ctx.uploadFile(hostPath, title);\n\t\t\t});\n\n\t\t\t// Create tools with executor\n\t\t\tconst tools = createMomTools(executor);\n\n\t\t\t// Create ephemeral agent\n\t\t\tagent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt,\n\t\t\t\t\tmodel,\n\t\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\t\ttools,\n\t\t\t\t},\n\t\t\t\ttransport: new ProviderTransport({\n\t\t\t\t\tgetApiKey: async () => getAnthropicApiKey(),\n\t\t\t\t}),\n\t\t\t});\n\n\t\t\t// Create logging context\n\t\t\tconst logCtx = {\n\t\t\t\tchannelId: ctx.message.channel,\n\t\t\t\tuserName: ctx.message.userName,\n\t\t\t\tchannelName: ctx.channelName,\n\t\t\t};\n\n\t\t\t// Track pending tool calls to pair args with results and timing\n\t\t\tconst pendingTools = new Map<string, { toolName: string; args: unknown; startTime: number }>();\n\n\t\t\t// Track usage across all assistant messages in this run\n\t\t\tconst totalUsage = {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\tcost: {\n\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotal: 0,\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Track stop reason\n\t\t\tlet stopReason = \"stop\";\n\n\t\t\t// Slack message limit is 40,000 characters - split into multiple messages if needed\n\t\t\tconst SLACK_MAX_LENGTH = 40000;\n\t\t\tconst splitForSlack = (text: string): string[] => {\n\t\t\t\tif (text.length <= SLACK_MAX_LENGTH) return [text];\n\t\t\t\tconst parts: string[] = [];\n\t\t\t\tlet remaining = text;\n\t\t\t\tlet partNum = 1;\n\t\t\t\twhile (remaining.length > 0) {\n\t\t\t\t\tconst chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tremaining = remaining.substring(SLACK_MAX_LENGTH - 50);\n\t\t\t\t\tconst suffix = remaining.length > 0 ? `\\n_(continued ${partNum}...)_` : \"\";\n\t\t\t\t\tparts.push(chunk + suffix);\n\t\t\t\t\tpartNum++;\n\t\t\t\t}\n\t\t\t\treturn parts;\n\t\t\t};\n\n\t\t\t// Promise queue to ensure ctx.respond/respondInThread calls execute in order\n\t\t\t// Handles errors gracefully by posting to thread instead of crashing\n\t\t\tconst queue = {\n\t\t\t\tchain: Promise.resolve(),\n\t\t\t\tenqueue(fn: () => Promise<void>, errorContext: string): void {\n\t\t\t\t\tthis.chain = this.chain.then(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait fn();\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\t\tlog.logWarning(`Slack API error (${errorContext})`, errMsg);\n\t\t\t\t\t\t\t// Try to post error to thread, but don't crash if that fails too\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait ctx.respondInThread(`_Error: ${errMsg}_`);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Ignore - we tried our best\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\t// Enqueue a message that may need splitting\n\t\t\t\tenqueueMessage(text: string, target: \"main\" | \"thread\", errorContext: string, log = true): void {\n\t\t\t\t\tconst parts = splitForSlack(text);\n\t\t\t\t\tfor (const part of parts) {\n\t\t\t\t\t\tthis.enqueue(\n\t\t\t\t\t\t\t() => (target === \"main\" ? ctx.respond(part, log) : ctx.respondInThread(part)),\n\t\t\t\t\t\t\terrorContext,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tflush(): Promise<void> {\n\t\t\t\t\treturn this.chain;\n\t\t\t\t},\n\t\t\t};\n\n\t\t\t// Subscribe to events\n\t\t\tagent.subscribe(async (event: AgentEvent) => {\n\t\t\t\tswitch (event.type) {\n\t\t\t\t\tcase \"tool_execution_start\": {\n\t\t\t\t\t\tconst args = event.args as { label?: string };\n\t\t\t\t\t\tconst label = args.label || event.toolName;\n\n\t\t\t\t\t\t// Store args to pair with result later\n\t\t\t\t\t\tpendingTools.set(event.toolCallId, {\n\t\t\t\t\t\t\ttoolName: event.toolName,\n\t\t\t\t\t\t\targs: event.args,\n\t\t\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tlog.logToolStart(logCtx, event.toolName, label, event.args as Record<string, unknown>);\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Show label in main message only\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_→ ${label}_`, false), \"tool label\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"tool_execution_end\": {\n\t\t\t\t\t\tconst resultStr = extractToolResultText(event.result);\n\t\t\t\t\t\tconst pending = pendingTools.get(event.toolCallId);\n\t\t\t\t\t\tpendingTools.delete(event.toolCallId);\n\n\t\t\t\t\t\tconst durationMs = pending ? Date.now() - pending.startTime : 0;\n\n\t\t\t\t\t\t// Log to console\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tlog.logToolError(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tlog.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Log to jsonl\n\t\t\t\t\t\tawait store.logMessage(ctx.message.channel, {\n\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\tts: toSlackTs(),\n\t\t\t\t\t\t\tuser: \"bot\",\n\t\t\t\t\t\t\ttext: `[Tool Result] ${event.toolName}: ${event.isError ? \"ERROR: \" : \"\"}${truncate(resultStr, 1000)}`,\n\t\t\t\t\t\t\tattachments: [],\n\t\t\t\t\t\t\tisBot: true,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Post args + result together in thread\n\t\t\t\t\t\tconst label = pending?.args ? (pending.args as { label?: string }).label : undefined;\n\t\t\t\t\t\tconst argsFormatted = pending\n\t\t\t\t\t\t\t? formatToolArgsForSlack(event.toolName, pending.args as Record<string, unknown>)\n\t\t\t\t\t\t\t: \"(args not found)\";\n\t\t\t\t\t\tconst duration = (durationMs / 1000).toFixed(1);\n\t\t\t\t\t\tlet threadMessage = `*${event.isError ? \"✗\" : \"✓\"} ${event.toolName}*`;\n\t\t\t\t\t\tif (label) {\n\t\t\t\t\t\t\tthreadMessage += `: ${label}`;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthreadMessage += ` (${duration}s)\\n`;\n\n\t\t\t\t\t\tif (argsFormatted) {\n\t\t\t\t\t\t\tthreadMessage += \"```\\n\" + argsFormatted + \"\\n```\\n\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tthreadMessage += \"*Result:*\\n```\\n\" + resultStr + \"\\n```\";\n\n\t\t\t\t\t\tqueue.enqueueMessage(threadMessage, \"thread\", \"tool result thread\", false);\n\n\t\t\t\t\t\t// Show brief error in main message if failed\n\t\t\t\t\t\tif (event.isError) {\n\t\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), \"tool error\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_update\": {\n\t\t\t\t\t\t// No longer stream to console - just track that we're streaming\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tcase \"message_start\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tlog.logResponseStart(logCtx);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"message_end\":\n\t\t\t\t\t\tif (event.message.role === \"assistant\") {\n\t\t\t\t\t\t\tconst assistantMsg = event.message as any; // AssistantMessage type\n\n\t\t\t\t\t\t\t// Track stop reason\n\t\t\t\t\t\t\tif (assistantMsg.stopReason) {\n\t\t\t\t\t\t\t\tstopReason = assistantMsg.stopReason;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Accumulate usage\n\t\t\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\t\t\ttotalUsage.input += assistantMsg.usage.input;\n\t\t\t\t\t\t\t\ttotalUsage.output += assistantMsg.usage.output;\n\t\t\t\t\t\t\t\ttotalUsage.cacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.input += assistantMsg.usage.cost.input;\n\t\t\t\t\t\t\t\ttotalUsage.cost.output += assistantMsg.usage.cost.output;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;\n\t\t\t\t\t\t\t\ttotalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;\n\t\t\t\t\t\t\t\ttotalUsage.cost.total += assistantMsg.usage.cost.total;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Extract thinking and text from assistant message\n\t\t\t\t\t\t\tconst content = event.message.content;\n\t\t\t\t\t\t\tconst thinkingParts: string[] = [];\n\t\t\t\t\t\t\tconst textParts: string[] = [];\n\t\t\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\t\t\tif (part.type === \"thinking\") {\n\t\t\t\t\t\t\t\t\tthinkingParts.push(part.thinking);\n\t\t\t\t\t\t\t\t} else if (part.type === \"text\") {\n\t\t\t\t\t\t\t\t\ttextParts.push(part.text);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst text = textParts.join(\"\\n\");\n\n\t\t\t\t\t\t\t// Post thinking to main message and thread\n\t\t\t\t\t\t\tfor (const thinking of thinkingParts) {\n\t\t\t\t\t\t\t\tlog.logThinking(logCtx, thinking);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"main\", \"thinking main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(`_${thinking}_`, \"thread\", \"thinking thread\", false);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Post text to main message and thread\n\t\t\t\t\t\t\tif (text.trim()) {\n\t\t\t\t\t\t\t\tlog.logResponse(logCtx, text);\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"main\", \"response main\");\n\t\t\t\t\t\t\t\tqueue.enqueueMessage(text, \"thread\", \"response thread\", false);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Run the agent with user's message\n\t\t\t// Prepend recent messages to the user prompt (not system prompt) for better caching\n\t\t\t// The current message is already the last entry in recentMessages\n\t\t\tconst userPrompt =\n\t\t\t\t`Conversation history (last 50 turns). Respond to the last message.\\n` +\n\t\t\t\t`Format: date TAB user TAB text TAB attachments\\n\\n` +\n\t\t\t\trecentMessages;\n\t\t\t// Debug: write full context to file\n\t\t\tconst toolDefs = tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }));\n\t\t\tconst debugPrompt =\n\t\t\t\t`=== SYSTEM PROMPT (${systemPrompt.length} chars) ===\\n\\n${systemPrompt}\\n\\n` +\n\t\t\t\t`=== TOOL DEFINITIONS (${JSON.stringify(toolDefs).length} chars) ===\\n\\n${JSON.stringify(toolDefs, null, 2)}\\n\\n` +\n\t\t\t\t`=== USER PROMPT (${userPrompt.length} chars) ===\\n\\n${userPrompt}`;\n\t\t\tawait writeFile(join(channelDir, \"last_prompt.txt\"), debugPrompt, \"utf-8\");\n\n\t\t\tawait agent.prompt(userPrompt);\n\n\t\t\t// Wait for all queued respond calls to complete\n\t\t\tawait queue.flush();\n\n\t\t\t// Get final assistant message text from agent state and replace main message\n\t\t\tconst messages = agent.state.messages;\n\t\t\tconst lastAssistant = messages.filter((m) => m.role === \"assistant\").pop();\n\t\t\tconst finalText =\n\t\t\t\tlastAssistant?.content\n\t\t\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t\t\t.map((c) => c.text)\n\t\t\t\t\t.join(\"\\n\") || \"\";\n\t\t\tif (finalText.trim()) {\n\t\t\t\ttry {\n\t\t\t\t\t// For the main message, truncate if too long (full text is in thread)\n\t\t\t\t\tconst mainText =\n\t\t\t\t\t\tfinalText.length > SLACK_MAX_LENGTH\n\t\t\t\t\t\t\t? finalText.substring(0, SLACK_MAX_LENGTH - 50) + \"\\n\\n_(see thread for full response)_\"\n\t\t\t\t\t\t\t: finalText;\n\t\t\t\t\tawait ctx.replaceMessage(mainText);\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tlog.logWarning(\"Failed to replace message with final text\", errMsg);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Log usage summary if there was any usage\n\t\t\tif (totalUsage.cost.total > 0) {\n\t\t\t\tconst summary = log.logUsageSummary(logCtx, totalUsage);\n\t\t\t\tqueue.enqueue(() => ctx.respondInThread(summary), \"usage summary\");\n\t\t\t\tawait queue.flush();\n\t\t\t}\n\n\t\t\treturn { stopReason };\n\t\t},\n\n\t\tabort(): void {\n\t\t\tagent?.abort();\n\t\t},\n\t};\n}\n\n/**\n * Translate container path back to host path for file operations\n */\nfunction translateToHostPath(\n\tcontainerPath: string,\n\tchannelDir: string,\n\tworkspacePath: string,\n\tchannelId: string,\n): string {\n\tif (workspacePath === \"/workspace\") {\n\t\t// Docker mode - translate /workspace/channelId/... to host path\n\t\tconst prefix = `/workspace/${channelId}/`;\n\t\tif (containerPath.startsWith(prefix)) {\n\t\t\treturn join(channelDir, containerPath.slice(prefix.length));\n\t\t}\n\t\t// Maybe it's just /workspace/...\n\t\tif (containerPath.startsWith(\"/workspace/\")) {\n\t\t\treturn join(channelDir, \"..\", containerPath.slice(\"/workspace/\".length));\n\t\t}\n\t}\n\t// Host mode or already a host path\n\treturn containerPath;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-mom",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "description": "Slack bot that delegates messages to the pi coding agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@anthropic-ai/sandbox-runtime": "^0.0.16",
24
- "@mariozechner/pi-agent-core": "^0.11.2",
25
- "@mariozechner/pi-ai": "^0.11.2",
24
+ "@mariozechner/pi-agent-core": "^0.11.3",
25
+ "@mariozechner/pi-ai": "^0.11.3",
26
26
  "@sinclair/typebox": "^0.34.0",
27
27
  "@slack/socket-mode": "^2.0.0",
28
28
  "@slack/web-api": "^7.0.0",