@geminixiang/mama 0.2.0-beta.0 → 0.2.0-beta.2

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 (147) hide show
  1. package/README.md +94 -27
  2. package/dist/adapter.d.ts +9 -5
  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.map +1 -1
  6. package/dist/adapters/discord/bot.js +9 -6
  7. package/dist/adapters/discord/bot.js.map +1 -1
  8. package/dist/adapters/discord/context.d.ts.map +1 -1
  9. package/dist/adapters/discord/context.js +16 -13
  10. package/dist/adapters/discord/context.js.map +1 -1
  11. package/dist/adapters/slack/bot.d.ts +10 -2
  12. package/dist/adapters/slack/bot.d.ts.map +1 -1
  13. package/dist/adapters/slack/bot.js +196 -32
  14. package/dist/adapters/slack/bot.js.map +1 -1
  15. package/dist/adapters/slack/context.d.ts.map +1 -1
  16. package/dist/adapters/slack/context.js +24 -17
  17. package/dist/adapters/slack/context.js.map +1 -1
  18. package/dist/adapters/telegram/bot.d.ts +2 -0
  19. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  20. package/dist/adapters/telegram/bot.js +109 -29
  21. package/dist/adapters/telegram/bot.js.map +1 -1
  22. package/dist/adapters/telegram/context.d.ts.map +1 -1
  23. package/dist/adapters/telegram/context.js +8 -43
  24. package/dist/adapters/telegram/context.js.map +1 -1
  25. package/dist/adapters/telegram/html.d.ts +3 -0
  26. package/dist/adapters/telegram/html.d.ts.map +1 -0
  27. package/dist/adapters/telegram/html.js +98 -0
  28. package/dist/adapters/telegram/html.js.map +1 -0
  29. package/dist/agent.d.ts +4 -9
  30. package/dist/agent.d.ts.map +1 -1
  31. package/dist/agent.js +141 -92
  32. package/dist/agent.js.map +1 -1
  33. package/dist/bindings.d.ts +44 -0
  34. package/dist/bindings.d.ts.map +1 -0
  35. package/dist/bindings.js +74 -0
  36. package/dist/bindings.js.map +1 -0
  37. package/dist/config.d.ts +7 -0
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +53 -12
  40. package/dist/config.js.map +1 -1
  41. package/dist/context.d.ts +7 -7
  42. package/dist/context.d.ts.map +1 -1
  43. package/dist/context.js +9 -9
  44. package/dist/context.js.map +1 -1
  45. package/dist/events.d.ts +14 -5
  46. package/dist/events.d.ts.map +1 -1
  47. package/dist/events.js +45 -10
  48. package/dist/events.js.map +1 -1
  49. package/dist/execution-resolver.d.ts +20 -0
  50. package/dist/execution-resolver.d.ts.map +1 -0
  51. package/dist/execution-resolver.js +49 -0
  52. package/dist/execution-resolver.js.map +1 -0
  53. package/dist/instrument.d.ts.map +1 -1
  54. package/dist/instrument.js +2 -1
  55. package/dist/instrument.js.map +1 -1
  56. package/dist/link-server.d.ts +17 -0
  57. package/dist/link-server.d.ts.map +1 -0
  58. package/dist/link-server.js +899 -0
  59. package/dist/link-server.js.map +1 -0
  60. package/dist/link-token.d.ts +32 -0
  61. package/dist/link-token.d.ts.map +1 -0
  62. package/dist/link-token.js +68 -0
  63. package/dist/link-token.js.map +1 -0
  64. package/dist/log.d.ts +2 -2
  65. package/dist/log.d.ts.map +1 -1
  66. package/dist/log.js +7 -7
  67. package/dist/log.js.map +1 -1
  68. package/dist/login.d.ts +29 -0
  69. package/dist/login.d.ts.map +1 -0
  70. package/dist/login.js +164 -0
  71. package/dist/login.js.map +1 -0
  72. package/dist/main.d.ts.map +1 -1
  73. package/dist/main.js +226 -55
  74. package/dist/main.js.map +1 -1
  75. package/dist/provisioner.d.ts +52 -0
  76. package/dist/provisioner.d.ts.map +1 -0
  77. package/dist/provisioner.js +291 -0
  78. package/dist/provisioner.js.map +1 -0
  79. package/dist/sandbox/container.d.ts +15 -0
  80. package/dist/sandbox/container.d.ts.map +1 -0
  81. package/dist/sandbox/container.js +122 -0
  82. package/dist/sandbox/container.js.map +1 -0
  83. package/dist/sandbox/errors.d.ts +6 -0
  84. package/dist/sandbox/errors.d.ts.map +1 -0
  85. package/dist/sandbox/errors.js +11 -0
  86. package/dist/sandbox/errors.js.map +1 -0
  87. package/dist/sandbox/firecracker.d.ts +16 -0
  88. package/dist/sandbox/firecracker.d.ts.map +1 -0
  89. package/dist/sandbox/firecracker.js +206 -0
  90. package/dist/sandbox/firecracker.js.map +1 -0
  91. package/dist/sandbox/host.d.ts +10 -0
  92. package/dist/sandbox/host.d.ts.map +1 -0
  93. package/dist/sandbox/host.js +85 -0
  94. package/dist/sandbox/host.js.map +1 -0
  95. package/dist/sandbox/image.d.ts +5 -0
  96. package/dist/sandbox/image.d.ts.map +1 -0
  97. package/dist/sandbox/image.js +30 -0
  98. package/dist/sandbox/image.js.map +1 -0
  99. package/dist/sandbox/index.d.ts +20 -0
  100. package/dist/sandbox/index.d.ts.map +1 -0
  101. package/dist/sandbox/index.js +51 -0
  102. package/dist/sandbox/index.js.map +1 -0
  103. package/dist/sandbox/types.d.ts +51 -0
  104. package/dist/sandbox/types.d.ts.map +1 -0
  105. package/dist/sandbox/types.js +2 -0
  106. package/dist/sandbox/types.js.map +1 -0
  107. package/dist/sandbox/utils.d.ts +4 -0
  108. package/dist/sandbox/utils.d.ts.map +1 -0
  109. package/dist/sandbox/utils.js +51 -0
  110. package/dist/sandbox/utils.js.map +1 -0
  111. package/dist/sandbox.d.ts +1 -39
  112. package/dist/sandbox.d.ts.map +1 -1
  113. package/dist/sandbox.js +1 -286
  114. package/dist/sandbox.js.map +1 -1
  115. package/dist/sentry.d.ts +1 -1
  116. package/dist/sentry.d.ts.map +1 -1
  117. package/dist/sentry.js +4 -2
  118. package/dist/sentry.js.map +1 -1
  119. package/dist/session-store.d.ts +2 -6
  120. package/dist/session-store.d.ts.map +1 -1
  121. package/dist/session-store.js +3 -10
  122. package/dist/session-store.js.map +1 -1
  123. package/dist/store.d.ts +1 -1
  124. package/dist/store.d.ts.map +1 -1
  125. package/dist/store.js +8 -8
  126. package/dist/store.js.map +1 -1
  127. package/dist/tools/event.d.ts +22 -0
  128. package/dist/tools/event.d.ts.map +1 -0
  129. package/dist/tools/event.js +104 -0
  130. package/dist/tools/event.js.map +1 -0
  131. package/dist/tools/index.d.ts +7 -1
  132. package/dist/tools/index.d.ts.map +1 -1
  133. package/dist/tools/index.js +5 -1
  134. package/dist/tools/index.js.map +1 -1
  135. package/dist/ui-copy.d.ts +12 -0
  136. package/dist/ui-copy.d.ts.map +1 -0
  137. package/dist/ui-copy.js +36 -0
  138. package/dist/ui-copy.js.map +1 -0
  139. package/dist/vault-routing.d.ts +9 -0
  140. package/dist/vault-routing.d.ts.map +1 -0
  141. package/dist/vault-routing.js +52 -0
  142. package/dist/vault-routing.js.map +1 -0
  143. package/dist/vault.d.ts +106 -0
  144. package/dist/vault.d.ts.map +1 -0
  145. package/dist/vault.js +389 -0
  146. package/dist/vault.js.map +1 -0
  147. package/package.json +12 -11
package/dist/agent.js CHANGED
@@ -1,16 +1,17 @@
1
1
  import { Agent } from "@mariozechner/pi-agent-core";
2
2
  import { getModel } from "@mariozechner/pi-ai";
3
- import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, loadSkillsFromDir, ModelRegistry, } from "@mariozechner/pi-coding-agent";
3
+ import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, getAgentDir, loadSkillsFromDir, ModelRegistry, } from "@mariozechner/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
7
  import { join } from "path";
8
8
  import { loadAgentConfig } from "./config.js";
9
9
  import { createMamaSettingsManager, syncLogToSessionManager } from "./context.js";
10
+ import { ActorExecutionResolver } from "./execution-resolver.js";
10
11
  import * as log from "./log.js";
11
12
  import { createExecutor } from "./sandbox.js";
12
13
  import { addLifecycleBreadcrumb, metricAttributes } from "./sentry.js";
13
- import { createManagedSessionFileAtPath, extractSessionSuffix, extractSessionUuid, forkThreadSessionFile, getSessionDir, getThreadSessionFile, openManagedSession, resolveChannelSessionFile, resolveManagedSessionFile, tryResolveThreadSession, } from "./session-store.js";
14
+ import { createManagedSessionFileAtPath, extractSessionSuffix, extractSessionUuid, forkThreadSessionFile, getChannelSessionDir, getThreadSessionFile, openManagedSession, resolveChannelSessionFile, resolveManagedSessionFile, tryResolveThreadSession, } from "./session-store.js";
14
15
  import { createMamaTools } from "./tools/index.js";
15
16
  import * as Sentry from "@sentry/node";
16
17
  const IMAGE_MIME_TYPES = {
@@ -23,10 +24,10 @@ const IMAGE_MIME_TYPES = {
23
24
  function getImageMimeType(filename) {
24
25
  return IMAGE_MIME_TYPES[filename.toLowerCase().split(".").pop() || ""];
25
26
  }
26
- async function getMemory(channelDir) {
27
+ async function getMemory(conversationDir) {
27
28
  const parts = [];
28
- // Read workspace-level memory (shared across all channels)
29
- const workspaceMemoryPath = join(channelDir, "..", "MEMORY.md");
29
+ // Read workspace-level memory (shared across all conversations)
30
+ const workspaceMemoryPath = join(conversationDir, "..", "MEMORY.md");
30
31
  if (existsSync(workspaceMemoryPath)) {
31
32
  try {
32
33
  const content = (await readFile(workspaceMemoryPath, "utf-8")).trim();
@@ -38,17 +39,17 @@ async function getMemory(channelDir) {
38
39
  log.logWarning("Failed to read workspace memory", `${workspaceMemoryPath}: ${error}`);
39
40
  }
40
41
  }
41
- // Read channel-specific memory
42
- const channelMemoryPath = join(channelDir, "MEMORY.md");
43
- if (existsSync(channelMemoryPath)) {
42
+ // Read conversation-specific memory
43
+ const conversationMemoryPath = join(conversationDir, "MEMORY.md");
44
+ if (existsSync(conversationMemoryPath)) {
44
45
  try {
45
- const content = (await readFile(channelMemoryPath, "utf-8")).trim();
46
+ const content = (await readFile(conversationMemoryPath, "utf-8")).trim();
46
47
  if (content) {
47
- parts.push(`### Channel-Specific Memory\n${content}`);
48
+ parts.push(`### Conversation-Specific Memory\n${content}`);
48
49
  }
49
50
  }
50
51
  catch (error) {
51
- log.logWarning("Failed to read channel memory", `${channelMemoryPath}: ${error}`);
52
+ log.logWarning("Failed to read conversation memory", `${conversationMemoryPath}: ${error}`);
52
53
  }
53
54
  }
54
55
  if (parts.length === 0) {
@@ -56,12 +57,12 @@ async function getMemory(channelDir) {
56
57
  }
57
58
  return parts.join("\n\n");
58
59
  }
59
- function loadMamaSkills(channelDir, workspacePath) {
60
+ function loadMamaSkills(conversationDir, workspacePath) {
60
61
  const skillMap = new Map();
61
- // channelDir is the host path (e.g., /Users/.../data/C0A34FL8PMH)
62
+ // conversationDir is the host path (e.g., /Users/.../data/C0A34FL8PMH)
62
63
  // hostWorkspacePath is the parent directory on host
63
64
  // workspacePath is the container path (e.g., /workspace)
64
- const hostWorkspacePath = join(channelDir, "..");
65
+ const hostWorkspacePath = join(conversationDir, "..");
65
66
  // Helper to translate host paths to container paths
66
67
  const translatePath = (hostPath) => {
67
68
  if (hostPath.startsWith(hostWorkspacePath)) {
@@ -77,18 +78,19 @@ function loadMamaSkills(channelDir, workspacePath) {
77
78
  skill.baseDir = translatePath(skill.baseDir);
78
79
  skillMap.set(skill.name, skill);
79
80
  }
80
- // Load channel-specific skills (override workspace skills on collision)
81
- const channelSkillsDir = join(channelDir, "skills");
82
- for (const skill of loadSkillsFromDir({ dir: channelSkillsDir, source: "channel" }).skills) {
81
+ // Load conversation-specific skills (override workspace skills on collision)
82
+ const conversationSkillsDir = join(conversationDir, "skills");
83
+ for (const skill of loadSkillsFromDir({ dir: conversationSkillsDir, source: "channel" }).skills) {
83
84
  skill.filePath = translatePath(skill.filePath);
84
85
  skill.baseDir = translatePath(skill.baseDir);
85
86
  skillMap.set(skill.name, skill);
86
87
  }
87
88
  return Array.from(skillMap.values());
88
89
  }
89
- function buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, platform, skills) {
90
- const channelPath = `${workspacePath}/${channelId}`;
91
- const isDocker = sandboxConfig.type === "docker";
90
+ function buildSystemPrompt(workspacePath, conversationId, conversationKind, currentUserId, memory, sandboxConfig, platform, skills) {
91
+ const conversationPath = `${workspacePath}/${conversationId}`;
92
+ const isContainer = sandboxConfig.type === "container" || sandboxConfig.type === "image";
93
+ const isImageSandbox = sandboxConfig.type === "image";
92
94
  const isFirecracker = sandboxConfig.type === "firecracker";
93
95
  // Format channel mappings
94
96
  const channelMappings = platform.channels.length > 0
@@ -98,17 +100,22 @@ function buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, plat
98
100
  const userMappings = platform.users.length > 0
99
101
  ? platform.users.map((u) => `${u.id}\t@${u.userName}\t${u.displayName}`).join("\n")
100
102
  : "(no users loaded)";
101
- const envDescription = isDocker
102
- ? `You are running inside a Docker container (Alpine Linux).
103
+ const envDescription = isImageSandbox
104
+ ? `You are running inside a managed per-user container.
103
105
  - Bash working directory: / (use cd or absolute paths)
104
- - Install tools with: apk add <package>
106
+ - Install tools with the image's package manager
107
+ - Your changes persist for this user's container until it is recreated`
108
+ : isContainer
109
+ ? `You are running inside a shared container.
110
+ - Bash working directory: / (use cd or absolute paths)
111
+ - Install tools with the container's package manager
105
112
  - Your changes persist across sessions`
106
- : isFirecracker
107
- ? `You are running inside a Firecracker microVM.
113
+ : isFirecracker
114
+ ? `You are running inside a Firecracker microVM.
108
115
  - Bash working directory: / (use cd or absolute paths)
109
116
  - Install tools with: apt-get install <package> (Debian-based)
110
117
  - Your changes persist across sessions`
111
- : `You are running directly on the host machine.
118
+ : `You are running directly on the host machine.
112
119
  - Bash working directory: ${process.cwd()}
113
120
  - Be careful with system modifications`;
114
121
  return `You are mama, a ${platform.name} bot assistant. Be concise. No emojis.
@@ -133,20 +140,20 @@ ${envDescription}
133
140
 
134
141
  ## Workspace Layout
135
142
  ${workspacePath}/
136
- ├── MEMORY.md # Global memory (all channels)
143
+ ├── MEMORY.md # Global memory (all conversations)
137
144
  ├── skills/ # Global CLI tools you create
138
- └── ${channelId}/ # This channel
139
- ├── MEMORY.md # Channel-specific memory
145
+ └── ${conversationId}/ # This conversation
146
+ ├── MEMORY.md # Conversation-specific memory
140
147
  ├── log.jsonl # Message history (no tool results)
141
148
  ├── attachments/ # User-shared files
142
149
  ├── scratch/ # Your working directory
143
- └── skills/ # Channel-specific tools
150
+ └── skills/ # Conversation-specific tools
144
151
 
145
152
  ## Skills (Custom CLI Tools)
146
153
  You can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).
147
154
 
148
155
  ### Creating Skills
149
- Store in \`${workspacePath}/skills/<name>/\` (global) or \`${channelPath}/skills/<name>/\` (channel-specific).
156
+ Store in \`${workspacePath}/skills/<name>/\` (global) or \`${conversationPath}/skills/<name>/\` (conversation-specific).
150
157
  Each skill directory needs a \`SKILL.md\` with YAML frontmatter:
151
158
 
152
159
  \`\`\`markdown
@@ -173,17 +180,17 @@ You can schedule events that wake you up at specific times or when external thin
173
180
 
174
181
  **Immediate** - Triggers as soon as harness sees the file. Use in scripts/webhooks to signal external events.
175
182
  \`\`\`json
176
- {"type": "immediate", "platform": "${platform.name}", "channelId": "${channelId}", "text": "New GitHub issue opened"}
183
+ {"type": "immediate", "platform": "${platform.name}", "conversationId": "${conversationId}", "conversationKind": "${conversationKind}", "userId": "${currentUserId ?? "<requester userId>"}", "text": "New GitHub issue opened"}
177
184
  \`\`\`
178
185
 
179
186
  **One-shot** - Triggers once at a specific time. Use for reminders.
180
187
  \`\`\`json
181
- {"type": "one-shot", "platform": "${platform.name}", "channelId": "${channelId}", "text": "Remind Mario about dentist", "at": "2025-12-15T09:00:00+01:00"}
188
+ {"type": "one-shot", "platform": "${platform.name}", "conversationId": "${conversationId}", "conversationKind": "${conversationKind}", "userId": "${currentUserId ?? "<requester userId>"}", "text": "Remind Mario about dentist", "at": "2025-12-15T09:00:00+01:00"}
182
189
  \`\`\`
183
190
 
184
191
  **Periodic** - Triggers on a cron schedule. Use for recurring tasks.
185
192
  \`\`\`json
186
- {"type": "periodic", "platform": "${platform.name}", "channelId": "${channelId}", "text": "Check inbox and summarize", "schedule": "0 9 * * 1-5", "timezone": "${Intl.DateTimeFormat().resolvedOptions().timeZone}"}
193
+ {"type": "periodic", "platform": "${platform.name}", "conversationId": "${conversationId}", "conversationKind": "${conversationKind}", "userId": "${currentUserId ?? "<requester userId>"}", "text": "Check inbox and summarize", "schedule": "0 9 * * 1-5", "timezone": "${Intl.DateTimeFormat().resolvedOptions().timeZone}"}
187
194
  \`\`\`
188
195
 
189
196
  ### Cron Format
@@ -196,14 +203,18 @@ You can schedule events that wake you up at specific times or when external thin
196
203
  ### Timezones
197
204
  All \`at\` timestamps must include offset (e.g., \`+01:00\`). Periodic events use IANA timezone names. The harness runs in ${Intl.DateTimeFormat().resolvedOptions().timeZone}. When users mention times without timezone, assume ${Intl.DateTimeFormat().resolvedOptions().timeZone}.
198
205
 
199
- ### Platform Routing
206
+ ### Platform and Credential Routing
200
207
  Set \`platform\` to the target bot platform (\`${platform.name}\` for this conversation). When only one platform is running, omitting \`platform\` is allowed for backward compatibility, but include it by default to avoid ambiguity.
201
208
 
209
+ Set \`userId\` to the platform userId of whoever asked for the event. When the event fires, tool execution routes using that user's vault selection in per-user modes. In \`container:<name>\`, events use the container's single shared vault.
210
+
211
+ Prefer the \`event\` tool over manually writing JSON files; it fills \`platform\`, \`conversationId\`, \`conversationKind\`, and \`userId\` for the current conversation automatically.
212
+
202
213
  ### Creating Events
203
214
  Use unique filenames to avoid overwriting existing events. Include a timestamp or random suffix:
204
215
  \`\`\`bash
205
216
  cat > ${workspacePath}/events/dentist-reminder-$(date +%s).json << 'EOF'
206
- {"type": "one-shot", "platform": "${platform.name}", "channelId": "${channelId}", "text": "Dentist tomorrow", "at": "2025-12-14T09:00:00+01:00"}
217
+ {"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"}
207
218
  EOF
208
219
  \`\`\`
209
220
  Or check if file exists first before creating.
@@ -232,7 +243,7 @@ Maximum 5 events can be queued. Don't create excessive immediate or periodic eve
232
243
  ## Memory
233
244
  Write to MEMORY.md files to persist context across conversations.
234
245
  - Global (${workspacePath}/MEMORY.md): skills, preferences, project info
235
- - Channel (${channelPath}/MEMORY.md): channel-specific decisions, ongoing work
246
+ - Conversation (${conversationPath}/MEMORY.md): conversation-specific decisions, ongoing work
236
247
  Update when you learn something important or when asked to remember something.
237
248
 
238
249
  ### Current Memory
@@ -250,7 +261,7 @@ Update this file whenever you modify the environment. On fresh container, read i
250
261
  ## Log Queries (for older history)
251
262
  Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
252
263
  The log contains user messages and your final responses (not tool calls/results).
253
- ${isDocker ? "Install jq: apk add jq" : ""}
264
+ ${isContainer ? "Install jq: apk add jq" : ""}
254
265
  ${isFirecracker ? "Install jq: apt-get install jq" : ""}
255
266
 
256
267
  \`\`\`bash
@@ -337,77 +348,100 @@ function formatToolArgsForSlack(_toolName, args) {
337
348
  * Runner caching is handled by the caller (channelStates in main.ts).
338
349
  * This is a stateless factory function.
339
350
  */
340
- export async function createRunner(sandboxConfig, sessionKey, channelId, channelDir, workspaceDir) {
351
+ export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, vaultManager, bindingStore, provisioner) {
341
352
  const agentConfig = loadAgentConfig(workspaceDir);
342
353
  // Initialize logger with settings from config
343
354
  log.initLogger({
344
355
  logFormat: agentConfig.logFormat,
345
356
  logLevel: agentConfig.logLevel,
346
357
  });
347
- const executor = createExecutor(sandboxConfig);
348
- const workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, ""));
358
+ const executionResolver = vaultManager &&
359
+ sandboxConfig.type !== "host" &&
360
+ (vaultManager.isEnabled() ||
361
+ !!bindingStore ||
362
+ sandboxConfig.type === "container" ||
363
+ sandboxConfig.type === "image")
364
+ ? new ActorExecutionResolver(sandboxConfig, vaultManager, bindingStore, provisioner)
365
+ : undefined;
366
+ let activeExecutor = executionResolver !== undefined
367
+ ? createExecutor({ type: "host" })
368
+ : createExecutor(sandboxConfig);
369
+ const executor = {
370
+ exec(command, options) {
371
+ return activeExecutor.exec(command, options);
372
+ },
373
+ getWorkspacePath(hostPath) {
374
+ return activeExecutor.getWorkspacePath(hostPath);
375
+ },
376
+ getSandboxConfig() {
377
+ return activeExecutor.getSandboxConfig();
378
+ },
379
+ };
380
+ const workspaceBase = conversationDir.replace(`/${conversationId}`, "");
381
+ const getWorkspacePath = () => executor.getWorkspacePath(workspaceBase);
382
+ let workspacePath = getWorkspacePath();
349
383
  // Create tools (per-runner, with per-runner upload function setter)
350
- const { tools, setUploadFunction } = createMamaTools(executor);
384
+ const { tools, setUploadFunction, setEventContext } = createMamaTools(executor, workspaceDir);
351
385
  // Resolve model from config
352
386
  // Use 'as any' cast because agentConfig.provider/model are plain strings,
353
387
  // while getModel() has constrained generic types for known providers.
354
388
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
355
389
  const model = getModel(agentConfig.provider, agentConfig.model);
356
390
  // Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
357
- const memory = await getMemory(channelDir);
358
- const skills = loadMamaSkills(channelDir, workspacePath);
391
+ const memory = await getMemory(conversationDir);
392
+ const skills = loadMamaSkills(conversationDir, workspacePath);
359
393
  const emptyPlatform = {
360
394
  name: "slack",
361
395
  formattingGuide: "",
362
396
  channels: [],
363
397
  users: [],
364
398
  };
365
- const systemPrompt = buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, emptyPlatform, skills);
399
+ const systemPrompt = buildSystemPrompt(workspacePath, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
366
400
  // Create session manager and settings manager
367
- // Channel sessions use {channelDir}/sessions/current.
368
- // Thread sessions use fixed files: {channelDir}/sessions/{threadTs}.jsonl
369
- const sessionDir = getSessionDir(channelDir, sessionKey);
401
+ // Conversation sessions use {conversationDir}/sessions/current.
402
+ // Thread sessions use fixed files: {conversationDir}/sessions/{threadTs}.jsonl
403
+ const sessionDir = getChannelSessionDir(conversationDir);
370
404
  const isThread = sessionKey.includes(":");
371
405
  let sessionManager;
372
406
  let contextFile;
373
407
  if (isThread) {
374
- const threadFile = getThreadSessionFile(channelDir, sessionKey);
408
+ const threadFile = getThreadSessionFile(conversationDir, sessionKey);
375
409
  const existing = tryResolveThreadSession(threadFile);
376
410
  if (existing) {
377
411
  contextFile = existing;
378
- sessionManager = openManagedSession(contextFile, sessionDir, channelDir);
412
+ sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
379
413
  }
380
414
  else {
381
- const channelSource = resolveChannelSessionFile(channelDir);
382
- if (channelSource) {
415
+ const conversationSource = resolveChannelSessionFile(conversationDir);
416
+ if (conversationSource) {
383
417
  try {
384
- contextFile = forkThreadSessionFile(channelSource, threadFile, channelDir);
385
- sessionManager = openManagedSession(contextFile, sessionDir, channelDir);
418
+ contextFile = forkThreadSessionFile(conversationSource, threadFile, conversationDir);
419
+ sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
386
420
  }
387
421
  catch {
388
- contextFile = createManagedSessionFileAtPath(threadFile, channelDir);
389
- sessionManager = openManagedSession(contextFile, sessionDir, channelDir);
422
+ contextFile = createManagedSessionFileAtPath(threadFile, conversationDir);
423
+ sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
390
424
  }
391
425
  }
392
426
  else {
393
- contextFile = createManagedSessionFileAtPath(threadFile, channelDir);
394
- sessionManager = openManagedSession(contextFile, sessionDir, channelDir);
427
+ contextFile = createManagedSessionFileAtPath(threadFile, conversationDir);
428
+ sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
395
429
  }
396
430
  }
397
431
  }
398
432
  else {
399
- // Channel/DM session: normal resolve
400
- contextFile = resolveManagedSessionFile(sessionDir, channelDir);
401
- sessionManager = openManagedSession(contextFile, sessionDir, channelDir);
433
+ // Direct/shared session: normal resolve
434
+ contextFile = resolveManagedSessionFile(sessionDir, conversationDir);
435
+ sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
402
436
  }
403
437
  const sessionUuid = extractSessionUuid(contextFile);
404
438
  // Used for Slack thread filtering — for non-Slack platforms this is effectively a no-op
405
439
  const rootTs = extractSessionSuffix(sessionKey);
406
- const settingsManager = createMamaSettingsManager(join(channelDir, ".."));
440
+ const settingsManager = createMamaSettingsManager(join(conversationDir, ".."));
407
441
  // Create AuthStorage and ModelRegistry
408
442
  // Auth stored outside workspace so agent can't access it
409
443
  const authStorage = AuthStorage.create(join(homedir(), ".pi", "mama", "auth.json"));
410
- const modelRegistry = new ModelRegistry(authStorage);
444
+ const modelRegistry = ModelRegistry.create(authStorage);
411
445
  // Create agent
412
446
  const agent = new Agent({
413
447
  initialState: {
@@ -427,14 +461,15 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
427
461
  // Load existing messages
428
462
  const loadedSession = sessionManager.buildSessionContext();
429
463
  if (loadedSession.messages.length > 0) {
430
- agent.replaceMessages(loadedSession.messages);
431
- log.logInfo(`[${channelId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
464
+ agent.state.messages = loadedSession.messages;
465
+ log.logInfo(`[${conversationId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
432
466
  }
433
467
  // Load extensions, skills, prompts, themes via DefaultResourceLoader
434
468
  // This reads ~/.pi/agent/settings.json (packages, extensions enable/disable)
435
469
  // and discovers resources from standard locations + npm/git packages.
436
470
  const resourceLoader = new DefaultResourceLoader({
437
471
  cwd: workspaceDir,
472
+ agentDir: getAgentDir(),
438
473
  systemPrompt,
439
474
  });
440
475
  try {
@@ -442,13 +477,13 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
442
477
  const extResult = resourceLoader.getExtensions();
443
478
  if (extResult.errors.length > 0) {
444
479
  for (const err of extResult.errors) {
445
- log.logWarning(`[${channelId}] Extension load error: ${err.path}`, err.error);
480
+ log.logWarning(`[${conversationId}] Extension load error: ${err.path}`, err.error);
446
481
  }
447
482
  }
448
- log.logInfo(`[${channelId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((e) => e.path).join(", ")}`);
483
+ log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((e) => e.path).join(", ")}`);
449
484
  }
450
485
  catch (error) {
451
- log.logWarning(`[${channelId}] Failed to load resources`, String(error));
486
+ log.logWarning(`[${conversationId}] Failed to load resources`, String(error));
452
487
  }
453
488
  const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
454
489
  // Create AgentSession wrapper
@@ -484,7 +519,7 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
484
519
  if (!runState.responseCtx || !runState.logCtx || !runState.queue)
485
520
  return;
486
521
  const { responseCtx, logCtx, queue, pendingTools } = runState;
487
- const baseAttrs = { channel_id: logCtx.channelId, session_id: logCtx.sessionId };
522
+ const baseAttrs = { channel_id: logCtx.conversationId, session_id: logCtx.sessionId };
488
523
  if (event.type === "tool_execution_start") {
489
524
  const agentEvent = event;
490
525
  const args = agentEvent.args;
@@ -694,10 +729,18 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
694
729
  };
695
730
  return {
696
731
  async run(message, responseCtx, platform) {
697
- // Extract channelId from sessionKey (format: "channelId:rootTs" or just "channelId")
698
- const sessionChannel = message.sessionKey.split(":")[0];
699
- // Ensure channel directory exists
700
- await mkdir(channelDir, { recursive: true });
732
+ // Extract conversationId from sessionKey (format: "conversationId:rootTs" or just "conversationId")
733
+ const sessionConversation = message.sessionKey.split(":")[0];
734
+ // Ensure conversation directory exists
735
+ await mkdir(conversationDir, { recursive: true });
736
+ if (executionResolver) {
737
+ executionResolver.refresh();
738
+ activeExecutor = await executionResolver.resolve({
739
+ platform: platform.name,
740
+ userId: message.userId,
741
+ });
742
+ workspacePath = getWorkspacePath();
743
+ }
701
744
  // Sync messages from log.jsonl that arrived while we were offline or busy
702
745
  // Exclude the current message (it will be added via prompt())
703
746
  // Default sync range is 10 days (handled by syncLogToSessionManager)
@@ -705,33 +748,39 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
705
748
  const threadFilter = message.sessionKey.includes(":")
706
749
  ? { scope: "thread", rootTs, threadTs: message.threadTs }
707
750
  : { scope: "top-level", rootTs };
708
- const syncedCount = await syncLogToSessionManager(sessionManager, channelDir, message.id, undefined, threadFilter);
751
+ const syncedCount = await syncLogToSessionManager(sessionManager, conversationDir, message.id, undefined, threadFilter);
709
752
  if (syncedCount > 0) {
710
- log.logInfo(`[${channelId}] Synced ${syncedCount} messages from log.jsonl`);
753
+ log.logInfo(`[${conversationId}] Synced ${syncedCount} messages from log.jsonl`);
711
754
  }
712
755
  // Reload messages from context.jsonl
713
756
  // This picks up any messages synced above
714
757
  const reloadedSession = sessionManager.buildSessionContext();
715
758
  if (reloadedSession.messages.length > 0) {
716
- agent.replaceMessages(reloadedSession.messages);
717
- log.logInfo(`[${channelId}] Reloaded ${reloadedSession.messages.length} messages from context`);
759
+ agent.state.messages = reloadedSession.messages;
760
+ log.logInfo(`[${conversationId}] Reloaded ${reloadedSession.messages.length} messages from context`);
718
761
  }
719
762
  // Update system prompt with fresh memory, channel/user info, and skills
720
- const memory = await getMemory(channelDir);
721
- const skills = loadMamaSkills(channelDir, workspacePath);
722
- const systemPrompt = buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, platform, skills);
723
- session.agent.setSystemPrompt(systemPrompt);
763
+ const memory = await getMemory(conversationDir);
764
+ const skills = loadMamaSkills(conversationDir, workspacePath);
765
+ const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills);
766
+ session.agent.state.systemPrompt = systemPrompt;
767
+ setEventContext({
768
+ platform: platform.name,
769
+ conversationId,
770
+ conversationKind: message.conversationKind,
771
+ userId: message.userId,
772
+ });
724
773
  // Set up file upload function
725
774
  setUploadFunction(async (filePath, title) => {
726
- const hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);
775
+ const hostPath = translateToHostPath(filePath, conversationDir, workspacePath, conversationId);
727
776
  await responseCtx.uploadFile(hostPath, title);
728
777
  });
729
778
  // Reset per-run state
730
779
  runState.responseCtx = responseCtx;
731
780
  runState.logCtx = {
732
- channelId: sessionChannel,
781
+ conversationId: sessionConversation,
733
782
  userName: message.userName,
734
- channelName: undefined,
783
+ conversationName: undefined,
735
784
  sessionId: sessionUuid,
736
785
  };
737
786
  runState.pendingTools.clear();
@@ -793,7 +842,7 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
793
842
  const imageAttachments = [];
794
843
  const nonImagePaths = [];
795
844
  for (const a of message.attachments || []) {
796
- // a.localPath is the path relative to the workspace (same as old a.local)
845
+ // a.localPath is the path relative to the workspace.
797
846
  const fullPath = `${workspacePath}/${a.localPath}`;
798
847
  const mimeType = getImageMimeType(a.localPath);
799
848
  if (mimeType && existsSync(fullPath)) {
@@ -822,11 +871,11 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
822
871
  newUserMessage: userMessage,
823
872
  imageAttachmentCount: imageAttachments.length,
824
873
  };
825
- await writeFile(join(channelDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
874
+ await writeFile(join(conversationDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
826
875
  addLifecycleBreadcrumb("agent.prompt.sent", {
827
876
  provider: model.provider,
828
877
  model: agentConfig.model,
829
- channel_id: sessionChannel,
878
+ channel_id: sessionConversation,
830
879
  session_id: sessionUuid,
831
880
  attachment_count: message.attachments?.length ?? 0,
832
881
  image_attachment_count: imageAttachments.length,
@@ -901,7 +950,7 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
901
950
  const runMetricAttributes = metricAttributes({
902
951
  provider: model.provider,
903
952
  model: agentConfig.model,
904
- channel_id: sessionChannel,
953
+ channel_id: sessionConversation,
905
954
  session_id: sessionUuid,
906
955
  stop_reason: runState.stopReason,
907
956
  llm_calls: runState.llmCallCount,
@@ -960,14 +1009,14 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
960
1009
  /**
961
1010
  * Translate container path back to host path for file operations
962
1011
  */
963
- function translateToHostPath(containerPath, channelDir, workspacePath, channelId) {
1012
+ function translateToHostPath(containerPath, conversationDir, workspacePath, conversationId) {
964
1013
  if (workspacePath === "/workspace") {
965
- const prefix = `/workspace/${channelId}/`;
1014
+ const prefix = `/workspace/${conversationId}/`;
966
1015
  if (containerPath.startsWith(prefix)) {
967
- return join(channelDir, containerPath.slice(prefix.length));
1016
+ return join(conversationDir, containerPath.slice(prefix.length));
968
1017
  }
969
1018
  if (containerPath.startsWith("/workspace/")) {
970
- return join(channelDir, "..", containerPath.slice("/workspace/".length));
1019
+ return join(conversationDir, "..", containerPath.slice("/workspace/".length));
971
1020
  }
972
1021
  }
973
1022
  return containerPath;