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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +133 -78
  2. package/dist/adapter.d.ts +22 -10
  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 +10 -7
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +228 -69
  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 +92 -34
  11. package/dist/adapters/discord/context.js.map +1 -1
  12. package/dist/adapters/shared.d.ts +23 -0
  13. package/dist/adapters/shared.d.ts.map +1 -0
  14. package/dist/adapters/shared.js +57 -0
  15. package/dist/adapters/shared.js.map +1 -0
  16. package/dist/adapters/slack/bot.d.ts +19 -11
  17. package/dist/adapters/slack/bot.d.ts.map +1 -1
  18. package/dist/adapters/slack/bot.js +356 -96
  19. package/dist/adapters/slack/bot.js.map +1 -1
  20. package/dist/adapters/slack/branch-manager.d.ts +21 -0
  21. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  22. package/dist/adapters/slack/branch-manager.js +96 -0
  23. package/dist/adapters/slack/branch-manager.js.map +1 -0
  24. package/dist/adapters/slack/context.d.ts.map +1 -1
  25. package/dist/adapters/slack/context.js +100 -67
  26. package/dist/adapters/slack/context.js.map +1 -1
  27. package/dist/adapters/slack/session.d.ts +3 -0
  28. package/dist/adapters/slack/session.d.ts.map +1 -0
  29. package/dist/adapters/slack/session.js +16 -0
  30. package/dist/adapters/slack/session.js.map +1 -0
  31. package/dist/adapters/telegram/bot.d.ts +4 -2
  32. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  33. package/dist/adapters/telegram/bot.js +141 -74
  34. package/dist/adapters/telegram/bot.js.map +1 -1
  35. package/dist/adapters/telegram/context.d.ts.map +1 -1
  36. package/dist/adapters/telegram/context.js +49 -109
  37. package/dist/adapters/telegram/context.js.map +1 -1
  38. package/dist/adapters/telegram/html.d.ts +3 -0
  39. package/dist/adapters/telegram/html.d.ts.map +1 -0
  40. package/dist/adapters/telegram/html.js +98 -0
  41. package/dist/adapters/telegram/html.js.map +1 -0
  42. package/dist/agent.d.ts +4 -11
  43. package/dist/agent.d.ts.map +1 -1
  44. package/dist/agent.js +116 -196
  45. package/dist/agent.js.map +1 -1
  46. package/dist/bindings.d.ts +1 -20
  47. package/dist/bindings.d.ts.map +1 -1
  48. package/dist/bindings.js +1 -21
  49. package/dist/bindings.js.map +1 -1
  50. package/dist/config.d.ts +9 -27
  51. package/dist/config.d.ts.map +1 -1
  52. package/dist/config.js +89 -63
  53. package/dist/config.js.map +1 -1
  54. package/dist/context.d.ts +13 -3
  55. package/dist/context.d.ts.map +1 -1
  56. package/dist/context.js +102 -18
  57. package/dist/context.js.map +1 -1
  58. package/dist/events.d.ts +18 -6
  59. package/dist/events.d.ts.map +1 -1
  60. package/dist/events.js +86 -35
  61. package/dist/events.js.map +1 -1
  62. package/dist/execution-resolver.d.ts.map +1 -1
  63. package/dist/execution-resolver.js +1 -3
  64. package/dist/execution-resolver.js.map +1 -1
  65. package/dist/instrument.d.ts.map +1 -1
  66. package/dist/instrument.js +5 -11
  67. package/dist/instrument.js.map +1 -1
  68. package/dist/{login.d.ts → login/index.d.ts} +2 -2
  69. package/dist/login/index.d.ts.map +1 -0
  70. package/dist/{login.js → login/index.js} +2 -2
  71. package/dist/login/index.js.map +1 -0
  72. package/dist/{link-server.d.ts → login/portal.d.ts} +6 -4
  73. package/dist/login/portal.d.ts.map +1 -0
  74. package/dist/login/portal.js +1453 -0
  75. package/dist/login/portal.js.map +1 -0
  76. package/dist/{link-token.d.ts → login/session.d.ts} +1 -1
  77. package/dist/login/session.d.ts.map +1 -0
  78. package/dist/{link-token.js → login/session.js} +1 -1
  79. package/dist/login/session.js.map +1 -0
  80. package/dist/main.d.ts.map +1 -1
  81. package/dist/main.js +175 -119
  82. package/dist/main.js.map +1 -1
  83. package/dist/provisioner.d.ts +17 -43
  84. package/dist/provisioner.d.ts.map +1 -1
  85. package/dist/provisioner.js +84 -50
  86. package/dist/provisioner.js.map +1 -1
  87. package/dist/sandbox/host.d.ts +0 -2
  88. package/dist/sandbox/host.d.ts.map +1 -1
  89. package/dist/sandbox/host.js +1 -5
  90. package/dist/sandbox/host.js.map +1 -1
  91. package/dist/sentry.d.ts.map +1 -1
  92. package/dist/sentry.js +2 -0
  93. package/dist/sentry.js.map +1 -1
  94. package/dist/session-policy.d.ts +13 -0
  95. package/dist/session-policy.d.ts.map +1 -0
  96. package/dist/session-policy.js +23 -0
  97. package/dist/session-policy.js.map +1 -0
  98. package/dist/session-store.d.ts +27 -1
  99. package/dist/session-store.d.ts.map +1 -1
  100. package/dist/session-store.js +162 -9
  101. package/dist/session-store.js.map +1 -1
  102. package/dist/session-view/command.d.ts +5 -0
  103. package/dist/session-view/command.d.ts.map +1 -0
  104. package/dist/session-view/command.js +11 -0
  105. package/dist/session-view/command.js.map +1 -0
  106. package/dist/session-view/portal.d.ts +9 -0
  107. package/dist/session-view/portal.d.ts.map +1 -0
  108. package/dist/session-view/portal.js +766 -0
  109. package/dist/session-view/portal.js.map +1 -0
  110. package/dist/session-view/service.d.ts +34 -0
  111. package/dist/session-view/service.d.ts.map +1 -0
  112. package/dist/session-view/service.js +380 -0
  113. package/dist/session-view/service.js.map +1 -0
  114. package/dist/session-view/store.d.ts +16 -0
  115. package/dist/session-view/store.d.ts.map +1 -0
  116. package/dist/session-view/store.js +38 -0
  117. package/dist/session-view/store.js.map +1 -0
  118. package/dist/store.d.ts +3 -6
  119. package/dist/store.d.ts.map +1 -1
  120. package/dist/store.js +15 -35
  121. package/dist/store.js.map +1 -1
  122. package/dist/tools/event.d.ts +3 -0
  123. package/dist/tools/event.d.ts.map +1 -1
  124. package/dist/tools/event.js +27 -8
  125. package/dist/tools/event.js.map +1 -1
  126. package/dist/tools/index.d.ts +3 -0
  127. package/dist/tools/index.d.ts.map +1 -1
  128. package/dist/tools/index.js +2 -2
  129. package/dist/tools/index.js.map +1 -1
  130. package/dist/ui-copy.d.ts +1 -0
  131. package/dist/ui-copy.d.ts.map +1 -1
  132. package/dist/ui-copy.js +3 -0
  133. package/dist/ui-copy.js.map +1 -1
  134. package/dist/vault-routing.d.ts +1 -2
  135. package/dist/vault-routing.d.ts.map +1 -1
  136. package/dist/vault-routing.js +1 -7
  137. package/dist/vault-routing.js.map +1 -1
  138. package/package.json +1 -1
  139. package/dist/link-server.d.ts.map +0 -1
  140. package/dist/link-server.js +0 -839
  141. package/dist/link-server.js.map +0 -1
  142. package/dist/link-token.d.ts.map +0 -1
  143. package/dist/link-token.js.map +0 -1
  144. package/dist/login.d.ts.map +0 -1
  145. package/dist/login.js.map +0 -1
  146. package/dist/vault.test.d.ts +0 -2
  147. package/dist/vault.test.d.ts.map +0 -1
  148. package/dist/vault.test.js +0 -67
  149. package/dist/vault.test.js.map +0 -1
package/dist/agent.js CHANGED
@@ -11,7 +11,7 @@ import { ActorExecutionResolver } from "./execution-resolver.js";
11
11
  import * as log from "./log.js";
12
12
  import { createExecutor } from "./sandbox.js";
13
13
  import { addLifecycleBreadcrumb, metricAttributes } from "./sentry.js";
14
- import { createManagedSessionFileAtPath, extractSessionSuffix, extractSessionUuid, forkThreadSessionFile, getChannelSessionDir, getThreadSessionFile, openManagedSession, resolveChannelSessionFile, resolveManagedSessionFile, tryResolveThreadSession, } from "./session-store.js";
14
+ import { extractSessionSuffix, extractSessionUuid, openManagedSession, } from "./session-store.js";
15
15
  import { createMamaTools } from "./tools/index.js";
16
16
  import * as Sentry from "@sentry/node";
17
17
  const IMAGE_MIME_TYPES = {
@@ -24,6 +24,13 @@ const IMAGE_MIME_TYPES = {
24
24
  function getImageMimeType(filename) {
25
25
  return IMAGE_MIME_TYPES[filename.toLowerCase().split(".").pop() || ""];
26
26
  }
27
+ function buildThreadSessionName(message) {
28
+ const text = message?.text?.trim();
29
+ if (!text)
30
+ return undefined;
31
+ const userLabel = message?.userName || message?.user || "unknown";
32
+ return `[${userLabel}]: ${text}`;
33
+ }
27
34
  async function getMemory(conversationDir) {
28
35
  const parts = [];
29
36
  // Read workspace-level memory (shared across all conversations)
@@ -87,11 +94,12 @@ function loadMamaSkills(conversationDir, workspacePath) {
87
94
  }
88
95
  return Array.from(skillMap.values());
89
96
  }
90
- function buildSystemPrompt(workspacePath, conversationId, currentUserId, memory, sandboxConfig, platform, skills) {
97
+ function buildSystemPrompt(workspacePath, conversationId, conversationKind, currentUserId, memory, sandboxConfig, platform, skills) {
91
98
  const conversationPath = `${workspacePath}/${conversationId}`;
92
99
  const isContainer = sandboxConfig.type === "container" || sandboxConfig.type === "image";
100
+ const isImageSandbox = sandboxConfig.type === "image";
93
101
  const isFirecracker = sandboxConfig.type === "firecracker";
94
- // Format platform conversation mappings
102
+ // Format channel mappings
95
103
  const channelMappings = platform.channels.length > 0
96
104
  ? platform.channels.map((c) => `${c.id}\t#${c.name}`).join("\n")
97
105
  : "(no channels loaded)";
@@ -99,17 +107,22 @@ function buildSystemPrompt(workspacePath, conversationId, currentUserId, memory,
99
107
  const userMappings = platform.users.length > 0
100
108
  ? platform.users.map((u) => `${u.id}\t@${u.userName}\t${u.displayName}`).join("\n")
101
109
  : "(no users loaded)";
102
- const envDescription = isContainer
103
- ? `You are running inside a container (Docker runtime, Alpine Linux).
110
+ const envDescription = isImageSandbox
111
+ ? `You are running inside a managed per-user container.
112
+ - Bash working directory: / (use cd or absolute paths)
113
+ - Install tools with the image's package manager
114
+ - Your changes persist for this user's container until it is recreated`
115
+ : isContainer
116
+ ? `You are running inside a shared container.
104
117
  - Bash working directory: / (use cd or absolute paths)
105
- - Install tools with: apk add <package>
118
+ - Install tools with the container's package manager
106
119
  - Your changes persist across sessions`
107
- : isFirecracker
108
- ? `You are running inside a Firecracker microVM.
120
+ : isFirecracker
121
+ ? `You are running inside a Firecracker microVM.
109
122
  - Bash working directory: / (use cd or absolute paths)
110
123
  - Install tools with: apt-get install <package> (Debian-based)
111
124
  - Your changes persist across sessions`
112
- : `You are running directly on the host machine.
125
+ : `You are running directly on the host machine.
113
126
  - Bash working directory: ${process.cwd()}
114
127
  - Be careful with system modifications`;
115
128
  return `You are mama, a ${platform.name} bot assistant. Be concise. No emojis.
@@ -117,8 +130,11 @@ function buildSystemPrompt(workspacePath, conversationId, currentUserId, memory,
117
130
  ## Context
118
131
  - For current date/time, use: date
119
132
  - You have access to previous conversation context including tool results from prior turns.
120
- - For older history beyond your context, search log.jsonl (contains user messages and your final responses, but not tool results).
121
- - User messages include a \`[in-thread:TS]\` marker when sent from within a Slack thread (TS is the root message timestamp). Without this marker, the message is a top-level channel message.
133
+ - For older human-readable history beyond your context, search \`log.jsonl\` (contains user messages and your final responses, but not tool results).
134
+ - Structured session history with tool results lives in \`${conversationPath}/sessions/\`.
135
+ - The active top-level session is selected by \`${conversationPath}/sessions/current\`, which points to a timestamped \`.jsonl\` file in the same directory.
136
+ - Scoped/thread sessions use fixed files at \`${conversationPath}/sessions/<scope_id>.jsonl\` (for example \`${conversationPath}/sessions/1777386320.800769.jsonl\`).
137
+ - User messages include a \`[in-thread:TS]\` marker when sent from within a platform thread/reply (TS is the thread or parent message identifier). Without this marker, the message is a top-level conversation message.
122
138
 
123
139
  ${platform.formattingGuide}
124
140
 
@@ -134,11 +150,15 @@ ${envDescription}
134
150
 
135
151
  ## Workspace Layout
136
152
  ${workspacePath}/
137
- ├── MEMORY.md # Global memory (all channels)
153
+ ├── MEMORY.md # Global memory (all conversations)
138
154
  ├── skills/ # Global CLI tools you create
139
155
  └── ${conversationId}/ # This conversation
140
156
  ├── MEMORY.md # Conversation-specific memory
141
- ├── log.jsonl # Message history (no tool results)
157
+ ├── log.jsonl # Human-readable message history (no tool results)
158
+ ├── sessions/ # Structured session history used for context reconstruction
159
+ │ ├── current # Active top-level session pointer
160
+ │ ├── <timestamp>_<id>.jsonl # Top-level session files
161
+ │ └── <scope_id>.jsonl # Scoped thread/reply session files
142
162
  ├── attachments/ # User-shared files
143
163
  ├── scratch/ # Your working directory
144
164
  └── skills/ # Conversation-specific tools
@@ -174,21 +194,19 @@ You can schedule events that wake you up at specific times or when external thin
174
194
 
175
195
  **Immediate** - Triggers as soon as harness sees the file. Use in scripts/webhooks to signal external events.
176
196
  \`\`\`json
177
- {"type": "immediate", "platform": "${platform.name}", "channelId": "${conversationId}", "userId": "<requester userId>", "text": "New GitHub issue opened"}
197
+ {"type": "immediate", "platform": "${platform.name}", "conversationId": "${conversationId}", "conversationKind": "${conversationKind}", "userId": "${currentUserId ?? "<requester userId>"}", "text": "New GitHub issue opened"}
178
198
  \`\`\`
179
199
 
180
200
  **One-shot** - Triggers once at a specific time. Use for reminders.
181
201
  \`\`\`json
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"}
202
+ {"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"}
183
203
  \`\`\`
184
204
 
185
205
  **Periodic** - Triggers on a cron schedule. Use for recurring tasks.
186
206
  \`\`\`json
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}"}
207
+ {"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}"}
188
208
  \`\`\`
189
209
 
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
-
192
210
  ### Cron Format
193
211
  \`minute hour day-of-month month day-of-week\`
194
212
  - \`0 9 * * *\` = daily at 9:00
@@ -199,23 +217,18 @@ Set \`userId\` to the platform userId of whoever asked for the event (look it up
199
217
  ### Timezones
200
218
  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}.
201
219
 
202
- ### Platform Routing
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.
220
+ ### Platform and Credential Routing
221
+ Set \`platform\` to the target bot platform (\`${platform.name}\` for this conversation). Include it explicitly to avoid ambiguity.
204
222
 
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.
223
+ 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.
208
224
 
209
- Current conversation defaults:
210
- - \`platform\`: \`${platform.name}\`
211
- - \`channelId\`: \`${conversationId}\`
212
- - \`userId\`: \`${currentUserId ?? "unknown"}\`
225
+ Prefer the \`event\` tool over manually writing JSON files; it fills \`platform\`, \`conversationId\`, \`conversationKind\`, and \`userId\` for the current conversation automatically.
213
226
 
214
- Manual file creation is fallback only:
227
+ ### Creating Events
215
228
  Use unique filenames to avoid overwriting existing events. Include a timestamp or random suffix:
216
229
  \`\`\`bash
217
230
  cat > ${workspacePath}/events/dentist-reminder-$(date +%s).json << 'EOF'
218
- {"type": "one-shot", "platform": "${platform.name}", "channelId": "${conversationId}", "userId": "<requester userId>", "text": "Dentist tomorrow", "at": "2025-12-14T09:00:00+01:00"}
231
+ {"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"}
219
232
  EOF
220
233
  \`\`\`
221
234
  Or check if file exists first before creating.
@@ -252,7 +265,7 @@ ${memory}
252
265
 
253
266
  ## System Configuration Log
254
267
  Maintain ${workspacePath}/SYSTEM.md to log all environment modifications:
255
- - Installed packages (apk add, npm install, pip install)
268
+ - Installed packages (apt install, npm install, uv pip install)
256
269
  - Environment variables set
257
270
  - Config files modified (~/.gitconfig, cron jobs, etc.)
258
271
  - Skill dependencies installed
@@ -262,7 +275,8 @@ Update this file whenever you modify the environment. On fresh container, read i
262
275
  ## Log Queries (for older history)
263
276
  Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
264
277
  The log contains user messages and your final responses (not tool calls/results).
265
- ${isContainer ? "Install jq: apk add jq" : ""}
278
+ Use \`log.jsonl\` for quick grep-style history. Use \`${conversationPath}/sessions/\` when you need structured turns, tool outputs, or branch lineage.
279
+ ${isContainer ? "Install jq: apt-get install jq" : ""}
266
280
  ${isFirecracker ? "Install jq: apt-get install jq" : ""}
267
281
 
268
282
  \`\`\`bash
@@ -274,6 +288,10 @@ grep -i "topic" log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user
274
288
 
275
289
  # Messages from specific user
276
290
  grep '"userName":"mario"' log.jsonl | tail -20 | jq -c '{date: .date[0:19], text}'
291
+
292
+ # Inspect top-level session pointer and available session files
293
+ cat sessions/current
294
+ ls -1 sessions/
277
295
  \`\`\`
278
296
 
279
297
  ## Tools
@@ -291,6 +309,12 @@ function truncate(text, maxLen) {
291
309
  return text;
292
310
  return `${text.substring(0, maxLen - 3)}...`;
293
311
  }
312
+ // Tools whose output is interesting in the structured session log but too noisy
313
+ // to surface as a per-tool diagnostic to the user.
314
+ const QUIET_TOOLS = new Set(["read", "write", "edit"]);
315
+ // Cap raw tool output before handing it to adapters. Bash output can be MB; without
316
+ // this each adapter's splitter would fan it out into many sequential platform posts.
317
+ const TOOL_RESULT_DIAGNOSTIC_CAP = 8000;
294
318
  function extractToolResultText(result) {
295
319
  if (typeof result === "string") {
296
320
  return result;
@@ -312,55 +336,29 @@ function extractToolResultText(result) {
312
336
  }
313
337
  return JSON.stringify(result);
314
338
  }
315
- function formatToolArgsForSlack(_toolName, args) {
316
- const lines = [];
317
- for (const [key, value] of Object.entries(args)) {
318
- if (key === "label")
319
- continue;
320
- if (key === "path" && typeof value === "string") {
321
- const offset = args.offset;
322
- const limit = args.limit;
323
- if (offset !== undefined && limit !== undefined) {
324
- lines.push(`${value}:${offset}-${offset + limit}`);
325
- }
326
- else {
327
- lines.push(value);
328
- }
329
- continue;
330
- }
331
- if (key === "offset" || key === "limit")
332
- continue;
333
- if (typeof value === "string") {
334
- lines.push(value);
335
- }
336
- else {
337
- lines.push(JSON.stringify(value));
338
- }
339
- }
340
- return lines.join("\n");
341
- }
342
339
  // ============================================================================
343
340
  // Agent runner
344
341
  // ============================================================================
345
342
  /**
346
- * Create a new AgentRunner for a conversation.
343
+ * Create a new AgentRunner for a channel.
347
344
  * Sets up the session and subscribes to events once.
348
345
  *
349
- * Runner caching is handled by the caller (conversationStates in main.ts).
346
+ * Runner caching is handled by the caller (channelStates in main.ts).
350
347
  * This is a stateless factory function.
351
348
  */
352
- export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, vaultManager, bindingStore, provisioner, stateDir) {
353
- const agentConfig = loadAgentConfig(stateDir ?? workspaceDir);
349
+ export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, bindingStore, provisioner) {
350
+ const agentConfig = loadAgentConfig(workspaceDir);
354
351
  // Initialize logger with settings from config
355
352
  log.initLogger({
356
353
  logFormat: agentConfig.logFormat,
357
354
  logLevel: agentConfig.logLevel,
358
355
  });
359
356
  const executionResolver = vaultManager &&
357
+ sandboxConfig.type !== "host" &&
360
358
  (vaultManager.isEnabled() ||
361
359
  !!bindingStore ||
362
- sandboxConfig.type === "image" ||
363
- sandboxConfig.type === "container")
360
+ sandboxConfig.type === "container" ||
361
+ sandboxConfig.type === "image")
364
362
  ? new ActorExecutionResolver(sandboxConfig, vaultManager, bindingStore, provisioner)
365
363
  : undefined;
366
364
  let activeExecutor = executionResolver !== undefined
@@ -378,7 +376,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
378
376
  },
379
377
  };
380
378
  const workspaceBase = conversationDir.replace(`/${conversationId}`, "");
381
- // Compute workspace path from the current executor. This may change per run.
382
379
  const getWorkspacePath = () => executor.getWorkspacePath(workspaceBase);
383
380
  let workspacePath = getWorkspacePath();
384
381
  // Create tools (per-runner, with per-runner upload function setter)
@@ -392,52 +389,24 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
392
389
  const memory = await getMemory(conversationDir);
393
390
  const skills = loadMamaSkills(conversationDir, workspacePath);
394
391
  const emptyPlatform = {
395
- name: "slack",
392
+ name: "chat",
396
393
  formattingGuide: "",
397
394
  channels: [],
398
395
  users: [],
399
396
  };
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);
397
+ const systemPrompt = buildSystemPrompt(workspacePath, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
398
+ // Create session manager and settings manager. Top-level/private sessions
399
+ // use the conversation's current pointer; scoped sessions use fixed files.
400
+ // Platform-specific branch/fork behavior is resolved before runner creation.
405
401
  const isThread = sessionKey.includes(":");
406
- let sessionManager;
407
- let sessionFile;
408
- if (isThread) {
409
- const threadFile = getThreadSessionFile(conversationDir, sessionKey);
410
- const existing = tryResolveThreadSession(threadFile);
411
- if (existing) {
412
- sessionFile = existing;
413
- sessionManager = openManagedSession(sessionFile, sessionDir, conversationDir);
414
- }
415
- else {
416
- const conversationSource = resolveChannelSessionFile(conversationDir);
417
- if (conversationSource) {
418
- try {
419
- sessionFile = forkThreadSessionFile(conversationSource, threadFile, conversationDir);
420
- sessionManager = openManagedSession(sessionFile, sessionDir, conversationDir);
421
- }
422
- catch {
423
- sessionFile = createManagedSessionFileAtPath(threadFile, conversationDir);
424
- sessionManager = openManagedSession(sessionFile, sessionDir, conversationDir);
425
- }
426
- }
427
- else {
428
- sessionFile = createManagedSessionFileAtPath(threadFile, conversationDir);
429
- sessionManager = openManagedSession(sessionFile, sessionDir, conversationDir);
430
- }
431
- }
432
- }
433
- else {
434
- // Top-level conversation session: resolve the current session file.
435
- sessionFile = resolveManagedSessionFile(sessionDir, conversationDir);
436
- sessionManager = openManagedSession(sessionFile, sessionDir, conversationDir);
437
- }
438
- const sessionUuid = extractSessionUuid(sessionFile);
439
- // Used for Slack thread filtering — for non-Slack platforms this is effectively a no-op
440
402
  const rootTs = extractSessionSuffix(sessionKey);
403
+ const { sessionDir, contextFile, threadRootMessage } = sessionScope;
404
+ const sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
405
+ const threadSessionName = buildThreadSessionName(threadRootMessage);
406
+ if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
407
+ sessionManager.appendSessionInfo(threadSessionName);
408
+ }
409
+ const sessionUuid = extractSessionUuid(contextFile);
441
410
  const settingsManager = createMamaSettingsManager(join(conversationDir, ".."));
442
411
  // Create AuthStorage and ModelRegistry
443
412
  // Auth stored outside workspace so agent can't access it
@@ -463,7 +432,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
463
432
  const loadedSession = sessionManager.buildSessionContext();
464
433
  if (loadedSession.messages.length > 0) {
465
434
  agent.state.messages = loadedSession.messages;
466
- log.logInfo(`[${conversationId}] Loaded ${loadedSession.messages.length} messages from session file`);
435
+ log.logInfo(`[${conversationId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
467
436
  }
468
437
  // Load extensions, skills, prompts, themes via DefaultResourceLoader
469
438
  // This reads ~/.pi/agent/settings.json (packages, extensions enable/disable)
@@ -535,8 +504,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
535
504
  ...baseAttrs,
536
505
  });
537
506
  log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
538
- // Tool labels are omitted from the main message to reduce Slack noise.
539
- // Tool execution details are still posted to the thread (see tool_execution_end).
540
507
  }
541
508
  else if (event.type === "tool_execution_end") {
542
509
  const agentEvent = event;
@@ -570,24 +537,16 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
570
537
  else {
571
538
  log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
572
539
  }
573
- // Post args + result to thread
574
- const label = pending?.args ? pending.args.label : undefined;
575
- const argsFormatted = pending
576
- ? formatToolArgsForSlack(agentEvent.toolName, pending.args)
577
- : "(args not found)";
578
- const duration = (durationMs / 1000).toFixed(1);
579
- let threadMessage = `*${agentEvent.isError ? "✗" : "✓"} ${agentEvent.toolName}*`;
580
- if (label)
581
- threadMessage += `: ${label}`;
582
- threadMessage += ` (${duration}s)\n`;
583
- if (argsFormatted)
584
- threadMessage += `\`\`\`\n${argsFormatted}\n\`\`\`\n`;
585
- threadMessage += `*Result:*\n\`\`\`\n${resultStr}\n\`\`\``;
586
- // Only post thread details for tools with meaningful output (bash, attach).
587
- // Skip read/write/edit to reduce Slack noise — their results are in the log.
588
- const quietTools = new Set(["read", "write", "edit"]);
589
- if (!quietTools.has(agentEvent.toolName)) {
590
- queue.enqueueMessage(threadMessage, "thread", "tool result thread", false);
540
+ if (!QUIET_TOOLS.has(agentEvent.toolName)) {
541
+ const toolResult = {
542
+ toolName: agentEvent.toolName,
543
+ label: pending?.args ? pending.args.label : undefined,
544
+ args: pending?.args,
545
+ result: truncate(resultStr, TOOL_RESULT_DIAGNOSTIC_CAP),
546
+ isError: agentEvent.isError,
547
+ durationMs,
548
+ };
549
+ queue.enqueue(() => responseCtx.respondToolResult(toolResult), "tool result diagnostic");
591
550
  }
592
551
  if (agentEvent.isError) {
593
552
  queue.enqueue(() => responseCtx.respond(`_Error: ${truncate(resultStr, 200)}_`), "tool error");
@@ -679,16 +638,12 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
679
638
  const text = textParts.join("\n");
680
639
  for (const thinking of thinkingParts) {
681
640
  log.logThinking(logCtx, thinking);
682
- queue.enqueueMessage(`_${thinking}_`, "main", "thinking main");
683
- queue.enqueueMessage(`_${thinking}_`, "thread", "thinking thread", false);
641
+ queue.enqueue(() => responseCtx.respond(`_${thinking}_`), "thinking main");
642
+ queue.enqueue(() => responseCtx.respondDiagnostic(`_${thinking}_`), "thinking diagnostic");
684
643
  }
685
644
  if (text.trim()) {
686
645
  log.logResponse(logCtx, text);
687
- queue.enqueueMessage(text, "main", "response main");
688
- // Only overflow to thread for texts that will be truncated in main
689
- if (text.length > SLACK_MAX_LENGTH) {
690
- queue.enqueueMessage(text, "thread", "response thread", false);
691
- }
646
+ queue.enqueue(() => responseCtx.respond(text), "response main");
692
647
  }
693
648
  }
694
649
  }
@@ -711,33 +666,12 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
711
666
  queue.enqueue(() => responseCtx.respond(`_Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})..._`), "retry");
712
667
  }
713
668
  });
714
- // Message limit constant
715
- const SLACK_MAX_LENGTH = 40000;
716
- const splitForSlack = (text) => {
717
- if (text.length <= SLACK_MAX_LENGTH)
718
- return [text];
719
- const parts = [];
720
- let remaining = text;
721
- let partNum = 1;
722
- while (remaining.length > 0) {
723
- const chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);
724
- remaining = remaining.substring(SLACK_MAX_LENGTH - 50);
725
- const suffix = remaining.length > 0 ? `\n_(continued ${partNum}...)_` : "";
726
- parts.push(chunk + suffix);
727
- partNum++;
728
- }
729
- return parts;
730
- };
731
669
  return {
732
670
  async run(message, responseCtx, platform) {
733
671
  // Extract conversationId from sessionKey (format: "conversationId:rootTs" or just "conversationId")
734
- const sessionConversationId = message.sessionKey.split(":")[0];
735
- // Ensure the conversation directory exists
672
+ const sessionConversation = message.sessionKey.split(":")[0];
673
+ // Ensure conversation directory exists
736
674
  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
675
  if (executionResolver) {
742
676
  executionResolver.refresh();
743
677
  activeExecutor = await executionResolver.resolve({
@@ -757,35 +691,38 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
757
691
  if (syncedCount > 0) {
758
692
  log.logInfo(`[${conversationId}] Synced ${syncedCount} messages from log.jsonl`);
759
693
  }
760
- // Reload messages from the session file.
761
- // This picks up any messages synced above.
694
+ // Reload messages from context.jsonl
695
+ // This picks up any messages synced above
762
696
  const reloadedSession = sessionManager.buildSessionContext();
763
697
  if (reloadedSession.messages.length > 0) {
764
698
  agent.state.messages = reloadedSession.messages;
765
699
  log.logInfo(`[${conversationId}] Reloaded ${reloadedSession.messages.length} messages from context`);
766
700
  }
767
701
  // Update system prompt with fresh memory, channel/user info, and skills
768
- // Use the actual executor's sandbox config, not the initial config,
769
- // to ensure accurate environment description for the model
770
702
  const memory = await getMemory(conversationDir);
771
703
  const skills = loadMamaSkills(conversationDir, workspacePath);
772
- const actualSandboxConfig = executor.getSandboxConfig();
773
- const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.userId, memory, actualSandboxConfig, platform, skills);
704
+ const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills);
774
705
  session.agent.state.systemPrompt = systemPrompt;
775
- // Set up file upload function
776
- setUploadFunction(async (filePath, title) => {
777
- const hostPath = translateToHostPath(filePath, conversationDir, workspacePath, conversationId);
778
- await responseCtx.uploadFile(hostPath, title);
779
- });
780
706
  setEventContext({
781
707
  platform: platform.name,
782
708
  conversationId,
709
+ conversationKind: message.conversationKind,
783
710
  userId: message.userId,
711
+ sessionKey: message.sessionKey,
712
+ // For Slack scheduled events, preserve thread targeting only when the
713
+ // request was created inside an existing thread. Top-level reminders
714
+ // should come back as top-level messages.
715
+ threadTs: message.threadTs,
716
+ });
717
+ // Set up file upload function
718
+ setUploadFunction(async (filePath, title) => {
719
+ const hostPath = translateToHostPath(filePath, conversationDir, workspacePath, conversationId);
720
+ await responseCtx.uploadFile(hostPath, title);
784
721
  });
785
722
  // Reset per-run state
786
723
  runState.responseCtx = responseCtx;
787
724
  runState.logCtx = {
788
- conversationId: sessionConversationId,
725
+ conversationId: sessionConversation,
789
726
  userName: message.userName,
790
727
  conversationName: undefined,
791
728
  sessionId: sessionUuid,
@@ -813,11 +750,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
813
750
  const errMsg = err instanceof Error ? err.message : String(err);
814
751
  log.logWarning(`API error (${errorContext})`, errMsg);
815
752
  try {
816
- // Split long error messages to avoid msg_too_long
817
- const errParts = splitForSlack(`_Error: ${errMsg}_`);
818
- for (const part of errParts) {
819
- await responseCtx.respondInThread(part);
820
- }
753
+ await responseCtx.respondDiagnostic(`Error: ${errMsg}`, { style: "error" });
821
754
  }
822
755
  catch {
823
756
  // Ignore
@@ -825,12 +758,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
825
758
  }
826
759
  });
827
760
  },
828
- enqueueMessage(text, target, errorContext, _doLog = true) {
829
- const parts = splitForSlack(text);
830
- for (const part of parts) {
831
- this.enqueue(() => target === "main" ? responseCtx.respond(part) : responseCtx.respondInThread(part), errorContext);
832
- }
833
- },
834
761
  };
835
762
  // Log context info
836
763
  log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
@@ -849,7 +776,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
849
776
  const imageAttachments = [];
850
777
  const nonImagePaths = [];
851
778
  for (const a of message.attachments || []) {
852
- // a.localPath is the path relative to the workspace
779
+ // a.localPath is the path relative to the workspace.
853
780
  const fullPath = `${workspacePath}/${a.localPath}`;
854
781
  const mimeType = getImageMimeType(a.localPath);
855
782
  if (mimeType && existsSync(fullPath)) {
@@ -882,7 +809,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
882
809
  addLifecycleBreadcrumb("agent.prompt.sent", {
883
810
  provider: model.provider,
884
811
  model: agentConfig.model,
885
- channel_id: sessionConversationId,
812
+ channel_id: sessionConversation,
886
813
  session_id: sessionUuid,
887
814
  attachment_count: message.attachments?.length ?? 0,
888
815
  image_attachment_count: imageAttachments.length,
@@ -894,11 +821,9 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
894
821
  if (runState.stopReason === "error" && runState.errorMessage) {
895
822
  try {
896
823
  await responseCtx.replaceResponse("_Sorry, something went wrong_");
897
- // Split long error messages to avoid msg_too_long
898
- const errorParts = splitForSlack(`_Error: ${runState.errorMessage}_`);
899
- for (const part of errorParts) {
900
- await responseCtx.respondInThread(part);
901
- }
824
+ await responseCtx.respondDiagnostic(`Error: ${runState.errorMessage}`, {
825
+ style: "error",
826
+ });
902
827
  }
903
828
  catch (err) {
904
829
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -926,10 +851,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
926
851
  }
927
852
  else if (finalText.trim()) {
928
853
  try {
929
- const mainText = finalText.length > SLACK_MAX_LENGTH
930
- ? `${finalText.substring(0, SLACK_MAX_LENGTH - 50)}\n\n_(see thread for full response)_`
931
- : finalText;
932
- await responseCtx.replaceResponse(mainText);
854
+ await responseCtx.replaceResponse(finalText);
933
855
  }
934
856
  catch (err) {
935
857
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -957,7 +879,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
957
879
  const runMetricAttributes = metricAttributes({
958
880
  provider: model.provider,
959
881
  model: agentConfig.model,
960
- channel_id: sessionConversationId,
882
+ channel_id: sessionConversation,
961
883
  session_id: sessionUuid,
962
884
  stop_reason: runState.stopReason,
963
885
  llm_calls: runState.llmCallCount,
@@ -982,12 +904,10 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
982
904
  attributes: runMetricAttributes,
983
905
  });
984
906
  const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
985
- // Split long summaries to avoid msg_too_long
986
- const summaryParts = splitForSlack(summary);
987
- for (const part of summaryParts) {
988
- runState.queue.enqueue(() => responseCtx.respondInThread(part, { style: "muted" }), "usage summary");
907
+ if (platform.diagnostics?.showUsageSummary === true) {
908
+ runState.queue.enqueue(() => responseCtx.respondDiagnostic(summary, { style: "muted" }), "usage summary");
909
+ await queueChain;
989
910
  }
990
- await queueChain;
991
911
  }
992
912
  // Clear run state
993
913
  runState.responseCtx = null;