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

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