@geminixiang/mama 0.2.0-beta.6 → 0.2.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +20 -14
  2. package/dist/adapter.d.ts +5 -2
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js.map +1 -1
  5. package/dist/adapters/discord/bot.d.ts +1 -0
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +29 -9
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts +1 -1
  10. package/dist/adapters/discord/context.d.ts.map +1 -1
  11. package/dist/adapters/discord/context.js +1 -2
  12. package/dist/adapters/discord/context.js.map +1 -1
  13. package/dist/adapters/slack/bot.d.ts +8 -0
  14. package/dist/adapters/slack/bot.d.ts.map +1 -1
  15. package/dist/adapters/slack/bot.js +177 -11
  16. package/dist/adapters/slack/bot.js.map +1 -1
  17. package/dist/adapters/slack/branch-manager.d.ts +1 -0
  18. package/dist/adapters/slack/branch-manager.d.ts.map +1 -1
  19. package/dist/adapters/slack/branch-manager.js +9 -8
  20. package/dist/adapters/slack/branch-manager.js.map +1 -1
  21. package/dist/adapters/slack/context.d.ts +1 -1
  22. package/dist/adapters/slack/context.d.ts.map +1 -1
  23. package/dist/adapters/slack/context.js +10 -8
  24. package/dist/adapters/slack/context.js.map +1 -1
  25. package/dist/adapters/slack/tools/attach.d.ts +1 -1
  26. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  27. package/dist/adapters/slack/tools/attach.js.map +1 -1
  28. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  29. package/dist/adapters/telegram/bot.js +33 -2
  30. package/dist/adapters/telegram/bot.js.map +1 -1
  31. package/dist/agent.d.ts +1 -2
  32. package/dist/agent.d.ts.map +1 -1
  33. package/dist/agent.js +507 -422
  34. package/dist/agent.js.map +1 -1
  35. package/dist/commands/index.d.ts.map +1 -1
  36. package/dist/commands/index.js +2 -0
  37. package/dist/commands/index.js.map +1 -1
  38. package/dist/commands/login.d.ts.map +1 -1
  39. package/dist/commands/login.js +41 -2
  40. package/dist/commands/login.js.map +1 -1
  41. package/dist/commands/model.d.ts +1 -1
  42. package/dist/commands/model.d.ts.map +1 -1
  43. package/dist/commands/model.js +25 -7
  44. package/dist/commands/model.js.map +1 -1
  45. package/dist/commands/new.d.ts.map +1 -1
  46. package/dist/commands/new.js +1 -1
  47. package/dist/commands/new.js.map +1 -1
  48. package/dist/commands/sandbox.d.ts +10 -0
  49. package/dist/commands/sandbox.d.ts.map +1 -0
  50. package/dist/commands/sandbox.js +88 -0
  51. package/dist/commands/sandbox.js.map +1 -0
  52. package/dist/commands/session-view.d.ts.map +1 -1
  53. package/dist/commands/session-view.js +34 -10
  54. package/dist/commands/session-view.js.map +1 -1
  55. package/dist/commands/types.d.ts +1 -3
  56. package/dist/commands/types.d.ts.map +1 -1
  57. package/dist/commands/types.js.map +1 -1
  58. package/dist/commands/utils.d.ts +3 -0
  59. package/dist/commands/utils.d.ts.map +1 -1
  60. package/dist/commands/utils.js +5 -0
  61. package/dist/commands/utils.js.map +1 -1
  62. package/dist/config.d.ts +7 -1
  63. package/dist/config.d.ts.map +1 -1
  64. package/dist/config.js +64 -23
  65. package/dist/config.js.map +1 -1
  66. package/dist/context.d.ts +2 -44
  67. package/dist/context.d.ts.map +1 -1
  68. package/dist/context.js +7 -210
  69. package/dist/context.js.map +1 -1
  70. package/dist/events.d.ts.map +1 -1
  71. package/dist/events.js +15 -14
  72. package/dist/events.js.map +1 -1
  73. package/dist/execution-resolver.d.ts +3 -2
  74. package/dist/execution-resolver.d.ts.map +1 -1
  75. package/dist/execution-resolver.js +40 -7
  76. package/dist/execution-resolver.js.map +1 -1
  77. package/dist/file-guards.d.ts +6 -0
  78. package/dist/file-guards.d.ts.map +1 -0
  79. package/dist/file-guards.js +48 -0
  80. package/dist/file-guards.js.map +1 -0
  81. package/dist/log.d.ts +1 -5
  82. package/dist/log.d.ts.map +1 -1
  83. package/dist/log.js +13 -38
  84. package/dist/log.js.map +1 -1
  85. package/dist/login/index.d.ts +14 -2
  86. package/dist/login/index.d.ts.map +1 -1
  87. package/dist/login/index.js +40 -13
  88. package/dist/login/index.js.map +1 -1
  89. package/dist/login/portal.d.ts +2 -1
  90. package/dist/login/portal.d.ts.map +1 -1
  91. package/dist/login/portal.js +12 -12
  92. package/dist/login/portal.js.map +1 -1
  93. package/dist/main.d.ts.map +1 -1
  94. package/dist/main.js +33 -28
  95. package/dist/main.js.map +1 -1
  96. package/dist/provisioner.d.ts +12 -2
  97. package/dist/provisioner.d.ts.map +1 -1
  98. package/dist/provisioner.js +43 -14
  99. package/dist/provisioner.js.map +1 -1
  100. package/dist/runtime/conversation-orchestrator.d.ts +42 -0
  101. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  102. package/dist/runtime/conversation-orchestrator.js +150 -0
  103. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  104. package/dist/runtime/session-runtime.d.ts +1 -1
  105. package/dist/runtime/session-runtime.d.ts.map +1 -1
  106. package/dist/runtime/session-runtime.js +49 -148
  107. package/dist/runtime/session-runtime.js.map +1 -1
  108. package/dist/sandbox/cloudflare.d.ts.map +1 -1
  109. package/dist/sandbox/cloudflare.js +2 -2
  110. package/dist/sandbox/cloudflare.js.map +1 -1
  111. package/dist/sandbox/container.d.ts.map +1 -1
  112. package/dist/sandbox/container.js +1 -1
  113. package/dist/sandbox/container.js.map +1 -1
  114. package/dist/sandbox/index.d.ts.map +1 -1
  115. package/dist/sandbox/index.js +4 -4
  116. package/dist/sandbox/index.js.map +1 -1
  117. package/dist/sentry.d.ts +1 -1
  118. package/dist/sentry.d.ts.map +1 -1
  119. package/dist/sentry.js +2 -2
  120. package/dist/sentry.js.map +1 -1
  121. package/dist/session-store.d.ts +2 -1
  122. package/dist/session-store.d.ts.map +1 -1
  123. package/dist/session-store.js +19 -15
  124. package/dist/session-store.js.map +1 -1
  125. package/dist/session-view/portal.d.ts +6 -1
  126. package/dist/session-view/portal.d.ts.map +1 -1
  127. package/dist/session-view/portal.js +829 -71
  128. package/dist/session-view/portal.js.map +1 -1
  129. package/dist/session-view/service.d.ts.map +1 -1
  130. package/dist/session-view/service.js +5 -4
  131. package/dist/session-view/service.js.map +1 -1
  132. package/dist/session-view/store.d.ts +2 -1
  133. package/dist/session-view/store.d.ts.map +1 -1
  134. package/dist/session-view/store.js +2 -1
  135. package/dist/session-view/store.js.map +1 -1
  136. package/dist/store.d.ts.map +1 -1
  137. package/dist/store.js +7 -13
  138. package/dist/store.js.map +1 -1
  139. package/dist/tool-diagnostics.d.ts +2 -0
  140. package/dist/tool-diagnostics.d.ts.map +1 -0
  141. package/dist/tool-diagnostics.js +7 -0
  142. package/dist/tool-diagnostics.js.map +1 -0
  143. package/dist/tools/bash.d.ts +1 -1
  144. package/dist/tools/bash.d.ts.map +1 -1
  145. package/dist/tools/bash.js.map +1 -1
  146. package/dist/tools/edit.d.ts +1 -1
  147. package/dist/tools/edit.d.ts.map +1 -1
  148. package/dist/tools/edit.js.map +1 -1
  149. package/dist/tools/event.d.ts +1 -1
  150. package/dist/tools/event.d.ts.map +1 -1
  151. package/dist/tools/event.js.map +1 -1
  152. package/dist/tools/index.d.ts +1 -1
  153. package/dist/tools/index.d.ts.map +1 -1
  154. package/dist/tools/index.js.map +1 -1
  155. package/dist/tools/read.d.ts +1 -1
  156. package/dist/tools/read.d.ts.map +1 -1
  157. package/dist/tools/read.js.map +1 -1
  158. package/dist/tools/write.d.ts +1 -1
  159. package/dist/tools/write.d.ts.map +1 -1
  160. package/dist/tools/write.js.map +1 -1
  161. package/dist/vault-routing.d.ts +0 -3
  162. package/dist/vault-routing.d.ts.map +1 -1
  163. package/dist/vault-routing.js +0 -24
  164. package/dist/vault-routing.js.map +1 -1
  165. package/dist/vault.d.ts +21 -57
  166. package/dist/vault.d.ts.map +1 -1
  167. package/dist/vault.js +114 -246
  168. package/dist/vault.js.map +1 -1
  169. package/package.json +6 -4
  170. package/dist/bindings.d.ts +0 -45
  171. package/dist/bindings.d.ts.map +0 -1
  172. package/dist/bindings.js +0 -75
  173. package/dist/bindings.js.map +0 -1
package/dist/agent.js CHANGED
@@ -1,17 +1,17 @@
1
- import { Agent } from "@mariozechner/pi-agent-core";
2
- import { getModel } from "@mariozechner/pi-ai";
3
- import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, getAgentDir, loadSkillsFromDir, ModelRegistry, } from "@mariozechner/pi-coding-agent";
1
+ import { Agent } from "@earendil-works/pi-agent-core";
2
+ import { getModel } from "@earendil-works/pi-ai";
3
+ import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, getAgentDir, loadSkillsFromDir, ModelRegistry, SettingsManager, } from "@earendil-works/pi-coding-agent";
4
4
  import { existsSync, readFileSync } from "fs";
5
5
  import { mkdir, readFile, writeFile } from "fs/promises";
6
6
  import { homedir } from "os";
7
- import { join } from "path";
7
+ import { join, posix } from "path";
8
8
  import { loadAgentConfigForConversation } from "./config.js";
9
- import { createMamaSettingsManager, syncLogToSessionManager } from "./context.js";
10
9
  import { ActorExecutionResolver } from "./execution-resolver.js";
11
10
  import * as log from "./log.js";
12
11
  import { createExecutor } from "./sandbox.js";
13
12
  import { addLifecycleBreadcrumb, metricAttributes } from "./sentry.js";
14
- import { extractSessionSuffix, extractSessionUuid, openManagedSession, } from "./session-store.js";
13
+ import { extractSessionUuid, openManagedSession, } from "./session-store.js";
14
+ import { shouldSurfaceToolDiagnostic } from "./tool-diagnostics.js";
15
15
  import { createMamaTools } from "./tools/index.js";
16
16
  import * as Sentry from "@sentry/node";
17
17
  const IMAGE_MIME_TYPES = {
@@ -94,12 +94,53 @@ function loadMamaSkills(conversationDir, workspacePath) {
94
94
  }
95
95
  return Array.from(skillMap.values());
96
96
  }
97
+ function buildRuntimePaths(workspacePath, conversationId) {
98
+ const workspaceRoot = workspacePath.replace(/\/+$/, "") || "/";
99
+ const conversationPath = posix.join(workspaceRoot, conversationId);
100
+ return {
101
+ workspaceRoot,
102
+ conversationPath,
103
+ scratchPath: posix.join(conversationPath, "scratch"),
104
+ };
105
+ }
106
+ function buildEnvDescription(sandboxType, workspaceRoot) {
107
+ switch (sandboxType) {
108
+ case "image":
109
+ return `You are running inside a managed per-user container.
110
+ - Runtime workspace root: ${workspaceRoot}
111
+ - Bash commands start in: ${workspaceRoot}
112
+ - Install tools with the image's package manager
113
+ - Your changes persist for this user's container until it is recreated`;
114
+ case "container":
115
+ return `You are running inside a shared container.
116
+ - Runtime workspace root: ${workspaceRoot}
117
+ - Bash commands start in: ${workspaceRoot}
118
+ - Install tools with the container's package manager
119
+ - Your changes persist across sessions`;
120
+ case "firecracker":
121
+ return `You are running inside a Firecracker microVM.
122
+ - Runtime workspace root: ${workspaceRoot}
123
+ - Use cd or absolute paths; project files are under ${workspaceRoot}
124
+ - Install tools with: apt-get install <package> (Debian-based)
125
+ - Your changes persist across sessions`;
126
+ case "cloudflare":
127
+ return `You are running through a Cloudflare Sandbox bridge.
128
+ - Runtime workspace root: ${workspaceRoot}
129
+ - Bash commands start in: ${workspaceRoot}
130
+ - Your commands run in a remote container managed by Cloudflare
131
+ - Important: the remote filesystem is not automatically synced back to the host workspace`;
132
+ default:
133
+ return `You are running directly on the host machine.
134
+ - Runtime workspace root: ${workspaceRoot}
135
+ - Bash commands start in: ${process.cwd()}
136
+ - Be careful with system modifications`;
137
+ }
138
+ }
97
139
  function buildSystemPrompt(workspacePath, conversationId, conversationKind, currentUserId, memory, sandboxConfig, platform, skills) {
98
- const conversationPath = `${workspacePath}/${conversationId}`;
99
- const isContainer = sandboxConfig.type === "container" || sandboxConfig.type === "image";
100
- const isImageSandbox = sandboxConfig.type === "image";
101
- const isFirecracker = sandboxConfig.type === "firecracker";
102
- const isCloudflareSandbox = sandboxConfig.type === "cloudflare";
140
+ const { workspaceRoot, conversationPath, scratchPath } = buildRuntimePaths(workspacePath, conversationId);
141
+ const sandboxType = sandboxConfig.type;
142
+ const isContainerLike = sandboxType === "container" || sandboxType === "image";
143
+ const isFirecracker = sandboxType === "firecracker";
103
144
  // Format channel mappings
104
145
  const channelMappings = platform.channels.length > 0
105
146
  ? platform.channels.map((c) => `${c.id}\t#${c.name}`).join("\n")
@@ -108,29 +149,7 @@ function buildSystemPrompt(workspacePath, conversationId, conversationKind, curr
108
149
  const userMappings = platform.users.length > 0
109
150
  ? platform.users.map((u) => `${u.id}\t@${u.userName}\t${u.displayName}`).join("\n")
110
151
  : "(no users loaded)";
111
- const envDescription = isImageSandbox
112
- ? `You are running inside a managed per-user container.
113
- - Bash working directory: / (use cd or absolute paths)
114
- - Install tools with the image's package manager
115
- - Your changes persist for this user's container until it is recreated`
116
- : isContainer
117
- ? `You are running inside a shared container.
118
- - Bash working directory: / (use cd or absolute paths)
119
- - Install tools with the container's package manager
120
- - Your changes persist across sessions`
121
- : isFirecracker
122
- ? `You are running inside a Firecracker microVM.
123
- - Bash working directory: / (use cd or absolute paths)
124
- - Install tools with: apt-get install <package> (Debian-based)
125
- - Your changes persist across sessions`
126
- : isCloudflareSandbox
127
- ? `You are running through a Cloudflare Sandbox bridge.
128
- - Bash working directory: /workspace
129
- - Your commands run in a remote container managed by Cloudflare
130
- - Important: the remote filesystem is not automatically synced back to the host workspace`
131
- : `You are running directly on the host machine.
132
- - Bash working directory: ${process.cwd()}
133
- - Be careful with system modifications`;
152
+ const envDescription = buildEnvDescription(sandboxType, workspaceRoot);
134
153
  return `You are mama, a ${platform.name} bot assistant. Be concise. No emojis.
135
154
 
136
155
  ## Context
@@ -153,9 +172,11 @@ When mentioning users, use <@username> format (e.g., <@mario>).
153
172
 
154
173
  ## Environment
155
174
  ${envDescription}
175
+ - Default place for clones, downloads, and experiments: ${scratchPath}
176
+ - Do not use host-only paths unless you are running in host mode and verified they exist.
156
177
 
157
178
  ## Workspace Layout
158
- ${workspacePath}/
179
+ ${workspaceRoot}/
159
180
  ├── MEMORY.md # Global memory (all conversations)
160
181
  ├── skills/ # Global CLI tools you create
161
182
  └── ${conversationId}/ # This conversation
@@ -166,14 +187,14 @@ ${workspacePath}/
166
187
  │ ├── <timestamp>_<id>.jsonl # Top-level session files
167
188
  │ └── <scope_id>.jsonl # Scoped thread/reply session files
168
189
  ├── attachments/ # User-shared files
169
- ├── scratch/ # Your working directory
190
+ ├── scratch/ # Working directory for clones/downloads/experiments: ${scratchPath}
170
191
  └── skills/ # Conversation-specific tools
171
192
 
172
193
  ## Skills (Custom CLI Tools)
173
194
  You can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).
174
195
 
175
196
  ### Creating Skills
176
- Store in \`${workspacePath}/skills/<name>/\` (global) or \`${conversationPath}/skills/<name>/\` (conversation-specific).
197
+ Store in \`${workspaceRoot}/skills/<name>/\` (global) or \`${conversationPath}/skills/<name>/\` (conversation-specific).
177
198
  Each skill directory needs a \`SKILL.md\` with YAML frontmatter:
178
199
 
179
200
  \`\`\`markdown
@@ -194,7 +215,7 @@ Scripts are in: {baseDir}/
194
215
  ${skills.length > 0 ? formatSkillsForPrompt(skills) : "(no skills installed yet)"}
195
216
 
196
217
  ## Events
197
- You can schedule events that wake you up at specific times or when external things happen. Events are JSON files in \`${workspacePath}/events/\`.
218
+ You can schedule events that wake you up at specific times or when external things happen. Events are JSON files in \`${workspaceRoot}/events/\`.
198
219
 
199
220
  ### Event Types
200
221
 
@@ -233,16 +254,16 @@ Prefer the \`event\` tool over manually writing JSON files; it fills \`platform\
233
254
  ### Creating Events
234
255
  Use unique filenames to avoid overwriting existing events. Include a timestamp or random suffix:
235
256
  \`\`\`bash
236
- cat > ${workspacePath}/events/dentist-reminder-$(date +%s).json << 'EOF'
257
+ cat > ${workspaceRoot}/events/dentist-reminder-$(date +%s).json << 'EOF'
237
258
  {"type": "one-shot", "platform": "${platform.name}", "conversationId": "${conversationId}", "conversationKind": "${conversationKind}", "userId": "${currentUserId ?? "<requester userId>"}", "text": "Dentist tomorrow", "at": "2025-12-14T09:00:00+01:00"}
238
259
  EOF
239
260
  \`\`\`
240
261
  Or check if file exists first before creating.
241
262
 
242
263
  ### Managing Events
243
- - List: \`ls ${workspacePath}/events/\`
244
- - View: \`cat ${workspacePath}/events/foo.json\`
245
- - Delete/cancel: \`rm ${workspacePath}/events/foo.json\`
264
+ - List: \`ls ${workspaceRoot}/events/\`
265
+ - View: \`cat ${workspaceRoot}/events/foo.json\`
266
+ - Delete/cancel: \`rm ${workspaceRoot}/events/foo.json\`
246
267
 
247
268
  ### When Events Trigger
248
269
  You receive a message like:
@@ -262,7 +283,7 @@ Maximum 5 events can be queued. Don't create excessive immediate or periodic eve
262
283
 
263
284
  ## Memory
264
285
  Write to MEMORY.md files to persist context across conversations.
265
- - Global (${workspacePath}/MEMORY.md): skills, preferences, project info
286
+ - Global (${workspaceRoot}/MEMORY.md): skills, preferences, project info
266
287
  - Conversation (${conversationPath}/MEMORY.md): conversation-specific decisions, ongoing work
267
288
  Update when you learn something important or when asked to remember something.
268
289
 
@@ -270,7 +291,7 @@ Update when you learn something important or when asked to remember something.
270
291
  ${memory}
271
292
 
272
293
  ## System Configuration Log
273
- Maintain ${workspacePath}/SYSTEM.md to log all environment modifications:
294
+ Maintain ${workspaceRoot}/SYSTEM.md to log all environment modifications:
274
295
  - Installed packages (apt install, npm install, uv pip install)
275
296
  - Environment variables set
276
297
  - Config files modified (~/.gitconfig, cron jobs, etc.)
@@ -282,8 +303,7 @@ Update this file whenever you modify the environment. On fresh container, read i
282
303
  Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
283
304
  The log contains user messages and your final responses (not tool calls/results).
284
305
  Use \`log.jsonl\` for quick grep-style history. Use \`${conversationPath}/sessions/\` when you need structured turns, tool outputs, or branch lineage.
285
- ${isContainer ? "Install jq: apt-get install jq" : ""}
286
- ${isFirecracker ? "Install jq: apt-get install jq" : ""}
306
+ ${isContainerLike || isFirecracker ? "Install jq: apt-get install jq" : ""}
287
307
 
288
308
  \`\`\`bash
289
309
  # Recent messages
@@ -315,50 +335,10 @@ function truncate(text, maxLen) {
315
335
  return text;
316
336
  return `${text.substring(0, maxLen - 3)}...`;
317
337
  }
318
- // Tools whose output is interesting in the structured session log but too noisy
319
- // to surface as a per-tool diagnostic to the user.
320
- const QUIET_TOOLS = new Set(["read", "write", "edit"]);
321
- // Cap raw tool output before handing it to adapters. Bash output can be MB; without
322
- // this each adapter's splitter would fan it out into many sequential platform posts.
323
- const TOOL_RESULT_DIAGNOSTIC_CAP = 8000;
324
- function extractToolResultText(result) {
325
- if (typeof result === "string") {
326
- return result;
327
- }
328
- if (result &&
329
- typeof result === "object" &&
330
- "content" in result &&
331
- Array.isArray(result.content)) {
332
- const content = result.content;
333
- const textParts = [];
334
- for (const part of content) {
335
- if (part.type === "text" && part.text) {
336
- textParts.push(part.text);
337
- }
338
- }
339
- if (textParts.length > 0) {
340
- return textParts.join("\n");
341
- }
342
- }
343
- return JSON.stringify(result);
338
+ function initialWorkspacePath(sandboxConfig, hostWorkspacePath) {
339
+ return sandboxConfig.type === "host" ? hostWorkspacePath : "/workspace";
344
340
  }
345
- // ============================================================================
346
- // Agent runner
347
- // ============================================================================
348
- /**
349
- * Create a new AgentRunner for a channel.
350
- * Sets up the session and subscribes to events once.
351
- *
352
- * Runner caching is handled by the caller (channelStates in main.ts).
353
- * This is a stateless factory function.
354
- */
355
- export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, bindingStore, provisioner) {
356
- const agentConfig = loadAgentConfigForConversation(conversationDir);
357
- // Initialize logger with settings from config
358
- log.initLogger({
359
- logFormat: agentConfig.logFormat,
360
- logLevel: agentConfig.logLevel,
361
- });
341
+ function createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, hostWorkspacePath) {
362
342
  const executionResolver = vaultManager &&
363
343
  sandboxConfig.type !== "host" &&
364
344
  (vaultManager.isEnabled() ||
@@ -384,68 +364,42 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
384
364
  return activeExecutor.getSandboxConfig();
385
365
  },
386
366
  };
387
- const workspaceBase = conversationDir.replace(`/${conversationId}`, "");
388
- const getWorkspacePath = () => executor.getWorkspacePath(workspaceBase);
389
- let workspacePath = getWorkspacePath();
390
- // Create tools (per-runner, with per-runner upload function setter)
391
- const { tools, setUploadFunction, setEventContext } = createMamaTools(executor, workspaceDir);
392
- // Resolve model from config
393
- // Use 'as any' cast because agentConfig.provider/model are plain strings,
394
- // while getModel() has constrained generic types for known providers.
395
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
396
- const model = getModel(agentConfig.provider, agentConfig.model);
397
- // Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
398
- const memory = await getMemory(conversationDir);
399
- const skills = loadMamaSkills(conversationDir, workspacePath);
400
- const emptyPlatform = {
401
- name: "chat",
402
- formattingGuide: "",
403
- channels: [],
404
- users: [],
367
+ return {
368
+ executionResolver,
369
+ executor,
370
+ getWorkspacePath: () => executor.getWorkspacePath(hostWorkspacePath),
371
+ async resolveExecutorForRun(context) {
372
+ if (!executionResolver)
373
+ return;
374
+ activeExecutor = await executionResolver.resolve(context);
375
+ },
405
376
  };
406
- const systemPrompt = buildSystemPrompt(workspacePath, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
407
- // Create session manager and settings manager. Top-level/private sessions
408
- // use the conversation's current pointer; scoped sessions use fixed files.
409
- // Platform-specific branch/fork behavior is resolved before runner creation.
410
- const isThread = sessionKey.includes(":");
411
- const rootTs = extractSessionSuffix(sessionKey);
412
- const { sessionDir, contextFile, threadRootMessage } = sessionScope;
413
- const sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
414
- const threadSessionName = buildThreadSessionName(threadRootMessage);
415
- if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
416
- sessionManager.appendSessionInfo(threadSessionName);
417
- }
418
- const sessionUuid = extractSessionUuid(contextFile);
419
- const settingsManager = createMamaSettingsManager(join(conversationDir, ".."));
420
- // Create AuthStorage and ModelRegistry
421
- // Auth stored outside workspace so agent can't access it
377
+ }
378
+ async function createConfiguredAgentSession(params) {
379
+ const { conversationId, workspaceDir, workspacePath, systemPrompt, model, thinkingLevel, tools, sessionManager, settingsManager, } = params;
422
380
  const authStorage = AuthStorage.create(join(homedir(), ".pi", "mama", "auth.json"));
423
381
  const modelRegistry = ModelRegistry.create(authStorage);
424
- // Create agent
425
382
  const agent = new Agent({
426
383
  initialState: {
427
384
  systemPrompt,
428
385
  model,
429
- thinkingLevel: agentConfig.thinkingLevel,
386
+ thinkingLevel,
430
387
  tools,
431
388
  },
432
389
  convertToLlm,
433
390
  getApiKey: async () => {
434
391
  const key = await modelRegistry.getApiKeyForProvider(model.provider);
435
- if (!key)
392
+ if (!key) {
436
393
  throw new Error(`No API key for provider "${model.provider}". Set the appropriate environment variable or configure via auth.json`);
394
+ }
437
395
  return key;
438
396
  },
439
397
  });
440
- // Load existing messages
441
398
  const loadedSession = sessionManager.buildSessionContext();
442
399
  if (loadedSession.messages.length > 0) {
443
400
  agent.state.messages = loadedSession.messages;
444
401
  log.logInfo(`[${conversationId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
445
402
  }
446
- // Load extensions, skills, prompts, themes via DefaultResourceLoader
447
- // This reads ~/.pi/agent/settings.json (packages, extensions enable/disable)
448
- // and discovers resources from standard locations + npm/git packages.
449
403
  const resourceLoader = new DefaultResourceLoader({
450
404
  cwd: workspaceDir,
451
405
  agentDir: getAgentDir(),
@@ -459,111 +413,354 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
459
413
  log.logWarning(`[${conversationId}] Extension load error: ${err.path}`, err.error);
460
414
  }
461
415
  }
462
- log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((e) => e.path).join(", ")}`);
416
+ log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((extension) => extension.path).join(", ")}`);
463
417
  }
464
418
  catch (error) {
465
419
  log.logWarning(`[${conversationId}] Failed to load resources`, String(error));
466
420
  }
467
421
  const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
468
- // Create AgentSession wrapper
469
422
  const session = new AgentSession({
470
423
  agent,
471
424
  sessionManager,
472
425
  settingsManager,
473
- cwd: workspaceDir,
426
+ cwd: workspacePath,
474
427
  modelRegistry,
475
428
  resourceLoader,
476
429
  baseToolsOverride,
477
430
  });
478
- // Mutable per-run state - event handler references this
479
- const runState = {
431
+ return { agent, session };
432
+ }
433
+ function createEmptyUsageTotals() {
434
+ return {
435
+ input: 0,
436
+ output: 0,
437
+ cacheRead: 0,
438
+ cacheWrite: 0,
439
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
440
+ };
441
+ }
442
+ function createRunState() {
443
+ return {
480
444
  responseCtx: null,
481
445
  logCtx: null,
482
446
  queue: null,
483
447
  pendingTools: new Map(),
484
- totalUsage: {
485
- input: 0,
486
- output: 0,
487
- cacheRead: 0,
488
- cacheWrite: 0,
489
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
490
- },
448
+ totalUsage: createEmptyUsageTotals(),
491
449
  llmCallCount: 0,
492
450
  stopReason: "stop",
493
451
  errorMessage: undefined,
494
452
  };
495
- // Subscribe to events ONCE
453
+ }
454
+ function resetRunState(runState, responseCtx, sessionConversation, userName, sessionUuid) {
455
+ runState.responseCtx = responseCtx;
456
+ runState.logCtx = {
457
+ conversationId: sessionConversation,
458
+ userName,
459
+ conversationName: undefined,
460
+ sessionId: sessionUuid,
461
+ };
462
+ runState.pendingTools.clear();
463
+ runState.totalUsage = createEmptyUsageTotals();
464
+ runState.llmCallCount = 0;
465
+ runState.stopReason = "stop";
466
+ runState.errorMessage = undefined;
467
+ }
468
+ function createRunQueue(responseCtx) {
469
+ let queueChain = Promise.resolve();
470
+ return {
471
+ queue: {
472
+ enqueue(fn, errorContext) {
473
+ queueChain = queueChain.then(async () => {
474
+ try {
475
+ await fn();
476
+ }
477
+ catch (err) {
478
+ const errMsg = err instanceof Error ? err.message : String(err);
479
+ log.logWarning(`API error (${errorContext})`, errMsg);
480
+ try {
481
+ await responseCtx.respondDiagnostic(`Error: ${errMsg}`, { style: "error" });
482
+ }
483
+ catch {
484
+ // Ignore
485
+ }
486
+ }
487
+ });
488
+ },
489
+ },
490
+ wait: () => queueChain,
491
+ };
492
+ }
493
+ function padTwoDigits(n) {
494
+ return n.toString().padStart(2, "0");
495
+ }
496
+ function formatTimestampedUserMessage(message) {
497
+ const now = new Date();
498
+ const offset = -now.getTimezoneOffset();
499
+ const offsetSign = offset >= 0 ? "+" : "-";
500
+ const offsetHours = padTwoDigits(Math.floor(Math.abs(offset) / 60));
501
+ const offsetMins = padTwoDigits(Math.abs(offset) % 60);
502
+ const timestamp = `${now.getFullYear()}-${padTwoDigits(now.getMonth() + 1)}-${padTwoDigits(now.getDate())} ` +
503
+ `${padTwoDigits(now.getHours())}:${padTwoDigits(now.getMinutes())}:${padTwoDigits(now.getSeconds())}` +
504
+ `${offsetSign}${offsetHours}:${offsetMins}`;
505
+ const threadContext = message.threadTs ? ` [in-thread:${message.threadTs}]` : "";
506
+ return `[${timestamp}] [${message.userName || "unknown"}]${threadContext}: ${message.text}`;
507
+ }
508
+ function collectMessageAttachments(message, workspacePath) {
509
+ const imageAttachments = [];
510
+ const nonImagePaths = [];
511
+ for (const attachment of message.attachments || []) {
512
+ const fullPath = `${workspacePath}/${attachment.localPath}`;
513
+ const mimeType = getImageMimeType(attachment.localPath);
514
+ if (mimeType && existsSync(fullPath)) {
515
+ try {
516
+ imageAttachments.push({
517
+ type: "image",
518
+ mimeType,
519
+ data: readFileSync(fullPath).toString("base64"),
520
+ });
521
+ }
522
+ catch {
523
+ nonImagePaths.push(fullPath);
524
+ }
525
+ }
526
+ else {
527
+ nonImagePaths.push(fullPath);
528
+ }
529
+ }
530
+ return { imageAttachments, nonImagePaths };
531
+ }
532
+ function buildPromptPayload(message, workspacePath) {
533
+ let userMessage = formatTimestampedUserMessage(message);
534
+ const { imageAttachments, nonImagePaths } = collectMessageAttachments(message, workspacePath);
535
+ if (nonImagePaths.length > 0) {
536
+ userMessage += `\n\n<slack_attachments>\n${nonImagePaths.join("\n")}\n</slack_attachments>`;
537
+ }
538
+ return { userMessage, imageAttachments };
539
+ }
540
+ async function writePromptDebugContext(conversationDir, systemPrompt, session, userMessage, imageAttachmentCount) {
541
+ const debugContext = {
542
+ systemPrompt,
543
+ messages: session.messages,
544
+ newUserMessage: userMessage,
545
+ imageAttachmentCount,
546
+ };
547
+ await writeFile(join(conversationDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
548
+ }
549
+ function getFinalAssistantText(session) {
550
+ const lastAssistant = session.messages.filter((message) => message.role === "assistant").pop();
551
+ return (lastAssistant?.content
552
+ .filter((content) => content.type === "text")
553
+ .map((content) => content.text)
554
+ .join("\n") || "");
555
+ }
556
+ async function finalizeRunResponse(responseCtx, session, runState) {
557
+ if (runState.stopReason === "error" && runState.errorMessage) {
558
+ try {
559
+ await responseCtx.replaceResponse("_Sorry, something went wrong_");
560
+ await responseCtx.respondDiagnostic(`Error: ${runState.errorMessage}`, {
561
+ style: "error",
562
+ });
563
+ }
564
+ catch (err) {
565
+ const errMsg = err instanceof Error ? err.message : String(err);
566
+ log.logWarning("Failed to post error message", errMsg);
567
+ }
568
+ return;
569
+ }
570
+ const finalText = getFinalAssistantText(session);
571
+ if (finalText.trim() === "[SILENT]" || finalText.trim().startsWith("[SILENT]")) {
572
+ try {
573
+ await responseCtx.deleteResponse();
574
+ log.logInfo("Silent response - deleted message and thread");
575
+ }
576
+ catch (err) {
577
+ const errMsg = err instanceof Error ? err.message : String(err);
578
+ log.logWarning("Failed to delete message for silent response", errMsg);
579
+ }
580
+ return;
581
+ }
582
+ if (!finalText.trim())
583
+ return;
584
+ try {
585
+ await responseCtx.replaceResponse(finalText);
586
+ }
587
+ catch (err) {
588
+ const errMsg = err instanceof Error ? err.message : String(err);
589
+ log.logWarning("Failed to replace message with final text", errMsg);
590
+ }
591
+ }
592
+ async function reportUsageSummary(ctx) {
593
+ const { session, runState, responseCtx, platform, model, agentConfig, sessionConversation, sessionUuid, waitForQueue, } = ctx;
594
+ if (runState.totalUsage.cost.total <= 0)
595
+ return;
596
+ const lastAssistantMessage = session.messages
597
+ .slice()
598
+ .toReversed()
599
+ .find((message) => message.role === "assistant" && message.stopReason !== "aborted");
600
+ const contextTokens = lastAssistantMessage
601
+ ? lastAssistantMessage.usage.input +
602
+ lastAssistantMessage.usage.output +
603
+ lastAssistantMessage.usage.cacheRead +
604
+ lastAssistantMessage.usage.cacheWrite
605
+ : 0;
606
+ const contextWindow = model.contextWindow || 200000;
607
+ const { totalUsage } = runState;
608
+ const runMetricAttributes = metricAttributes({
609
+ provider: model.provider,
610
+ model: agentConfig.model,
611
+ channel_id: sessionConversation,
612
+ session_id: sessionUuid,
613
+ stop_reason: runState.stopReason,
614
+ llm_calls: runState.llmCallCount,
615
+ });
616
+ Sentry.metrics.distribution("agent.run.tokens_in", totalUsage.input, {
617
+ attributes: runMetricAttributes,
618
+ });
619
+ Sentry.metrics.distribution("agent.run.tokens_out", totalUsage.output, {
620
+ attributes: runMetricAttributes,
621
+ });
622
+ Sentry.metrics.distribution("agent.run.cache_read", totalUsage.cacheRead, {
623
+ attributes: runMetricAttributes,
624
+ });
625
+ Sentry.metrics.distribution("agent.run.cache_write", totalUsage.cacheWrite, {
626
+ attributes: runMetricAttributes,
627
+ });
628
+ Sentry.metrics.distribution("agent.run.cost", totalUsage.cost.total, {
629
+ attributes: runMetricAttributes,
630
+ });
631
+ Sentry.metrics.gauge("agent.context.utilization", contextTokens / contextWindow, {
632
+ unit: "ratio",
633
+ attributes: runMetricAttributes,
634
+ });
635
+ const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
636
+ if (platform.diagnostics?.showUsageSummary === true) {
637
+ runState.queue.enqueue(() => responseCtx.respondDiagnostic(summary, { style: "muted" }), "usage summary");
638
+ await waitForQueue();
639
+ }
640
+ }
641
+ function reloadSessionMessages(sessionManager, conversationId, agent) {
642
+ const messages = sessionManager.buildSessionContext().messages;
643
+ if (messages.length > 0) {
644
+ agent.state.messages = messages;
645
+ log.logInfo(`[${conversationId}] Reloaded ${messages.length} messages from context`);
646
+ }
647
+ }
648
+ async function prepareRunContext(params) {
649
+ const { message, responseCtx, platform, conversationId, conversationDir, sessionUuid, runState, executor, executionResolver, resolveExecutorForRun, getWorkspacePath, sessionManager, session, agent, setEventContext, setUploadFunction, } = params;
650
+ let workspacePath = params.workspacePath;
651
+ const sessionConversation = message.sessionKey.split(":")[0];
652
+ await mkdir(join(conversationDir, "scratch"), { recursive: true });
653
+ if (executionResolver) {
654
+ await resolveExecutorForRun({
655
+ platform: platform.name,
656
+ userId: message.userId,
657
+ conversationId,
658
+ });
659
+ workspacePath = getWorkspacePath();
660
+ }
661
+ reloadSessionMessages(sessionManager, conversationId, agent);
662
+ const memory = await getMemory(conversationDir);
663
+ const skills = loadMamaSkills(conversationDir, workspacePath);
664
+ const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills);
665
+ session.agent.state.systemPrompt = systemPrompt;
666
+ setEventContext({
667
+ platform: platform.name,
668
+ conversationId,
669
+ conversationKind: message.conversationKind,
670
+ userId: message.userId,
671
+ sessionKey: message.sessionKey,
672
+ threadTs: message.threadTs,
673
+ });
674
+ setUploadFunction(async (filePath, title) => {
675
+ const hostPath = translateToHostPath(filePath, conversationDir, workspacePath, conversationId);
676
+ await responseCtx.uploadFile(hostPath, title);
677
+ });
678
+ resetRunState(runState, responseCtx, sessionConversation, message.userName, sessionUuid);
679
+ const runQueue = createRunQueue(responseCtx);
680
+ runState.queue = runQueue.queue;
681
+ log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
682
+ log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
683
+ const { userMessage, imageAttachments } = buildPromptPayload(message, workspacePath);
684
+ await writePromptDebugContext(conversationDir, systemPrompt, session, userMessage, imageAttachments.length);
685
+ return {
686
+ sessionConversation,
687
+ runQueue,
688
+ userMessage,
689
+ imageAttachments,
690
+ workspacePath,
691
+ };
692
+ }
693
+ function attachSessionEventHandlers(params) {
694
+ const { session, runState, model, agentConfig } = params;
496
695
  session.subscribe(async (event) => {
497
- // Skip if no active run
498
696
  if (!runState.responseCtx || !runState.logCtx || !runState.queue)
499
697
  return;
500
698
  const { responseCtx, logCtx, queue, pendingTools } = runState;
501
699
  const baseAttrs = { channel_id: logCtx.conversationId, session_id: logCtx.sessionId };
502
700
  if (event.type === "tool_execution_start") {
503
- const agentEvent = event;
504
- const args = agentEvent.args;
505
- const label = args.label || agentEvent.toolName;
506
- pendingTools.set(agentEvent.toolCallId, {
507
- toolName: agentEvent.toolName,
508
- args: agentEvent.args,
701
+ const args = (event.args ?? {});
702
+ const label = args.label || event.toolName;
703
+ pendingTools.set(event.toolCallId, {
704
+ toolName: event.toolName,
705
+ args: event.args,
509
706
  startTime: Date.now(),
510
707
  });
511
708
  addLifecycleBreadcrumb("agent.tool.started", {
512
- tool: agentEvent.toolName,
709
+ tool: event.toolName,
513
710
  ...baseAttrs,
514
711
  });
515
- log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
712
+ log.logToolStart(logCtx, event.toolName, label, event.args);
713
+ return;
516
714
  }
517
- else if (event.type === "tool_execution_end") {
518
- const agentEvent = event;
519
- const resultStr = extractToolResultText(agentEvent.result);
520
- const pending = pendingTools.get(agentEvent.toolCallId);
521
- pendingTools.delete(agentEvent.toolCallId);
715
+ if (event.type === "tool_execution_end") {
716
+ const resultStr = extractToolResultText(event.result);
717
+ const pending = pendingTools.get(event.toolCallId);
718
+ pendingTools.delete(event.toolCallId);
522
719
  const durationMs = pending ? Date.now() - pending.startTime : 0;
523
720
  Sentry.metrics.count("agent.tool.calls", 1, {
524
721
  attributes: metricAttributes({
525
- tool: agentEvent.toolName,
526
- error: String(agentEvent.isError),
722
+ tool: event.toolName,
723
+ error: String(event.isError),
527
724
  ...baseAttrs,
528
725
  }),
529
726
  });
530
727
  Sentry.metrics.distribution("agent.tool.duration", durationMs, {
531
728
  unit: "millisecond",
532
729
  attributes: metricAttributes({
533
- tool: agentEvent.toolName,
730
+ tool: event.toolName,
534
731
  ...baseAttrs,
535
732
  }),
536
733
  });
537
734
  addLifecycleBreadcrumb("agent.tool.completed", {
538
- tool: agentEvent.toolName,
539
- error: agentEvent.isError,
735
+ tool: event.toolName,
736
+ error: event.isError,
540
737
  duration_ms: durationMs,
541
738
  ...baseAttrs,
542
739
  });
543
- if (agentEvent.isError) {
544
- log.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);
740
+ if (event.isError) {
741
+ log.logToolError(logCtx, event.toolName, durationMs, resultStr);
545
742
  }
546
743
  else {
547
- log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
744
+ log.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);
548
745
  }
549
- if (!QUIET_TOOLS.has(agentEvent.toolName)) {
746
+ if (shouldSurfaceToolDiagnostic(event.toolName)) {
550
747
  const toolResult = {
551
- toolName: agentEvent.toolName,
748
+ toolName: event.toolName,
552
749
  label: pending?.args ? pending.args.label : undefined,
553
750
  args: pending?.args,
554
751
  result: truncate(resultStr, TOOL_RESULT_DIAGNOSTIC_CAP),
555
- isError: agentEvent.isError,
752
+ isError: event.isError,
556
753
  durationMs,
557
754
  };
558
755
  queue.enqueue(() => responseCtx.respondToolResult(toolResult), "tool result diagnostic");
559
756
  }
560
- if (agentEvent.isError) {
757
+ if (event.isError) {
561
758
  queue.enqueue(() => responseCtx.respond(`_Error: ${truncate(resultStr, 200)}_`), "tool error");
562
759
  }
760
+ return;
563
761
  }
564
- else if (event.type === "message_start") {
565
- const agentEvent = event;
566
- if (agentEvent.message.role === "assistant") {
762
+ if (event.type === "message_start") {
763
+ if (event.message.role === "assistant") {
567
764
  runState.llmCallCount += 1;
568
765
  addLifecycleBreadcrumb("agent.llm.call.started", {
569
766
  call_index: runState.llmCallCount,
@@ -573,11 +770,11 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
573
770
  });
574
771
  log.logResponseStart(logCtx);
575
772
  }
773
+ return;
576
774
  }
577
- else if (event.type === "message_end") {
578
- const agentEvent = event;
579
- if (agentEvent.message.role === "assistant") {
580
- const assistantMsg = agentEvent.message;
775
+ if (event.type === "message_end") {
776
+ if (event.message.role === "assistant") {
777
+ const assistantMsg = event.message;
581
778
  if (assistantMsg.stopReason) {
582
779
  runState.stopReason = assistantMsg.stopReason;
583
780
  }
@@ -594,7 +791,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
594
791
  runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
595
792
  runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
596
793
  runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
597
- // Per-turn LLM metrics
598
794
  const llmAttributes = metricAttributes({
599
795
  provider: model.provider,
600
796
  model: agentConfig.model,
@@ -633,10 +829,9 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
633
829
  cost_total_usd: assistantMsg.usage.cost.total,
634
830
  });
635
831
  }
636
- const content = agentEvent.message.content;
637
832
  const thinkingParts = [];
638
833
  const textParts = [];
639
- for (const part of content) {
834
+ for (const part of assistantMsg.content) {
640
835
  if (part.type === "thinking") {
641
836
  thinkingParts.push(part.thinking);
642
837
  }
@@ -655,270 +850,160 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
655
850
  queue.enqueue(() => responseCtx.respond(text), "response main");
656
851
  }
657
852
  }
853
+ return;
658
854
  }
659
- else if (event.type === "compaction_start") {
855
+ if (event.type === "compaction_start") {
660
856
  log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
661
857
  queue.enqueue(() => responseCtx.respond("_Compacting context..._"), "compaction start");
858
+ return;
662
859
  }
663
- else if (event.type === "compaction_end") {
664
- const compEvent = event;
665
- if (compEvent.result) {
666
- log.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);
860
+ if (event.type === "compaction_end") {
861
+ if (event.result) {
862
+ log.logInfo(`Auto-compaction complete: ${event.result.tokensBefore} tokens compacted`);
667
863
  }
668
- else if (compEvent.aborted) {
864
+ else if (event.aborted) {
669
865
  log.logInfo("Auto-compaction aborted");
670
866
  }
867
+ return;
671
868
  }
672
- else if (event.type === "auto_retry_start") {
673
- const retryEvent = event;
674
- log.logWarning(`Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})`, retryEvent.errorMessage);
675
- queue.enqueue(() => responseCtx.respond(`_Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})..._`), "retry");
869
+ if (event.type === "auto_retry_start") {
870
+ log.logWarning(`Retrying (${event.attempt}/${event.maxAttempts})`, event.errorMessage);
871
+ queue.enqueue(() => responseCtx.respond(`_Retrying (${event.attempt}/${event.maxAttempts})..._`), "retry");
676
872
  }
677
873
  });
874
+ }
875
+ // Cap raw tool output before handing it to adapters. Bash output can be MB; without
876
+ // this each adapter's splitter would fan it out into many sequential platform posts.
877
+ const TOOL_RESULT_DIAGNOSTIC_CAP = 8000;
878
+ function extractToolResultText(result) {
879
+ if (typeof result === "string") {
880
+ return result;
881
+ }
882
+ if (result &&
883
+ typeof result === "object" &&
884
+ "content" in result &&
885
+ Array.isArray(result.content)) {
886
+ const content = result.content;
887
+ const textParts = [];
888
+ for (const part of content) {
889
+ if (part.type === "text" && part.text) {
890
+ textParts.push(part.text);
891
+ }
892
+ }
893
+ if (textParts.length > 0) {
894
+ return textParts.join("\n");
895
+ }
896
+ }
897
+ return JSON.stringify(result);
898
+ }
899
+ // ============================================================================
900
+ // Agent runner
901
+ // ============================================================================
902
+ /**
903
+ * Create a new AgentRunner for a channel.
904
+ * Sets up the session and subscribes to events once.
905
+ *
906
+ * Runner caching is handled by the caller (channelStates in main.ts).
907
+ * This is a stateless factory function.
908
+ */
909
+ export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, provisioner) {
910
+ const agentConfig = loadAgentConfigForConversation(conversationDir);
911
+ // Initialize logger with settings from config
912
+ log.initLogger({
913
+ logFormat: agentConfig.logFormat,
914
+ logLevel: agentConfig.logLevel,
915
+ });
916
+ const workspaceBase = join(conversationDir, "..");
917
+ const { executionResolver, executor, getWorkspacePath, resolveExecutorForRun } = createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, workspaceBase);
918
+ let workspacePath = initialWorkspacePath(sandboxConfig, workspaceBase);
919
+ // Create tools (per-runner, with per-runner upload function setter)
920
+ const { tools, setUploadFunction, setEventContext } = createMamaTools(executor, workspaceDir);
921
+ // Resolve model from config
922
+ // Use 'as any' cast because agentConfig.provider/model are plain strings,
923
+ // while getModel() has constrained generic types for known providers.
924
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
925
+ const model = getModel(agentConfig.provider, agentConfig.model);
926
+ // Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
927
+ const memory = await getMemory(conversationDir);
928
+ const skills = loadMamaSkills(conversationDir, workspacePath);
929
+ const emptyPlatform = {
930
+ name: "chat",
931
+ formattingGuide: "",
932
+ channels: [],
933
+ users: [],
934
+ };
935
+ const systemPrompt = buildSystemPrompt(workspacePath, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
936
+ // Create session manager and settings manager. Top-level/private sessions
937
+ // use the conversation's current pointer; scoped sessions use fixed files.
938
+ // Platform-specific branch/fork behavior is resolved before runner creation.
939
+ const isThread = sessionKey.includes(":");
940
+ const { sessionDir, contextFile, threadRootMessage } = sessionScope;
941
+ const sessionManager = openManagedSession(contextFile, sessionDir, workspacePath);
942
+ const threadSessionName = buildThreadSessionName(threadRootMessage);
943
+ if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
944
+ sessionManager.appendSessionInfo(threadSessionName);
945
+ }
946
+ const sessionUuid = extractSessionUuid(contextFile);
947
+ const settingsManager = SettingsManager.inMemory();
948
+ const { agent, session } = await createConfiguredAgentSession({
949
+ conversationId,
950
+ workspaceDir,
951
+ workspacePath,
952
+ systemPrompt,
953
+ model,
954
+ thinkingLevel: agentConfig.thinkingLevel,
955
+ tools,
956
+ sessionManager,
957
+ settingsManager,
958
+ });
959
+ // Mutable per-run state - event handler references this
960
+ const runState = createRunState();
961
+ attachSessionEventHandlers({ session, runState, model, agentConfig });
678
962
  return {
679
963
  async run(message, responseCtx, platform) {
680
- // Extract conversationId from sessionKey (format: "conversationId:rootTs" or just "conversationId")
681
- const sessionConversation = message.sessionKey.split(":")[0];
682
- // Ensure conversation directory exists
683
- await mkdir(conversationDir, { recursive: true });
684
- if (executionResolver) {
685
- executionResolver.refresh();
686
- activeExecutor = await executionResolver.resolve({
687
- platform: platform.name,
688
- userId: message.userId,
689
- conversationId,
690
- });
691
- workspacePath = getWorkspacePath();
692
- }
693
- // Sync messages from log.jsonl that arrived while we were offline or busy
694
- // Exclude the current message (it will be added via prompt())
695
- // Default sync range is 10 days (handled by syncLogToSessionManager)
696
- // Thread filter ensures only messages from this session's thread are synced
697
- const threadFilter = message.sessionKey.includes(":")
698
- ? { scope: "thread", rootTs, threadTs: message.threadTs }
699
- : { scope: "top-level", rootTs };
700
- const syncedCount = await syncLogToSessionManager(sessionManager, conversationDir, message.id, undefined, threadFilter);
701
- if (syncedCount > 0) {
702
- log.logInfo(`[${conversationId}] Synced ${syncedCount} messages from log.jsonl`);
703
- }
704
- // Reload messages from context.jsonl
705
- // This picks up any messages synced above
706
- const reloadedSession = sessionManager.buildSessionContext();
707
- if (reloadedSession.messages.length > 0) {
708
- agent.state.messages = reloadedSession.messages;
709
- log.logInfo(`[${conversationId}] Reloaded ${reloadedSession.messages.length} messages from context`);
710
- }
711
- // Update system prompt with fresh memory, channel/user info, and skills
712
- const memory = await getMemory(conversationDir);
713
- const skills = loadMamaSkills(conversationDir, workspacePath);
714
- const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills);
715
- session.agent.state.systemPrompt = systemPrompt;
716
- setEventContext({
717
- platform: platform.name,
964
+ const prepared = await prepareRunContext({
965
+ message,
966
+ responseCtx,
967
+ platform,
718
968
  conversationId,
719
- conversationKind: message.conversationKind,
720
- userId: message.userId,
721
- sessionKey: message.sessionKey,
722
- // For Slack scheduled events, preserve thread targeting only when the
723
- // request was created inside an existing thread. Top-level reminders
724
- // should come back as top-level messages.
725
- threadTs: message.threadTs,
726
- });
727
- // Set up file upload function
728
- setUploadFunction(async (filePath, title) => {
729
- const hostPath = translateToHostPath(filePath, conversationDir, workspacePath, conversationId);
730
- await responseCtx.uploadFile(hostPath, title);
969
+ conversationDir,
970
+ sessionUuid,
971
+ runState,
972
+ executor,
973
+ executionResolver,
974
+ resolveExecutorForRun,
975
+ getWorkspacePath,
976
+ sessionManager,
977
+ session,
978
+ agent,
979
+ setEventContext,
980
+ setUploadFunction,
981
+ workspacePath,
731
982
  });
732
- // Reset per-run state
733
- runState.responseCtx = responseCtx;
734
- runState.logCtx = {
735
- conversationId: sessionConversation,
736
- userName: message.userName,
737
- conversationName: undefined,
738
- sessionId: sessionUuid,
739
- };
740
- runState.pendingTools.clear();
741
- runState.totalUsage = {
742
- input: 0,
743
- output: 0,
744
- cacheRead: 0,
745
- cacheWrite: 0,
746
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
747
- };
748
- runState.llmCallCount = 0;
749
- runState.stopReason = "stop";
750
- runState.errorMessage = undefined;
751
- // Create queue for this run
752
- let queueChain = Promise.resolve();
753
- runState.queue = {
754
- enqueue(fn, errorContext) {
755
- queueChain = queueChain.then(async () => {
756
- try {
757
- await fn();
758
- }
759
- catch (err) {
760
- const errMsg = err instanceof Error ? err.message : String(err);
761
- log.logWarning(`API error (${errorContext})`, errMsg);
762
- try {
763
- await responseCtx.respondDiagnostic(`Error: ${errMsg}`, { style: "error" });
764
- }
765
- catch {
766
- // Ignore
767
- }
768
- }
769
- });
770
- },
771
- };
772
- // Log context info
773
- log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
774
- log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
775
- // Build user message with timestamp and username prefix
776
- // Format: "[YYYY-MM-DD HH:MM:SS+HH:MM] [username]: message" so LLM knows when and who
777
- const now = new Date();
778
- const pad = (n) => n.toString().padStart(2, "0");
779
- const offset = -now.getTimezoneOffset();
780
- const offsetSign = offset >= 0 ? "+" : "-";
781
- const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
782
- const offsetMins = pad(Math.abs(offset) % 60);
783
- const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
784
- const threadContext = message.threadTs ? ` [in-thread:${message.threadTs}]` : "";
785
- let userMessage = `[${timestamp}] [${message.userName || "unknown"}]${threadContext}: ${message.text}`;
786
- const imageAttachments = [];
787
- const nonImagePaths = [];
788
- for (const a of message.attachments || []) {
789
- // a.localPath is the path relative to the workspace.
790
- const fullPath = `${workspacePath}/${a.localPath}`;
791
- const mimeType = getImageMimeType(a.localPath);
792
- if (mimeType && existsSync(fullPath)) {
793
- try {
794
- imageAttachments.push({
795
- type: "image",
796
- mimeType,
797
- data: readFileSync(fullPath).toString("base64"),
798
- });
799
- }
800
- catch {
801
- nonImagePaths.push(fullPath);
802
- }
803
- }
804
- else {
805
- nonImagePaths.push(fullPath);
806
- }
807
- }
808
- if (nonImagePaths.length > 0) {
809
- userMessage += `\n\n<slack_attachments>\n${nonImagePaths.join("\n")}\n</slack_attachments>`;
810
- }
811
- // Debug: write context to last_prompt.jsonl
812
- const debugContext = {
813
- systemPrompt,
814
- messages: session.messages,
815
- newUserMessage: userMessage,
816
- imageAttachmentCount: imageAttachments.length,
817
- };
818
- await writeFile(join(conversationDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
983
+ workspacePath = prepared.workspacePath;
819
984
  addLifecycleBreadcrumb("agent.prompt.sent", {
820
985
  provider: model.provider,
821
986
  model: agentConfig.model,
822
- channel_id: sessionConversation,
987
+ channel_id: prepared.sessionConversation,
823
988
  session_id: sessionUuid,
824
989
  attachment_count: message.attachments?.length ?? 0,
825
- image_attachment_count: imageAttachments.length,
990
+ image_attachment_count: prepared.imageAttachments.length,
826
991
  });
827
- await session.prompt(userMessage, imageAttachments.length > 0 ? { images: imageAttachments } : undefined);
992
+ await session.prompt(prepared.userMessage, prepared.imageAttachments.length > 0 ? { images: prepared.imageAttachments } : undefined);
828
993
  // Wait for queued messages
829
- await queueChain;
830
- // Handle error case - update main message and post error to thread
831
- if (runState.stopReason === "error" && runState.errorMessage) {
832
- try {
833
- await responseCtx.replaceResponse("_Sorry, something went wrong_");
834
- await responseCtx.respondDiagnostic(`Error: ${runState.errorMessage}`, {
835
- style: "error",
836
- });
837
- }
838
- catch (err) {
839
- const errMsg = err instanceof Error ? err.message : String(err);
840
- log.logWarning("Failed to post error message", errMsg);
841
- }
842
- }
843
- else {
844
- // Final message update
845
- const messages = session.messages;
846
- const lastAssistant = messages.filter((m) => m.role === "assistant").pop();
847
- const finalText = lastAssistant?.content
848
- .filter((c) => c.type === "text")
849
- .map((c) => c.text)
850
- .join("\n") || "";
851
- // Check for [SILENT] marker - delete message and thread instead of posting
852
- if (finalText.trim() === "[SILENT]" || finalText.trim().startsWith("[SILENT]")) {
853
- try {
854
- await responseCtx.deleteResponse();
855
- log.logInfo("Silent response - deleted message and thread");
856
- }
857
- catch (err) {
858
- const errMsg = err instanceof Error ? err.message : String(err);
859
- log.logWarning("Failed to delete message for silent response", errMsg);
860
- }
861
- }
862
- else if (finalText.trim()) {
863
- try {
864
- await responseCtx.replaceResponse(finalText);
865
- }
866
- catch (err) {
867
- const errMsg = err instanceof Error ? err.message : String(err);
868
- log.logWarning("Failed to replace message with final text", errMsg);
869
- }
870
- }
871
- }
872
- // Log usage summary with context info
873
- if (runState.totalUsage.cost.total > 0) {
874
- // Get last non-aborted assistant message for context calculation
875
- const messages = session.messages;
876
- const lastAssistantMessage = messages
877
- .slice()
878
- .reverse()
879
- .find((m) => m.role === "assistant" && m.stopReason !== "aborted");
880
- const contextTokens = lastAssistantMessage
881
- ? lastAssistantMessage.usage.input +
882
- lastAssistantMessage.usage.output +
883
- lastAssistantMessage.usage.cacheRead +
884
- lastAssistantMessage.usage.cacheWrite
885
- : 0;
886
- const contextWindow = model.contextWindow || 200000;
887
- // Run-level Sentry metrics
888
- const { totalUsage } = runState;
889
- const runMetricAttributes = metricAttributes({
890
- provider: model.provider,
891
- model: agentConfig.model,
892
- channel_id: sessionConversation,
893
- session_id: sessionUuid,
894
- stop_reason: runState.stopReason,
895
- llm_calls: runState.llmCallCount,
896
- });
897
- Sentry.metrics.distribution("agent.run.tokens_in", totalUsage.input, {
898
- attributes: runMetricAttributes,
899
- });
900
- Sentry.metrics.distribution("agent.run.tokens_out", totalUsage.output, {
901
- attributes: runMetricAttributes,
902
- });
903
- Sentry.metrics.distribution("agent.run.cache_read", totalUsage.cacheRead, {
904
- attributes: runMetricAttributes,
905
- });
906
- Sentry.metrics.distribution("agent.run.cache_write", totalUsage.cacheWrite, {
907
- attributes: runMetricAttributes,
908
- });
909
- Sentry.metrics.distribution("agent.run.cost", totalUsage.cost.total, {
910
- attributes: runMetricAttributes,
911
- });
912
- Sentry.metrics.gauge("agent.context.utilization", contextTokens / contextWindow, {
913
- unit: "ratio",
914
- attributes: runMetricAttributes,
915
- });
916
- const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
917
- if (platform.diagnostics?.showUsageSummary === true) {
918
- runState.queue.enqueue(() => responseCtx.respondDiagnostic(summary, { style: "muted" }), "usage summary");
919
- await queueChain;
920
- }
921
- }
994
+ await prepared.runQueue.wait();
995
+ await finalizeRunResponse(responseCtx, session, runState);
996
+ await reportUsageSummary({
997
+ session,
998
+ runState,
999
+ responseCtx,
1000
+ platform,
1001
+ model,
1002
+ agentConfig,
1003
+ sessionConversation: prepared.sessionConversation,
1004
+ sessionUuid,
1005
+ waitForQueue: () => prepared.runQueue.wait(),
1006
+ });
922
1007
  // Clear run state
923
1008
  runState.responseCtx = null;
924
1009
  runState.logCtx = null;