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

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 (264) hide show
  1. package/README.md +157 -391
  2. package/dist/adapter.d.ts +31 -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 +10 -5
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +347 -115
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts +1 -1
  10. package/dist/adapters/discord/context.d.ts.map +1 -1
  11. package/dist/adapters/discord/context.js +118 -25
  12. package/dist/adapters/discord/context.js.map +1 -1
  13. package/dist/adapters/shared.d.ts +91 -0
  14. package/dist/adapters/shared.d.ts.map +1 -0
  15. package/dist/adapters/shared.js +191 -0
  16. package/dist/adapters/shared.js.map +1 -0
  17. package/dist/adapters/slack/bot.d.ts +21 -22
  18. package/dist/adapters/slack/bot.d.ts.map +1 -1
  19. package/dist/adapters/slack/bot.js +530 -221
  20. package/dist/adapters/slack/bot.js.map +1 -1
  21. package/dist/adapters/slack/branch-manager.d.ts +28 -0
  22. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  23. package/dist/adapters/slack/branch-manager.js +107 -0
  24. package/dist/adapters/slack/branch-manager.js.map +1 -0
  25. package/dist/adapters/slack/context.d.ts +4 -1
  26. package/dist/adapters/slack/context.d.ts.map +1 -1
  27. package/dist/adapters/slack/context.js +193 -75
  28. package/dist/adapters/slack/context.js.map +1 -1
  29. package/dist/adapters/slack/session.d.ts +38 -0
  30. package/dist/adapters/slack/session.d.ts.map +1 -0
  31. package/dist/adapters/slack/session.js +66 -0
  32. package/dist/adapters/slack/session.js.map +1 -0
  33. package/dist/adapters/slack/tools/attach.d.ts +1 -1
  34. package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
  35. package/dist/adapters/slack/tools/attach.js.map +1 -1
  36. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  37. package/dist/adapters/telegram/bot.js +140 -153
  38. package/dist/adapters/telegram/bot.js.map +1 -1
  39. package/dist/adapters/telegram/context.d.ts +1 -1
  40. package/dist/adapters/telegram/context.d.ts.map +1 -1
  41. package/dist/adapters/telegram/context.js +74 -20
  42. package/dist/adapters/telegram/context.js.map +1 -1
  43. package/dist/agent.d.ts +13 -3
  44. package/dist/agent.d.ts.map +1 -1
  45. package/dist/agent.js +677 -552
  46. package/dist/agent.js.map +1 -1
  47. package/dist/commands/auto-reply.d.ts +16 -0
  48. package/dist/commands/auto-reply.d.ts.map +1 -0
  49. package/dist/commands/auto-reply.js +72 -0
  50. package/dist/commands/auto-reply.js.map +1 -0
  51. package/dist/commands/index.d.ts +5 -0
  52. package/dist/commands/index.d.ts.map +1 -0
  53. package/dist/commands/index.js +18 -0
  54. package/dist/commands/index.js.map +1 -0
  55. package/dist/commands/login.d.ts +5 -0
  56. package/dist/commands/login.d.ts.map +1 -0
  57. package/dist/commands/login.js +91 -0
  58. package/dist/commands/login.js.map +1 -0
  59. package/dist/commands/model.d.ts +14 -0
  60. package/dist/commands/model.d.ts.map +1 -0
  61. package/dist/commands/model.js +112 -0
  62. package/dist/commands/model.js.map +1 -0
  63. package/dist/commands/new.d.ts +9 -0
  64. package/dist/commands/new.d.ts.map +1 -0
  65. package/dist/commands/new.js +28 -0
  66. package/dist/commands/new.js.map +1 -0
  67. package/dist/commands/registry.d.ts +4 -0
  68. package/dist/commands/registry.d.ts.map +1 -0
  69. package/dist/commands/registry.js +9 -0
  70. package/dist/commands/registry.js.map +1 -0
  71. package/dist/commands/sandbox.d.ts +10 -0
  72. package/dist/commands/sandbox.d.ts.map +1 -0
  73. package/dist/commands/sandbox.js +88 -0
  74. package/dist/commands/sandbox.js.map +1 -0
  75. package/dist/commands/session-view.d.ts +5 -0
  76. package/dist/commands/session-view.d.ts.map +1 -0
  77. package/dist/commands/session-view.js +62 -0
  78. package/dist/commands/session-view.js.map +1 -0
  79. package/dist/commands/types.d.ts +41 -0
  80. package/dist/commands/types.d.ts.map +1 -0
  81. package/dist/commands/types.js +2 -0
  82. package/dist/commands/types.js.map +1 -0
  83. package/dist/commands/utils.d.ts +8 -0
  84. package/dist/commands/utils.d.ts.map +1 -0
  85. package/dist/commands/utils.js +14 -0
  86. package/dist/commands/utils.js.map +1 -0
  87. package/dist/config.d.ts +46 -8
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +306 -67
  90. package/dist/config.js.map +1 -1
  91. package/dist/context.d.ts +10 -42
  92. package/dist/context.d.ts.map +1 -1
  93. package/dist/context.js +14 -127
  94. package/dist/context.js.map +1 -1
  95. package/dist/events.d.ts +2 -0
  96. package/dist/events.d.ts.map +1 -1
  97. package/dist/events.js +148 -67
  98. package/dist/events.js.map +1 -1
  99. package/dist/execution-resolver.d.ts +12 -7
  100. package/dist/execution-resolver.d.ts.map +1 -1
  101. package/dist/execution-resolver.js +150 -21
  102. package/dist/execution-resolver.js.map +1 -1
  103. package/dist/file-guards.d.ts +9 -0
  104. package/dist/file-guards.d.ts.map +1 -0
  105. package/dist/file-guards.js +56 -0
  106. package/dist/file-guards.js.map +1 -0
  107. package/dist/fs-atomic.d.ts +10 -0
  108. package/dist/fs-atomic.d.ts.map +1 -0
  109. package/dist/fs-atomic.js +45 -0
  110. package/dist/fs-atomic.js.map +1 -0
  111. package/dist/index.d.ts +7 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +4 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/instrument.d.ts.map +1 -1
  116. package/dist/instrument.js +2 -3
  117. package/dist/instrument.js.map +1 -1
  118. package/dist/log.d.ts +1 -12
  119. package/dist/log.d.ts.map +1 -1
  120. package/dist/log.js +12 -143
  121. package/dist/log.js.map +1 -1
  122. package/dist/{login.d.ts → login/index.d.ts} +16 -3
  123. package/dist/login/index.d.ts.map +1 -0
  124. package/dist/{login.js → login/index.js} +94 -17
  125. package/dist/login/index.js.map +1 -0
  126. package/dist/{link-server.d.ts → login/portal.d.ts} +6 -4
  127. package/dist/login/portal.d.ts.map +1 -0
  128. package/dist/login/portal.js +1544 -0
  129. package/dist/login/portal.js.map +1 -0
  130. package/dist/login/session.d.ts +26 -0
  131. package/dist/login/session.d.ts.map +1 -0
  132. package/dist/{link-token.js → login/session.js} +10 -22
  133. package/dist/login/session.js.map +1 -0
  134. package/dist/main.d.ts.map +1 -1
  135. package/dist/main.js +138 -352
  136. package/dist/main.js.map +1 -1
  137. package/dist/provisioner.d.ts +42 -11
  138. package/dist/provisioner.d.ts.map +1 -1
  139. package/dist/provisioner.js +273 -64
  140. package/dist/provisioner.js.map +1 -1
  141. package/dist/runtime/conversation-orchestrator.d.ts +40 -0
  142. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  143. package/dist/runtime/conversation-orchestrator.js +183 -0
  144. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  145. package/dist/runtime/index.d.ts +2 -0
  146. package/dist/runtime/index.d.ts.map +1 -0
  147. package/dist/runtime/index.js +2 -0
  148. package/dist/runtime/index.js.map +1 -0
  149. package/dist/runtime/session-runtime.d.ts +26 -0
  150. package/dist/runtime/session-runtime.d.ts.map +1 -0
  151. package/dist/runtime/session-runtime.js +221 -0
  152. package/dist/runtime/session-runtime.js.map +1 -0
  153. package/dist/sandbox/cloudflare.d.ts +15 -0
  154. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  155. package/dist/sandbox/cloudflare.js +137 -0
  156. package/dist/sandbox/cloudflare.js.map +1 -0
  157. package/dist/sandbox/container.d.ts +2 -1
  158. package/dist/sandbox/container.d.ts.map +1 -1
  159. package/dist/sandbox/container.js +18 -2
  160. package/dist/sandbox/container.js.map +1 -1
  161. package/dist/sandbox/firecracker.d.ts +2 -1
  162. package/dist/sandbox/firecracker.d.ts.map +1 -1
  163. package/dist/sandbox/firecracker.js +6 -0
  164. package/dist/sandbox/firecracker.js.map +1 -1
  165. package/dist/sandbox/host.d.ts +2 -1
  166. package/dist/sandbox/host.d.ts.map +1 -1
  167. package/dist/sandbox/host.js +4 -0
  168. package/dist/sandbox/host.js.map +1 -1
  169. package/dist/sandbox/index.d.ts +6 -4
  170. package/dist/sandbox/index.d.ts.map +1 -1
  171. package/dist/sandbox/index.js +9 -6
  172. package/dist/sandbox/index.js.map +1 -1
  173. package/dist/sandbox/path-context.d.ts +4 -0
  174. package/dist/sandbox/path-context.d.ts.map +1 -0
  175. package/dist/sandbox/path-context.js +20 -0
  176. package/dist/sandbox/path-context.js.map +1 -0
  177. package/dist/sandbox/types.d.ts +17 -1
  178. package/dist/sandbox/types.d.ts.map +1 -1
  179. package/dist/sandbox/types.js.map +1 -1
  180. package/dist/sentry.d.ts +20 -1
  181. package/dist/sentry.d.ts.map +1 -1
  182. package/dist/sentry.js +58 -8
  183. package/dist/sentry.js.map +1 -1
  184. package/dist/session-policy.d.ts +13 -0
  185. package/dist/session-policy.d.ts.map +1 -0
  186. package/dist/session-policy.js +23 -0
  187. package/dist/session-policy.js.map +1 -0
  188. package/dist/session-store.d.ts +33 -2
  189. package/dist/session-store.d.ts.map +1 -1
  190. package/dist/session-store.js +179 -13
  191. package/dist/session-store.js.map +1 -1
  192. package/dist/session-view/command.d.ts +5 -0
  193. package/dist/session-view/command.d.ts.map +1 -0
  194. package/dist/session-view/command.js +11 -0
  195. package/dist/session-view/command.js.map +1 -0
  196. package/dist/session-view/portal.d.ts +16 -0
  197. package/dist/session-view/portal.d.ts.map +1 -0
  198. package/dist/session-view/portal.js +1822 -0
  199. package/dist/session-view/portal.js.map +1 -0
  200. package/dist/session-view/service.d.ts +34 -0
  201. package/dist/session-view/service.d.ts.map +1 -0
  202. package/dist/session-view/service.js +427 -0
  203. package/dist/session-view/service.js.map +1 -0
  204. package/dist/session-view/store.d.ts +18 -0
  205. package/dist/session-view/store.d.ts.map +1 -0
  206. package/dist/session-view/store.js +36 -0
  207. package/dist/session-view/store.js.map +1 -0
  208. package/dist/store.d.ts +3 -6
  209. package/dist/store.d.ts.map +1 -1
  210. package/dist/store.js +22 -48
  211. package/dist/store.js.map +1 -1
  212. package/dist/tool-diagnostics.d.ts +2 -0
  213. package/dist/tool-diagnostics.d.ts.map +1 -0
  214. package/dist/tool-diagnostics.js +7 -0
  215. package/dist/tool-diagnostics.js.map +1 -0
  216. package/dist/tools/bash.d.ts +2 -2
  217. package/dist/tools/bash.d.ts.map +1 -1
  218. package/dist/tools/bash.js.map +1 -1
  219. package/dist/tools/edit.d.ts +2 -2
  220. package/dist/tools/edit.d.ts.map +1 -1
  221. package/dist/tools/edit.js.map +1 -1
  222. package/dist/tools/event.d.ts +42 -2
  223. package/dist/tools/event.d.ts.map +1 -1
  224. package/dist/tools/event.js +43 -9
  225. package/dist/tools/event.js.map +1 -1
  226. package/dist/tools/index.d.ts +2 -2
  227. package/dist/tools/index.d.ts.map +1 -1
  228. package/dist/tools/index.js +2 -2
  229. package/dist/tools/index.js.map +1 -1
  230. package/dist/tools/read.d.ts +2 -2
  231. package/dist/tools/read.d.ts.map +1 -1
  232. package/dist/tools/read.js.map +1 -1
  233. package/dist/tools/write.d.ts +2 -2
  234. package/dist/tools/write.d.ts.map +1 -1
  235. package/dist/tools/write.js.map +1 -1
  236. package/dist/trigger.d.ts +31 -0
  237. package/dist/trigger.d.ts.map +1 -0
  238. package/dist/trigger.js +98 -0
  239. package/dist/trigger.js.map +1 -0
  240. package/dist/vault-routing.d.ts +2 -7
  241. package/dist/vault-routing.d.ts.map +1 -1
  242. package/dist/vault-routing.js +6 -42
  243. package/dist/vault-routing.js.map +1 -1
  244. package/dist/vault.d.ts +22 -56
  245. package/dist/vault.d.ts.map +1 -1
  246. package/dist/vault.js +155 -263
  247. package/dist/vault.js.map +1 -1
  248. package/package.json +11 -11
  249. package/dist/bindings.d.ts +0 -44
  250. package/dist/bindings.d.ts.map +0 -1
  251. package/dist/bindings.js +0 -74
  252. package/dist/bindings.js.map +0 -1
  253. package/dist/link-server.d.ts.map +0 -1
  254. package/dist/link-server.js +0 -899
  255. package/dist/link-server.js.map +0 -1
  256. package/dist/link-token.d.ts +0 -32
  257. package/dist/link-token.d.ts.map +0 -1
  258. package/dist/link-token.js.map +0 -1
  259. package/dist/login.d.ts.map +0 -1
  260. package/dist/login.js.map +0 -1
  261. package/dist/sandbox.d.ts +0 -2
  262. package/dist/sandbox.d.ts.map +0 -1
  263. package/dist/sandbox.js +0 -2
  264. package/dist/sandbox.js.map +0 -1
package/dist/agent.js CHANGED
@@ -1,17 +1,19 @@
1
- import { Agent } from "@mariozechner/pi-agent-core";
2
- import { getModel } from "@mariozechner/pi-ai";
3
- import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, getAgentDir, loadSkillsFromDir, ModelRegistry, } from "@mariozechner/pi-coding-agent";
1
+ import { Agent } from "@earendil-works/pi-agent-core";
2
+ import { getModel } from "@earendil-works/pi-ai";
3
+ import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, getAgentDir, loadSkillsFromDir, ModelRegistry, SettingsManager, } from "@earendil-works/pi-coding-agent";
4
4
  import { existsSync, readFileSync } from "fs";
5
5
  import { mkdir, readFile, writeFile } from "fs/promises";
6
6
  import { homedir } from "os";
7
- import { join } from "path";
8
- import { loadAgentConfig } from "./config.js";
9
- import { createMamaSettingsManager, syncLogToSessionManager } from "./context.js";
7
+ import { join, posix } from "path";
8
+ import { loadAgentConfigForConversation } from "./config.js";
10
9
  import { ActorExecutionResolver } from "./execution-resolver.js";
11
10
  import * as log from "./log.js";
12
- import { createExecutor } from "./sandbox.js";
11
+ import { reportUserFacingError } from "./sentry.js";
12
+ import { createExecutor, } from "./sandbox/index.js";
13
+ import { createMountedRuntimePathContext } from "./sandbox/path-context.js";
13
14
  import { addLifecycleBreadcrumb, metricAttributes } from "./sentry.js";
14
- import { createManagedSessionFileAtPath, extractSessionSuffix, extractSessionUuid, forkThreadSessionFile, getChannelSessionDir, getThreadSessionFile, openManagedSession, resolveChannelSessionFile, resolveManagedSessionFile, tryResolveThreadSession, } from "./session-store.js";
15
+ import { extractSessionUuid, openManagedSession, } from "./session-store.js";
16
+ import { shouldSurfaceToolDiagnostic } from "./tool-diagnostics.js";
15
17
  import { createMamaTools } from "./tools/index.js";
16
18
  import * as Sentry from "@sentry/node";
17
19
  const IMAGE_MIME_TYPES = {
@@ -24,6 +26,13 @@ const IMAGE_MIME_TYPES = {
24
26
  function getImageMimeType(filename) {
25
27
  return IMAGE_MIME_TYPES[filename.toLowerCase().split(".").pop() || ""];
26
28
  }
29
+ function buildThreadSessionName(message) {
30
+ const text = message?.text?.trim();
31
+ if (!text)
32
+ return undefined;
33
+ const userLabel = message?.userName || message?.user || "unknown";
34
+ return `[${userLabel}]: ${text}`;
35
+ }
27
36
  async function getMemory(conversationDir) {
28
37
  const parts = [];
29
38
  // Read workspace-level memory (shared across all conversations)
@@ -87,11 +96,89 @@ function loadMamaSkills(conversationDir, workspacePath) {
87
96
  }
88
97
  return Array.from(skillMap.values());
89
98
  }
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";
94
- const isFirecracker = sandboxConfig.type === "firecracker";
99
+ function buildRuntimePaths(runtimeWorkspaceRoot, conversationId) {
100
+ const workspaceRoot = runtimeWorkspaceRoot.replace(/\/+$/, "") || "/";
101
+ const conversationPath = posix.join(workspaceRoot, conversationId);
102
+ return {
103
+ workspaceRoot,
104
+ conversationPath,
105
+ scratchPath: posix.join(conversationPath, "scratch"),
106
+ };
107
+ }
108
+ function buildEnvDescription(sandboxType, workspaceRoot) {
109
+ switch (sandboxType) {
110
+ case "image":
111
+ return `You are running inside a managed per-user container.
112
+ - Runtime workspace root: ${workspaceRoot}
113
+ - Bash commands start in: ${workspaceRoot}
114
+ - Install tools with the image's package manager
115
+ - Your changes persist for this user's container until it is recreated`;
116
+ case "container":
117
+ return `You are running inside a shared container.
118
+ - Runtime workspace root: ${workspaceRoot}
119
+ - Bash commands start in: ${workspaceRoot}
120
+ - Install tools with the container's package manager
121
+ - Your changes persist across sessions`;
122
+ case "firecracker":
123
+ return `You are running inside a Firecracker microVM.
124
+ - Runtime workspace root: ${workspaceRoot}
125
+ - Use cd or absolute paths; project files are under ${workspaceRoot}
126
+ - Install tools with: apt-get install <package> (Debian-based)
127
+ - Your changes persist across sessions`;
128
+ case "cloudflare":
129
+ return `You are running through a Cloudflare Sandbox bridge.
130
+ - Runtime workspace root: ${workspaceRoot}
131
+ - Bash commands start in: ${workspaceRoot}
132
+ - Your commands run in a remote container managed by Cloudflare
133
+ - Important: the remote filesystem is not automatically synced back to the host workspace`;
134
+ default:
135
+ return `You are running directly on the host machine.
136
+ - Runtime workspace root: ${workspaceRoot}
137
+ - Bash commands start in: ${process.cwd()}
138
+ - Be careful with system modifications`;
139
+ }
140
+ }
141
+ export function buildEventFilesystemInstructions(sandboxType, workspaceRoot) {
142
+ if (sandboxType === "host" || sandboxType === "container" || sandboxType === "image") {
143
+ return `Events live in the host-side mama control plane and are mounted at \`${workspaceRoot}/events/\` in this runtime.
144
+
145
+ Prefer the \`event\` tool over manually writing JSON files; it fills \`platform\`, \`conversationId\`, \`conversationKind\`, and \`userId\` for the current conversation automatically.
146
+
147
+ ### Creating Events Manually
148
+ Only do this when you need to create events from a script. Use unique filenames to avoid overwriting existing events. Include a timestamp or random suffix:
149
+ \`\`\`bash
150
+ cat > ${workspaceRoot}/events/dentist-reminder-$(date +%s).json << 'EOF'
151
+ {"type": "one-shot", "platform": "<platform>", "conversationId": "<conversationId>", "conversationKind": "<direct|shared>", "userId": "<requester userId>", "text": "Dentist tomorrow", "at": "2025-12-14T09:00:00+01:00"}
152
+ EOF
153
+ \`\`\`
154
+
155
+ ### Managing Events
156
+ - List: \`ls ${workspaceRoot}/events/\`
157
+ - View: \`cat ${workspaceRoot}/events/foo.json\`
158
+ - Delete/cancel: \`rm ${workspaceRoot}/events/foo.json\``;
159
+ }
160
+ return `Events live in the host-side mama control plane, not necessarily in this runtime filesystem.
161
+
162
+ Use the \`event\` tool to create events. It writes to the correct host-side events directory and fills \`platform\`, \`conversationId\`, \`conversationKind\`, and \`userId\` for the current conversation automatically.
163
+
164
+ Do not create event files with bash in \`${workspaceRoot}/events/\` from this sandbox unless you have explicitly verified that path is mounted back to the host-side mama events directory.`;
165
+ }
166
+ export function resolveTriggerAttribution(message) {
167
+ const eventTextMatch = message.text.match(/^\[EVENT:([^:]+):/);
168
+ if (eventTextMatch)
169
+ return `[event: ${eventTextMatch[1]}]`;
170
+ const eventIdMatch = message.id.match(/^event:([^:]+)/);
171
+ if (eventIdMatch)
172
+ return `[event: ${eventIdMatch[1]}]`;
173
+ if (message.userName)
174
+ return `@${message.userName}`;
175
+ return undefined;
176
+ }
177
+ function buildSystemPrompt(workspacePath, conversationId, conversationKind, currentUserId, memory, sandboxConfig, platform, skills, isEventTrigger = false, triggerAttribution) {
178
+ const { workspaceRoot, conversationPath, scratchPath } = buildRuntimePaths(workspacePath, conversationId);
179
+ const sandboxType = sandboxConfig.type;
180
+ const isContainerLike = sandboxType === "container" || sandboxType === "image";
181
+ const isFirecracker = sandboxType === "firecracker";
95
182
  // Format channel mappings
96
183
  const channelMappings = platform.channels.length > 0
97
184
  ? platform.channels.map((c) => `${c.id}\t#${c.name}`).join("\n")
@@ -100,32 +187,39 @@ function buildSystemPrompt(workspacePath, conversationId, conversationKind, curr
100
187
  const userMappings = platform.users.length > 0
101
188
  ? platform.users.map((u) => `${u.id}\t@${u.userName}\t${u.displayName}`).join("\n")
102
189
  : "(no users loaded)";
103
- const envDescription = isImageSandbox
104
- ? `You are running inside a managed per-user container.
105
- - Bash working directory: / (use cd or absolute paths)
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
112
- - Your changes persist across sessions`
113
- : isFirecracker
114
- ? `You are running inside a Firecracker microVM.
115
- - Bash working directory: / (use cd or absolute paths)
116
- - Install tools with: apt-get install <package> (Debian-based)
117
- - Your changes persist across sessions`
118
- : `You are running directly on the host machine.
119
- - Bash working directory: ${process.cwd()}
120
- - Be careful with system modifications`;
190
+ const envDescription = buildEnvDescription(sandboxType, workspaceRoot);
191
+ const eventFilesystemInstructions = buildEventFilesystemInstructions(sandboxType, workspaceRoot);
192
+ const eventTriggerInstructions = isEventTrigger
193
+ ? `
194
+ ## Event Trigger Mode
195
+ - You are handling a scheduled/background event, not opening a brand new chat with a stranger.
196
+ - Treat the incoming user message as a self-contained task prepared by an earlier run.
197
+ - Complete the task directly. Avoid generic greetings, self-introductions, or boilerplate offers to help.
198
+ - For reminders/follow-ups, prefer a short direct response that sounds like a continuation of prior intent.
199
+ - If the event text includes tone, brevity, or language instructions, follow them literally.
200
+ `
201
+ : "";
202
+ const attributionInstructions = triggerAttribution
203
+ ? `
204
+ ## Attribution
205
+ Always end your final ${platform.name} response and any GitHub issue/PR comments or descriptions you write via tools with:
206
+ _Triggered by ${triggerAttribution}_
207
+
208
+ Do not add this to \`[SILENT]\` responses.
209
+ `
210
+ : "";
121
211
  return `You are mama, a ${platform.name} bot assistant. Be concise. No emojis.
122
212
 
123
213
  ## Context
124
214
  - For current date/time, use: date
125
215
  - You have access to previous conversation context including tool results from prior turns.
126
- - For older history beyond your context, search log.jsonl (contains user messages and your final responses, but not tool results).
127
- - 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.
128
-
216
+ - For older human-readable history beyond your context, search \`log.jsonl\` (contains user messages and your final responses, but not tool results).
217
+ - Structured session history with tool results lives in \`${conversationPath}/sessions/\`.
218
+ - The active top-level session is selected by \`${conversationPath}/sessions/current\`, which points to a timestamped \`.jsonl\` file in the same directory.
219
+ - Scoped/thread sessions use fixed files at \`${conversationPath}/sessions/<scope_id>.jsonl\` (for example \`${conversationPath}/sessions/1777386320.800769.jsonl\`).
220
+ - If a user asks about something that should exist in conversation history but is not found in the current context window, do not answer "I don't know" or "I don't have that". Instead, search the thread session, top-level session, and \`log.jsonl\` before responding.
221
+ - 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.
222
+ ${eventTriggerInstructions}
129
223
  ${platform.formattingGuide}
130
224
 
131
225
  ## Platform IDs
@@ -137,23 +231,29 @@ When mentioning users, use <@username> format (e.g., <@mario>).
137
231
 
138
232
  ## Environment
139
233
  ${envDescription}
234
+ - Default place for clones, downloads, and experiments: ${scratchPath}
235
+ - Do not use host-only paths unless you are running in host mode and verified they exist.
140
236
 
141
237
  ## Workspace Layout
142
- ${workspacePath}/
238
+ ${workspaceRoot}/
143
239
  ├── MEMORY.md # Global memory (all conversations)
144
240
  ├── skills/ # Global CLI tools you create
145
241
  └── ${conversationId}/ # This conversation
146
242
  ├── MEMORY.md # Conversation-specific memory
147
- ├── log.jsonl # Message history (no tool results)
243
+ ├── log.jsonl # Human-readable message history (no tool results)
244
+ ├── sessions/ # Structured session history used for context reconstruction
245
+ │ ├── current # Active top-level session pointer
246
+ │ ├── <timestamp>_<id>.jsonl # Top-level session files
247
+ │ └── <scope_id>.jsonl # Scoped thread/reply session files
148
248
  ├── attachments/ # User-shared files
149
- ├── scratch/ # Your working directory
249
+ ├── scratch/ # Working directory for clones/downloads/experiments: ${scratchPath}
150
250
  └── skills/ # Conversation-specific tools
151
251
 
152
252
  ## Skills (Custom CLI Tools)
153
253
  You can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).
154
254
 
155
255
  ### Creating Skills
156
- Store in \`${workspacePath}/skills/<name>/\` (global) or \`${conversationPath}/skills/<name>/\` (conversation-specific).
256
+ Store in \`${workspaceRoot}/skills/<name>/\` (global) or \`${conversationPath}/skills/<name>/\` (conversation-specific).
157
257
  Each skill directory needs a \`SKILL.md\` with YAML frontmatter:
158
258
 
159
259
  \`\`\`markdown
@@ -174,7 +274,8 @@ Scripts are in: {baseDir}/
174
274
  ${skills.length > 0 ? formatSkillsForPrompt(skills) : "(no skills installed yet)"}
175
275
 
176
276
  ## Events
177
- You can schedule events that wake you up at specific times or when external things happen. Events are JSON files in \`${workspacePath}/events/\`.
277
+ You can schedule events that wake you up at specific times or when external things happen.
278
+ ${eventFilesystemInstructions}
178
279
 
179
280
  ### Event Types
180
281
 
@@ -204,25 +305,11 @@ You can schedule events that wake you up at specific times or when external thin
204
305
  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}.
205
306
 
206
307
  ### Platform and Credential Routing
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.
308
+ Set \`platform\` to the target bot platform (\`${platform.name}\` for this conversation). Include it explicitly to avoid ambiguity.
208
309
 
209
310
  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
311
 
211
- Prefer the \`event\` tool over manually writing JSON files; it fills \`platform\`, \`conversationId\`, \`conversationKind\`, and \`userId\` for the current conversation automatically.
212
-
213
- ### Creating Events
214
- Use unique filenames to avoid overwriting existing events. Include a timestamp or random suffix:
215
- \`\`\`bash
216
- cat > ${workspacePath}/events/dentist-reminder-$(date +%s).json << 'EOF'
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"}
218
- EOF
219
- \`\`\`
220
- Or check if file exists first before creating.
221
-
222
- ### Managing Events
223
- - List: \`ls ${workspacePath}/events/\`
224
- - View: \`cat ${workspacePath}/events/foo.json\`
225
- - Delete/cancel: \`rm ${workspacePath}/events/foo.json\`
312
+ When scheduling an event, write \`text\` as a self-contained task for your future self. Include the minimum necessary context, tone, and constraints in the text itself because events do not inherit normal conversation history. Good: \`Please remind the user that break time is over and it is time to return to class. Keep it brief, in Traditional Chinese, and do not ask follow-up questions.\` Bad: \`back to class\`.
226
313
 
227
314
  ### When Events Trigger
228
315
  You receive a message like:
@@ -242,7 +329,7 @@ Maximum 5 events can be queued. Don't create excessive immediate or periodic eve
242
329
 
243
330
  ## Memory
244
331
  Write to MEMORY.md files to persist context across conversations.
245
- - Global (${workspacePath}/MEMORY.md): skills, preferences, project info
332
+ - Global (${workspaceRoot}/MEMORY.md): skills, preferences, project info
246
333
  - Conversation (${conversationPath}/MEMORY.md): conversation-specific decisions, ongoing work
247
334
  Update when you learn something important or when asked to remember something.
248
335
 
@@ -250,8 +337,8 @@ Update when you learn something important or when asked to remember something.
250
337
  ${memory}
251
338
 
252
339
  ## System Configuration Log
253
- Maintain ${workspacePath}/SYSTEM.md to log all environment modifications:
254
- - Installed packages (apk add, npm install, pip install)
340
+ Maintain ${workspaceRoot}/SYSTEM.md to log all environment modifications:
341
+ - Installed packages (apt install, npm install, uv pip install)
255
342
  - Environment variables set
256
343
  - Config files modified (~/.gitconfig, cron jobs, etc.)
257
344
  - Skill dependencies installed
@@ -261,9 +348,9 @@ Update this file whenever you modify the environment. On fresh container, read i
261
348
  ## Log Queries (for older history)
262
349
  Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
263
350
  The log contains user messages and your final responses (not tool calls/results).
264
- ${isContainer ? "Install jq: apk add jq" : ""}
265
- ${isFirecracker ? "Install jq: apt-get install jq" : ""}
266
-
351
+ Use \`log.jsonl\` for quick grep-style history. Use \`${conversationPath}/sessions/\` when you need structured turns, tool outputs, or branch lineage.
352
+ ${isContainerLike || isFirecracker ? "Install jq: apt-get install jq" : ""}
353
+ ${attributionInstructions}
267
354
  \`\`\`bash
268
355
  # Recent messages
269
356
  tail -30 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'
@@ -273,6 +360,10 @@ grep -i "topic" log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user
273
360
 
274
361
  # Messages from specific user
275
362
  grep '"userName":"mario"' log.jsonl | tail -20 | jq -c '{date: .date[0:19], text}'
363
+
364
+ # Inspect top-level session pointer and available session files
365
+ cat sessions/current
366
+ ls -1 sessions/
276
367
  \`\`\`
277
368
 
278
369
  ## Tools
@@ -290,79 +381,24 @@ function truncate(text, maxLen) {
290
381
  return text;
291
382
  return `${text.substring(0, maxLen - 3)}...`;
292
383
  }
293
- function extractToolResultText(result) {
294
- if (typeof result === "string") {
295
- return result;
296
- }
297
- if (result &&
298
- typeof result === "object" &&
299
- "content" in result &&
300
- Array.isArray(result.content)) {
301
- const content = result.content;
302
- const textParts = [];
303
- for (const part of content) {
304
- if (part.type === "text" && part.text) {
305
- textParts.push(part.text);
306
- }
307
- }
308
- if (textParts.length > 0) {
309
- return textParts.join("\n");
310
- }
311
- }
312
- return JSON.stringify(result);
313
- }
314
- function formatToolArgsForSlack(_toolName, args) {
315
- const lines = [];
316
- for (const [key, value] of Object.entries(args)) {
317
- if (key === "label")
318
- continue;
319
- if (key === "path" && typeof value === "string") {
320
- const offset = args.offset;
321
- const limit = args.limit;
322
- if (offset !== undefined && limit !== undefined) {
323
- lines.push(`${value}:${offset}-${offset + limit}`);
324
- }
325
- else {
326
- lines.push(value);
327
- }
328
- continue;
329
- }
330
- if (key === "offset" || key === "limit")
331
- continue;
332
- if (typeof value === "string") {
333
- lines.push(value);
334
- }
335
- else {
336
- lines.push(JSON.stringify(value));
337
- }
384
+ export function getUnresolvedSandboxPathContext(sandboxConfig, hostWorkspaceRoot) {
385
+ if (sandboxConfig.type === "image") {
386
+ return createMountedRuntimePathContext(hostWorkspaceRoot, "/workspace");
338
387
  }
339
- return lines.join("\n");
388
+ return createExecutor(sandboxConfig).getPathContext(hostWorkspaceRoot);
340
389
  }
341
- // ============================================================================
342
- // Agent runner
343
- // ============================================================================
344
- /**
345
- * Create a new AgentRunner for a channel.
346
- * Sets up the session and subscribes to events once.
347
- *
348
- * Runner caching is handled by the caller (channelStates in main.ts).
349
- * This is a stateless factory function.
350
- */
351
- export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, vaultManager, bindingStore, provisioner) {
352
- const agentConfig = loadAgentConfig(workspaceDir);
353
- // Initialize logger with settings from config
354
- log.initLogger({
355
- logFormat: agentConfig.logFormat,
356
- logLevel: agentConfig.logLevel,
357
- });
390
+ function createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, hostWorkspacePath) {
358
391
  const executionResolver = vaultManager &&
359
392
  sandboxConfig.type !== "host" &&
360
393
  (vaultManager.isEnabled() ||
361
- !!bindingStore ||
362
394
  sandboxConfig.type === "container" ||
363
- sandboxConfig.type === "image")
364
- ? new ActorExecutionResolver(sandboxConfig, vaultManager, bindingStore, provisioner)
395
+ sandboxConfig.type === "image" ||
396
+ sandboxConfig.type === "cloudflare" ||
397
+ sandboxConfig.type === "firecracker")
398
+ ? new ActorExecutionResolver(sandboxConfig, vaultManager, provisioner, workspaceDir)
365
399
  : undefined;
400
+ // activeExecutor is replaced at the start of each run() call when executionResolver
401
+ // is present, so the stable `executor` wrapper always delegates to the latest resolved value.
366
402
  let activeExecutor = executionResolver !== undefined
367
403
  ? createExecutor({ type: "host" })
368
404
  : createExecutor(sandboxConfig);
@@ -376,97 +412,46 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
376
412
  getSandboxConfig() {
377
413
  return activeExecutor.getSandboxConfig();
378
414
  },
415
+ getPathContext(hostWorkspaceRoot) {
416
+ return activeExecutor.getPathContext(hostWorkspaceRoot);
417
+ },
379
418
  };
380
- const workspaceBase = conversationDir.replace(`/${conversationId}`, "");
381
- const getWorkspacePath = () => executor.getWorkspacePath(workspaceBase);
382
- let workspacePath = getWorkspacePath();
383
- // Create tools (per-runner, with per-runner upload function setter)
384
- const { tools, setUploadFunction, setEventContext } = createMamaTools(executor, workspaceDir);
385
- // Resolve model from config
386
- // Use 'as any' cast because agentConfig.provider/model are plain strings,
387
- // while getModel() has constrained generic types for known providers.
388
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
389
- const model = getModel(agentConfig.provider, agentConfig.model);
390
- // Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
391
- const memory = await getMemory(conversationDir);
392
- const skills = loadMamaSkills(conversationDir, workspacePath);
393
- const emptyPlatform = {
394
- name: "slack",
395
- formattingGuide: "",
396
- channels: [],
397
- users: [],
419
+ return {
420
+ executionResolver,
421
+ executor,
422
+ getPathContext: () => executor.getPathContext(hostWorkspacePath),
423
+ async resolveExecutorForRun(context) {
424
+ if (!executionResolver)
425
+ return;
426
+ activeExecutor = await executionResolver.resolve(context);
427
+ },
398
428
  };
399
- const systemPrompt = buildSystemPrompt(workspacePath, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
400
- // Create session manager and settings manager
401
- // Conversation sessions use {conversationDir}/sessions/current.
402
- // Thread sessions use fixed files: {conversationDir}/sessions/{threadTs}.jsonl
403
- const sessionDir = getChannelSessionDir(conversationDir);
404
- const isThread = sessionKey.includes(":");
405
- let sessionManager;
406
- let contextFile;
407
- if (isThread) {
408
- const threadFile = getThreadSessionFile(conversationDir, sessionKey);
409
- const existing = tryResolveThreadSession(threadFile);
410
- if (existing) {
411
- contextFile = existing;
412
- sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
413
- }
414
- else {
415
- const conversationSource = resolveChannelSessionFile(conversationDir);
416
- if (conversationSource) {
417
- try {
418
- contextFile = forkThreadSessionFile(conversationSource, threadFile, conversationDir);
419
- sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
420
- }
421
- catch {
422
- contextFile = createManagedSessionFileAtPath(threadFile, conversationDir);
423
- sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
424
- }
425
- }
426
- else {
427
- contextFile = createManagedSessionFileAtPath(threadFile, conversationDir);
428
- sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
429
- }
430
- }
431
- }
432
- else {
433
- // Direct/shared session: normal resolve
434
- contextFile = resolveManagedSessionFile(sessionDir, conversationDir);
435
- sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
436
- }
437
- const sessionUuid = extractSessionUuid(contextFile);
438
- // Used for Slack thread filtering — for non-Slack platforms this is effectively a no-op
439
- const rootTs = extractSessionSuffix(sessionKey);
440
- const settingsManager = createMamaSettingsManager(join(conversationDir, ".."));
441
- // Create AuthStorage and ModelRegistry
442
- // Auth stored outside workspace so agent can't access it
429
+ }
430
+ async function createConfiguredAgentSession(params) {
431
+ const { conversationId, workspaceDir, runtimeWorkspaceRoot, systemPrompt, model, thinkingLevel, tools, sessionManager, settingsManager, } = params;
443
432
  const authStorage = AuthStorage.create(join(homedir(), ".pi", "mama", "auth.json"));
444
433
  const modelRegistry = ModelRegistry.create(authStorage);
445
- // Create agent
446
434
  const agent = new Agent({
447
435
  initialState: {
448
436
  systemPrompt,
449
437
  model,
450
- thinkingLevel: agentConfig.thinkingLevel ?? "off",
438
+ thinkingLevel,
451
439
  tools,
452
440
  },
453
441
  convertToLlm,
454
442
  getApiKey: async () => {
455
443
  const key = await modelRegistry.getApiKeyForProvider(model.provider);
456
- if (!key)
444
+ if (!key) {
457
445
  throw new Error(`No API key for provider "${model.provider}". Set the appropriate environment variable or configure via auth.json`);
446
+ }
458
447
  return key;
459
448
  },
460
449
  });
461
- // Load existing messages
462
450
  const loadedSession = sessionManager.buildSessionContext();
463
451
  if (loadedSession.messages.length > 0) {
464
452
  agent.state.messages = loadedSession.messages;
465
- log.logInfo(`[${conversationId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
453
+ log.logInfo(`[${conversationId}] Reloaded ${loadedSession.messages.length} messages from session context`);
466
454
  }
467
- // Load extensions, skills, prompts, themes via DefaultResourceLoader
468
- // This reads ~/.pi/agent/settings.json (packages, extensions enable/disable)
469
- // and discovers resources from standard locations + npm/git packages.
470
455
  const resourceLoader = new DefaultResourceLoader({
471
456
  cwd: workspaceDir,
472
457
  agentDir: getAgentDir(),
@@ -480,121 +465,407 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
480
465
  log.logWarning(`[${conversationId}] Extension load error: ${err.path}`, err.error);
481
466
  }
482
467
  }
483
- log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((e) => e.path).join(", ")}`);
468
+ log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((extension) => extension.path).join(", ")}`);
484
469
  }
485
470
  catch (error) {
486
471
  log.logWarning(`[${conversationId}] Failed to load resources`, String(error));
487
472
  }
488
473
  const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
489
- // Create AgentSession wrapper
490
474
  const session = new AgentSession({
491
475
  agent,
492
476
  sessionManager,
493
477
  settingsManager,
494
- cwd: workspaceDir,
478
+ cwd: runtimeWorkspaceRoot,
495
479
  modelRegistry,
496
480
  resourceLoader,
497
481
  baseToolsOverride,
498
482
  });
499
- // Mutable per-run state - event handler references this
500
- const runState = {
483
+ return { agent, session };
484
+ }
485
+ function createEmptyUsageTotals() {
486
+ return {
487
+ input: 0,
488
+ output: 0,
489
+ cacheRead: 0,
490
+ cacheWrite: 0,
491
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
492
+ };
493
+ }
494
+ function createRunState() {
495
+ return {
501
496
  responseCtx: null,
502
497
  logCtx: null,
503
498
  queue: null,
504
499
  pendingTools: new Map(),
505
- totalUsage: {
506
- input: 0,
507
- output: 0,
508
- cacheRead: 0,
509
- cacheWrite: 0,
510
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
511
- },
500
+ totalUsage: createEmptyUsageTotals(),
512
501
  llmCallCount: 0,
513
502
  stopReason: "stop",
514
503
  errorMessage: undefined,
504
+ reportedLlmError: false,
505
+ };
506
+ }
507
+ function resetRunState(runState, responseCtx, sessionConversation, userName, sessionUuid) {
508
+ runState.responseCtx = responseCtx;
509
+ runState.logCtx = {
510
+ conversationId: sessionConversation,
511
+ userName,
512
+ conversationName: undefined,
513
+ sessionId: sessionUuid,
515
514
  };
516
- // Subscribe to events ONCE
515
+ runState.pendingTools.clear();
516
+ runState.totalUsage = createEmptyUsageTotals();
517
+ runState.llmCallCount = 0;
518
+ runState.stopReason = "stop";
519
+ runState.errorMessage = undefined;
520
+ runState.reportedLlmError = false;
521
+ }
522
+ function createRunQueue(responseCtx) {
523
+ let queueChain = Promise.resolve();
524
+ return {
525
+ queue: {
526
+ enqueue(fn, errorContext) {
527
+ queueChain = queueChain.then(async () => {
528
+ try {
529
+ await fn();
530
+ }
531
+ catch (err) {
532
+ const errMsg = err instanceof Error ? err.message : String(err);
533
+ log.logWarning(`API error (${errorContext})`, errMsg);
534
+ try {
535
+ await responseCtx.respondDiagnostic(`Error: ${errMsg}`, { style: "error" });
536
+ }
537
+ catch {
538
+ // Ignore
539
+ }
540
+ }
541
+ });
542
+ },
543
+ },
544
+ wait: () => queueChain,
545
+ };
546
+ }
547
+ function padTwoDigits(n) {
548
+ return n.toString().padStart(2, "0");
549
+ }
550
+ function formatTimestampedUserMessage(message) {
551
+ const now = new Date();
552
+ const offset = -now.getTimezoneOffset();
553
+ const offsetSign = offset >= 0 ? "+" : "-";
554
+ const offsetHours = padTwoDigits(Math.floor(Math.abs(offset) / 60));
555
+ const offsetMins = padTwoDigits(Math.abs(offset) % 60);
556
+ const timestamp = `${now.getFullYear()}-${padTwoDigits(now.getMonth() + 1)}-${padTwoDigits(now.getDate())} ` +
557
+ `${padTwoDigits(now.getHours())}:${padTwoDigits(now.getMinutes())}:${padTwoDigits(now.getSeconds())}` +
558
+ `${offsetSign}${offsetHours}:${offsetMins}`;
559
+ const threadContext = message.threadTs ? ` [in-thread:${message.threadTs}]` : "";
560
+ return `[${timestamp}] [${message.userName || "unknown"}]${threadContext}: ${message.text}`;
561
+ }
562
+ function collectMessageAttachments(message, workspacePath) {
563
+ const imageAttachments = [];
564
+ const nonImagePaths = [];
565
+ for (const attachment of message.attachments || []) {
566
+ const fullPath = `${workspacePath}/${attachment.localPath}`;
567
+ const mimeType = getImageMimeType(attachment.localPath);
568
+ if (mimeType && existsSync(fullPath)) {
569
+ try {
570
+ imageAttachments.push({
571
+ type: "image",
572
+ mimeType,
573
+ data: readFileSync(fullPath).toString("base64"),
574
+ });
575
+ }
576
+ catch {
577
+ nonImagePaths.push(fullPath);
578
+ }
579
+ }
580
+ else {
581
+ nonImagePaths.push(fullPath);
582
+ }
583
+ }
584
+ return { imageAttachments, nonImagePaths };
585
+ }
586
+ function buildPromptPayload(message, workspacePath) {
587
+ let userMessage = formatTimestampedUserMessage(message);
588
+ const { imageAttachments, nonImagePaths } = collectMessageAttachments(message, workspacePath);
589
+ if (nonImagePaths.length > 0) {
590
+ userMessage += `\n\n<slack_attachments>\n${nonImagePaths.join("\n")}\n</slack_attachments>`;
591
+ }
592
+ return { userMessage, imageAttachments };
593
+ }
594
+ async function writePromptDebugContext(conversationDir, systemPrompt, session, userMessage, imageAttachmentCount) {
595
+ const debugContext = {
596
+ systemPrompt,
597
+ messages: session.messages,
598
+ newUserMessage: userMessage,
599
+ imageAttachmentCount,
600
+ };
601
+ await writeFile(join(conversationDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
602
+ }
603
+ function getFinalAssistantText(session) {
604
+ const lastAssistant = session.messages.filter((message) => message.role === "assistant").pop();
605
+ return (lastAssistant?.content
606
+ .filter((content) => content.type === "text")
607
+ .map((content) => content.text)
608
+ .join("\n") || "");
609
+ }
610
+ export function appendTriggerAttribution(text, triggerAttribution) {
611
+ if (!triggerAttribution)
612
+ return text;
613
+ const suffix = `_Triggered by ${triggerAttribution}_`;
614
+ if (text.trimEnd().endsWith(suffix))
615
+ return text;
616
+ return `${text.trimEnd()}\n\n${suffix}`;
617
+ }
618
+ async function finalizeRunResponse(responseCtx, session, runState, options) {
619
+ if (runState.stopReason === "error" && runState.errorMessage) {
620
+ if (!runState.reportedLlmError) {
621
+ runState.reportedLlmError = true;
622
+ reportUserFacingError(new Error("LLM run completed with error stop reason"), {
623
+ domain: "llm",
624
+ surface: "assistant_response",
625
+ operation: "llm_turn",
626
+ severity: "error",
627
+ platform: options?.platform,
628
+ provider: options?.model?.provider,
629
+ model: options?.model?.name,
630
+ stopReason: runState.stopReason,
631
+ context: {
632
+ sessionConversation: options?.sessionConversation,
633
+ sessionUuid: options?.sessionUuid,
634
+ hasErrorMessage: true,
635
+ llmCallCount: runState.llmCallCount,
636
+ },
637
+ });
638
+ }
639
+ try {
640
+ await responseCtx.replaceResponse("_Sorry, something went wrong_");
641
+ await responseCtx.respondDiagnostic(`Error: ${runState.errorMessage}`, {
642
+ style: "error",
643
+ });
644
+ }
645
+ catch (err) {
646
+ const errMsg = err instanceof Error ? err.message : String(err);
647
+ log.logWarning("Failed to post error message", errMsg);
648
+ reportUserFacingError(err, {
649
+ domain: "chat_platform",
650
+ surface: "final_response",
651
+ operation: "finalize_error_response",
652
+ severity: "error",
653
+ platform: options?.platform,
654
+ context: {
655
+ sessionConversation: options?.sessionConversation,
656
+ sessionUuid: options?.sessionUuid,
657
+ stopReason: runState.stopReason,
658
+ },
659
+ });
660
+ }
661
+ return;
662
+ }
663
+ const finalText = getFinalAssistantText(session);
664
+ if (finalText.trim() === "[SILENT]" || finalText.trim().startsWith("[SILENT]")) {
665
+ try {
666
+ await responseCtx.deleteResponse();
667
+ log.logInfo("Silent response - deleted message and thread");
668
+ }
669
+ catch (err) {
670
+ const errMsg = err instanceof Error ? err.message : String(err);
671
+ log.logWarning("Failed to delete message for silent response", errMsg);
672
+ }
673
+ return;
674
+ }
675
+ if (!finalText.trim())
676
+ return;
677
+ try {
678
+ await responseCtx.replaceResponse(appendTriggerAttribution(finalText, options?.triggerAttribution), { createOverflowLink: options?.createOverflowLink });
679
+ }
680
+ catch (err) {
681
+ const errMsg = err instanceof Error ? err.message : String(err);
682
+ log.logWarning("Failed to replace message with final text", errMsg);
683
+ reportUserFacingError(err, {
684
+ domain: "chat_platform",
685
+ surface: "final_response",
686
+ operation: "replace_final_response",
687
+ severity: "error",
688
+ platform: options?.platform,
689
+ context: {
690
+ sessionConversation: options?.sessionConversation,
691
+ sessionUuid: options?.sessionUuid,
692
+ finalTextLength: finalText.length,
693
+ },
694
+ });
695
+ }
696
+ }
697
+ async function reportUsageSummary(ctx) {
698
+ const { session, runState, responseCtx, platform, model, agentConfig, sessionConversation, sessionUuid, waitForQueue, } = ctx;
699
+ if (runState.totalUsage.cost.total <= 0)
700
+ return;
701
+ const lastAssistantMessage = session.messages
702
+ .slice()
703
+ .toReversed()
704
+ .find((message) => message.role === "assistant" && message.stopReason !== "aborted");
705
+ const contextTokens = lastAssistantMessage
706
+ ? lastAssistantMessage.usage.input +
707
+ lastAssistantMessage.usage.output +
708
+ lastAssistantMessage.usage.cacheRead +
709
+ lastAssistantMessage.usage.cacheWrite
710
+ : 0;
711
+ const contextWindow = model.contextWindow || 200000;
712
+ const { totalUsage } = runState;
713
+ const runMetricAttributes = metricAttributes({
714
+ provider: model.provider,
715
+ model: agentConfig.model,
716
+ channel_id: sessionConversation,
717
+ session_id: sessionUuid,
718
+ stop_reason: runState.stopReason,
719
+ llm_calls: runState.llmCallCount,
720
+ });
721
+ Sentry.metrics.distribution("agent.run.tokens_in", totalUsage.input, {
722
+ attributes: runMetricAttributes,
723
+ });
724
+ Sentry.metrics.distribution("agent.run.tokens_out", totalUsage.output, {
725
+ attributes: runMetricAttributes,
726
+ });
727
+ Sentry.metrics.distribution("agent.run.cache_read", totalUsage.cacheRead, {
728
+ attributes: runMetricAttributes,
729
+ });
730
+ Sentry.metrics.distribution("agent.run.cache_write", totalUsage.cacheWrite, {
731
+ attributes: runMetricAttributes,
732
+ });
733
+ Sentry.metrics.distribution("agent.run.cost", totalUsage.cost.total, {
734
+ attributes: runMetricAttributes,
735
+ });
736
+ Sentry.metrics.gauge("agent.context.utilization", contextTokens / contextWindow, {
737
+ unit: "ratio",
738
+ attributes: runMetricAttributes,
739
+ });
740
+ const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
741
+ if (platform.diagnostics?.showUsageSummary === true) {
742
+ runState.queue.enqueue(() => responseCtx.respondDiagnostic(summary, { style: "muted" }), "usage summary");
743
+ await waitForQueue();
744
+ }
745
+ }
746
+ function reloadSessionMessages(sessionManager, conversationId, agent) {
747
+ const messages = sessionManager.buildSessionContext().messages;
748
+ if (messages.length > 0) {
749
+ agent.state.messages = messages;
750
+ log.logInfo(`[${conversationId}] Reloaded ${messages.length} messages from context`);
751
+ }
752
+ }
753
+ async function prepareRunContext(params) {
754
+ const { message, responseCtx, platform, conversationId, conversationDir, sessionUuid, runState, executor, executionResolver, resolveExecutorForRun, getPathContext, sessionManager, session, agent, setEventContext, setUploadFunction, } = params;
755
+ let pathContext = params.pathContext;
756
+ const sessionConversation = message.sessionKey.split(":")[0];
757
+ await mkdir(join(conversationDir, "scratch"), { recursive: true });
758
+ if (executionResolver) {
759
+ await resolveExecutorForRun({
760
+ platform: platform.name,
761
+ userId: message.userId,
762
+ conversationId,
763
+ });
764
+ pathContext = getPathContext();
765
+ }
766
+ reloadSessionMessages(sessionManager, conversationId, agent);
767
+ const memory = await getMemory(conversationDir);
768
+ const skills = loadMamaSkills(conversationDir, pathContext.runtimeWorkspaceRoot);
769
+ const triggerAttribution = resolveTriggerAttribution(message);
770
+ const systemPrompt = buildSystemPrompt(pathContext.runtimeWorkspaceRoot, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills, message.id.startsWith("event:"), triggerAttribution);
771
+ session.agent.state.systemPrompt = systemPrompt;
772
+ setEventContext({
773
+ platform: platform.name,
774
+ conversationId,
775
+ conversationKind: message.conversationKind,
776
+ userId: message.userId,
777
+ });
778
+ setUploadFunction(async (filePath, title) => {
779
+ const hostPath = translateRuntimePathToHost(filePath, pathContext);
780
+ await responseCtx.uploadFile(hostPath, title);
781
+ });
782
+ resetRunState(runState, responseCtx, sessionConversation, message.userName, sessionUuid);
783
+ const runQueue = createRunQueue(responseCtx);
784
+ runState.queue = runQueue.queue;
785
+ log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
786
+ log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
787
+ const { userMessage, imageAttachments } = buildPromptPayload(message, pathContext.runtimeWorkspaceRoot);
788
+ await writePromptDebugContext(conversationDir, systemPrompt, session, userMessage, imageAttachments.length);
789
+ return {
790
+ sessionConversation,
791
+ runQueue,
792
+ userMessage,
793
+ imageAttachments,
794
+ triggerAttribution,
795
+ pathContext,
796
+ };
797
+ }
798
+ function attachSessionEventHandlers(params) {
799
+ const { session, runState, model, agentConfig } = params;
517
800
  session.subscribe(async (event) => {
518
- // Skip if no active run
519
801
  if (!runState.responseCtx || !runState.logCtx || !runState.queue)
520
802
  return;
521
803
  const { responseCtx, logCtx, queue, pendingTools } = runState;
522
804
  const baseAttrs = { channel_id: logCtx.conversationId, session_id: logCtx.sessionId };
523
805
  if (event.type === "tool_execution_start") {
524
- const agentEvent = event;
525
- const args = agentEvent.args;
526
- const label = args.label || agentEvent.toolName;
527
- pendingTools.set(agentEvent.toolCallId, {
528
- toolName: agentEvent.toolName,
529
- args: agentEvent.args,
806
+ const args = (event.args ?? {});
807
+ const label = args.label || event.toolName;
808
+ pendingTools.set(event.toolCallId, {
809
+ toolName: event.toolName,
810
+ args: event.args,
530
811
  startTime: Date.now(),
531
812
  });
532
813
  addLifecycleBreadcrumb("agent.tool.started", {
533
- tool: agentEvent.toolName,
814
+ tool: event.toolName,
534
815
  ...baseAttrs,
535
816
  });
536
- log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
537
- // Tool labels are omitted from the main message to reduce Slack noise.
538
- // Tool execution details are still posted to the thread (see tool_execution_end).
817
+ log.logToolStart(logCtx, event.toolName, label, event.args);
818
+ return;
539
819
  }
540
- else if (event.type === "tool_execution_end") {
541
- const agentEvent = event;
542
- const resultStr = extractToolResultText(agentEvent.result);
543
- const pending = pendingTools.get(agentEvent.toolCallId);
544
- pendingTools.delete(agentEvent.toolCallId);
820
+ if (event.type === "tool_execution_end") {
821
+ const resultStr = extractToolResultText(event.result);
822
+ const pending = pendingTools.get(event.toolCallId);
823
+ pendingTools.delete(event.toolCallId);
545
824
  const durationMs = pending ? Date.now() - pending.startTime : 0;
546
825
  Sentry.metrics.count("agent.tool.calls", 1, {
547
826
  attributes: metricAttributes({
548
- tool: agentEvent.toolName,
549
- error: String(agentEvent.isError),
827
+ tool: event.toolName,
828
+ error: String(event.isError),
550
829
  ...baseAttrs,
551
830
  }),
552
831
  });
553
832
  Sentry.metrics.distribution("agent.tool.duration", durationMs, {
554
833
  unit: "millisecond",
555
834
  attributes: metricAttributes({
556
- tool: agentEvent.toolName,
835
+ tool: event.toolName,
557
836
  ...baseAttrs,
558
837
  }),
559
838
  });
560
839
  addLifecycleBreadcrumb("agent.tool.completed", {
561
- tool: agentEvent.toolName,
562
- error: agentEvent.isError,
840
+ tool: event.toolName,
841
+ error: event.isError,
563
842
  duration_ms: durationMs,
564
843
  ...baseAttrs,
565
844
  });
566
- if (agentEvent.isError) {
567
- log.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);
845
+ if (event.isError) {
846
+ log.logToolError(logCtx, event.toolName, durationMs, resultStr);
568
847
  }
569
848
  else {
570
- log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
849
+ log.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);
571
850
  }
572
- // Post args + result to thread
573
- const label = pending?.args ? pending.args.label : undefined;
574
- const argsFormatted = pending
575
- ? formatToolArgsForSlack(agentEvent.toolName, pending.args)
576
- : "(args not found)";
577
- const duration = (durationMs / 1000).toFixed(1);
578
- let threadMessage = `*${agentEvent.isError ? "✗" : "✓"} ${agentEvent.toolName}*`;
579
- if (label)
580
- threadMessage += `: ${label}`;
581
- threadMessage += ` (${duration}s)\n`;
582
- if (argsFormatted)
583
- threadMessage += `\`\`\`\n${argsFormatted}\n\`\`\`\n`;
584
- threadMessage += `*Result:*\n\`\`\`\n${resultStr}\n\`\`\``;
585
- // Only post thread details for tools with meaningful output (bash, attach).
586
- // Skip read/write/edit to reduce Slack noise — their results are in the log.
587
- const quietTools = new Set(["read", "write", "edit"]);
588
- if (!quietTools.has(agentEvent.toolName)) {
589
- queue.enqueueMessage(threadMessage, "thread", "tool result thread", false);
851
+ if (shouldSurfaceToolDiagnostic(event.toolName)) {
852
+ const toolResult = {
853
+ toolName: event.toolName,
854
+ label: pending?.args ? pending.args.label : undefined,
855
+ args: pending?.args,
856
+ result: truncate(resultStr, TOOL_RESULT_DIAGNOSTIC_CAP),
857
+ isError: event.isError,
858
+ durationMs,
859
+ };
860
+ queue.enqueue(() => responseCtx.respondToolResult(toolResult), "tool result diagnostic");
590
861
  }
591
- if (agentEvent.isError) {
862
+ if (event.isError && shouldSurfaceToolDiagnostic(event.toolName)) {
592
863
  queue.enqueue(() => responseCtx.respond(`_Error: ${truncate(resultStr, 200)}_`), "tool error");
593
864
  }
865
+ return;
594
866
  }
595
- else if (event.type === "message_start") {
596
- const agentEvent = event;
597
- if (agentEvent.message.role === "assistant") {
867
+ if (event.type === "message_start") {
868
+ if (event.message.role === "assistant") {
598
869
  runState.llmCallCount += 1;
599
870
  addLifecycleBreadcrumb("agent.llm.call.started", {
600
871
  call_index: runState.llmCallCount,
@@ -604,11 +875,11 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
604
875
  });
605
876
  log.logResponseStart(logCtx);
606
877
  }
878
+ return;
607
879
  }
608
- else if (event.type === "message_end") {
609
- const agentEvent = event;
610
- if (agentEvent.message.role === "assistant") {
611
- const assistantMsg = agentEvent.message;
880
+ if (event.type === "message_end") {
881
+ if (event.message.role === "assistant") {
882
+ const assistantMsg = event.message;
612
883
  if (assistantMsg.stopReason) {
613
884
  runState.stopReason = assistantMsg.stopReason;
614
885
  }
@@ -625,7 +896,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
625
896
  runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
626
897
  runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
627
898
  runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
628
- // Per-turn LLM metrics
629
899
  const llmAttributes = metricAttributes({
630
900
  provider: model.provider,
631
901
  model: agentConfig.model,
@@ -664,10 +934,9 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
664
934
  cost_total_usd: assistantMsg.usage.cost.total,
665
935
  });
666
936
  }
667
- const content = agentEvent.message.content;
668
937
  const thinkingParts = [];
669
938
  const textParts = [];
670
- for (const part of content) {
939
+ for (const part of assistantMsg.content) {
671
940
  if (part.type === "thinking") {
672
941
  thinkingParts.push(part.thinking);
673
942
  }
@@ -678,310 +947,175 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
678
947
  const text = textParts.join("\n");
679
948
  for (const thinking of thinkingParts) {
680
949
  log.logThinking(logCtx, thinking);
681
- queue.enqueueMessage(`_${thinking}_`, "main", "thinking main");
682
- queue.enqueueMessage(`_${thinking}_`, "thread", "thinking thread", false);
950
+ queue.enqueue(() => responseCtx.respond(`_${thinking}_`), "thinking main");
951
+ queue.enqueue(() => responseCtx.respondDiagnostic(`_${thinking}_`), "thinking diagnostic");
683
952
  }
684
953
  if (text.trim()) {
685
954
  log.logResponse(logCtx, text);
686
- queue.enqueueMessage(text, "main", "response main");
687
- // Only overflow to thread for texts that will be truncated in main
688
- if (text.length > SLACK_MAX_LENGTH) {
689
- queue.enqueueMessage(text, "thread", "response thread", false);
690
- }
955
+ queue.enqueue(() => responseCtx.respond(text), "response main");
691
956
  }
692
957
  }
958
+ return;
693
959
  }
694
- else if (event.type === "compaction_start") {
960
+ if (event.type === "compaction_start") {
695
961
  log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
696
962
  queue.enqueue(() => responseCtx.respond("_Compacting context..._"), "compaction start");
963
+ return;
697
964
  }
698
- else if (event.type === "compaction_end") {
699
- const compEvent = event;
700
- if (compEvent.result) {
701
- log.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);
965
+ if (event.type === "compaction_end") {
966
+ if (event.result) {
967
+ log.logInfo(`Auto-compaction complete: ${event.result.tokensBefore} tokens compacted`);
702
968
  }
703
- else if (compEvent.aborted) {
969
+ else if (event.aborted) {
704
970
  log.logInfo("Auto-compaction aborted");
705
971
  }
972
+ return;
706
973
  }
707
- else if (event.type === "auto_retry_start") {
708
- const retryEvent = event;
709
- log.logWarning(`Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})`, retryEvent.errorMessage);
710
- queue.enqueue(() => responseCtx.respond(`_Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})..._`), "retry");
974
+ if (event.type === "auto_retry_start") {
975
+ log.logWarning(`Retrying (${event.attempt}/${event.maxAttempts})`, event.errorMessage);
976
+ queue.enqueue(() => responseCtx.respond(`_Retrying (${event.attempt}/${event.maxAttempts})..._`), "retry");
711
977
  }
712
978
  });
713
- // Message limit constant
714
- const SLACK_MAX_LENGTH = 40000;
715
- const splitForSlack = (text) => {
716
- if (text.length <= SLACK_MAX_LENGTH)
717
- return [text];
718
- const parts = [];
719
- let remaining = text;
720
- let partNum = 1;
721
- while (remaining.length > 0) {
722
- const chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);
723
- remaining = remaining.substring(SLACK_MAX_LENGTH - 50);
724
- const suffix = remaining.length > 0 ? `\n_(continued ${partNum}...)_` : "";
725
- parts.push(chunk + suffix);
726
- partNum++;
979
+ }
980
+ // Cap raw tool output before handing it to adapters. Bash output can be MB; without
981
+ // this each adapter's splitter would fan it out into many sequential platform posts.
982
+ const TOOL_RESULT_DIAGNOSTIC_CAP = 8000;
983
+ function extractToolResultText(result) {
984
+ if (typeof result === "string") {
985
+ return result;
986
+ }
987
+ if (result &&
988
+ typeof result === "object" &&
989
+ "content" in result &&
990
+ Array.isArray(result.content)) {
991
+ const content = result.content;
992
+ const textParts = [];
993
+ for (const part of content) {
994
+ if (part.type === "text" && part.text) {
995
+ textParts.push(part.text);
996
+ }
997
+ }
998
+ if (textParts.length > 0) {
999
+ return textParts.join("\n");
727
1000
  }
728
- return parts;
1001
+ }
1002
+ return JSON.stringify(result);
1003
+ }
1004
+ /**
1005
+ * Create a new AgentRunner for a channel.
1006
+ * Sets up the session and subscribes to events once.
1007
+ *
1008
+ * Runner caching is handled by the caller (channelStates in main.ts).
1009
+ * This is a stateless factory function.
1010
+ */
1011
+ export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, provisioner, sessionView) {
1012
+ const agentConfig = loadAgentConfigForConversation(conversationDir);
1013
+ const workspaceBase = join(conversationDir, "..");
1014
+ const { executionResolver, executor, getPathContext, resolveExecutorForRun } = createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, workspaceBase);
1015
+ let pathContext = getUnresolvedSandboxPathContext(sandboxConfig, workspaceBase);
1016
+ // Create tools (per-runner, with per-runner upload function setter)
1017
+ const { tools, setUploadFunction, setEventContext } = createMamaTools(executor, workspaceDir);
1018
+ // Resolve model from config
1019
+ // Use 'as any' cast because agentConfig.provider/model are plain strings,
1020
+ // while getModel() has constrained generic types for known providers.
1021
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1022
+ const model = getModel(agentConfig.provider, agentConfig.model);
1023
+ // Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
1024
+ const memory = await getMemory(conversationDir);
1025
+ const skills = loadMamaSkills(conversationDir, pathContext.runtimeWorkspaceRoot);
1026
+ const emptyPlatform = {
1027
+ name: "chat",
1028
+ formattingGuide: "",
1029
+ channels: [],
1030
+ users: [],
729
1031
  };
1032
+ const systemPrompt = buildSystemPrompt(pathContext.runtimeWorkspaceRoot, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
1033
+ // Create session manager and settings manager. Top-level/private sessions
1034
+ // use the conversation's current pointer; scoped sessions use fixed files.
1035
+ // Platform-specific branch/fork behavior is resolved before runner creation.
1036
+ const isThread = sessionKey.includes(":");
1037
+ const { sessionDir, contextFile, threadRootMessage } = sessionScope;
1038
+ const sessionManager = openManagedSession(contextFile, sessionDir, pathContext.runtimeWorkspaceRoot);
1039
+ const threadSessionName = buildThreadSessionName(threadRootMessage);
1040
+ if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
1041
+ sessionManager.appendSessionInfo(threadSessionName);
1042
+ }
1043
+ const sessionUuid = extractSessionUuid(contextFile);
1044
+ const settingsManager = SettingsManager.inMemory();
1045
+ const { agent, session } = await createConfiguredAgentSession({
1046
+ conversationId,
1047
+ workspaceDir,
1048
+ runtimeWorkspaceRoot: pathContext.runtimeWorkspaceRoot,
1049
+ systemPrompt,
1050
+ model,
1051
+ thinkingLevel: agentConfig.thinkingLevel,
1052
+ tools,
1053
+ sessionManager,
1054
+ settingsManager,
1055
+ });
1056
+ // Mutable per-run state - event handler references this
1057
+ const runState = createRunState();
1058
+ attachSessionEventHandlers({ session, runState, model, agentConfig });
730
1059
  return {
731
1060
  async run(message, responseCtx, platform) {
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
- }
744
- // Sync messages from log.jsonl that arrived while we were offline or busy
745
- // Exclude the current message (it will be added via prompt())
746
- // Default sync range is 10 days (handled by syncLogToSessionManager)
747
- // Thread filter ensures only messages from this session's thread are synced
748
- const threadFilter = message.sessionKey.includes(":")
749
- ? { scope: "thread", rootTs, threadTs: message.threadTs }
750
- : { scope: "top-level", rootTs };
751
- const syncedCount = await syncLogToSessionManager(sessionManager, conversationDir, message.id, undefined, threadFilter);
752
- if (syncedCount > 0) {
753
- log.logInfo(`[${conversationId}] Synced ${syncedCount} messages from log.jsonl`);
754
- }
755
- // Reload messages from context.jsonl
756
- // This picks up any messages synced above
757
- const reloadedSession = sessionManager.buildSessionContext();
758
- if (reloadedSession.messages.length > 0) {
759
- agent.state.messages = reloadedSession.messages;
760
- log.logInfo(`[${conversationId}] Reloaded ${reloadedSession.messages.length} messages from context`);
761
- }
762
- // Update system prompt with fresh memory, channel/user info, and skills
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,
1061
+ const prepared = await prepareRunContext({
1062
+ message,
1063
+ responseCtx,
1064
+ platform,
769
1065
  conversationId,
770
- conversationKind: message.conversationKind,
771
- userId: message.userId,
772
- });
773
- // Set up file upload function
774
- setUploadFunction(async (filePath, title) => {
775
- const hostPath = translateToHostPath(filePath, conversationDir, workspacePath, conversationId);
776
- await responseCtx.uploadFile(hostPath, title);
1066
+ conversationDir,
1067
+ sessionUuid,
1068
+ runState,
1069
+ executor,
1070
+ executionResolver,
1071
+ resolveExecutorForRun,
1072
+ getPathContext,
1073
+ sessionManager,
1074
+ session,
1075
+ agent,
1076
+ setEventContext,
1077
+ setUploadFunction,
1078
+ pathContext,
777
1079
  });
778
- // Reset per-run state
779
- runState.responseCtx = responseCtx;
780
- runState.logCtx = {
781
- conversationId: sessionConversation,
782
- userName: message.userName,
783
- conversationName: undefined,
784
- sessionId: sessionUuid,
785
- };
786
- runState.pendingTools.clear();
787
- runState.totalUsage = {
788
- input: 0,
789
- output: 0,
790
- cacheRead: 0,
791
- cacheWrite: 0,
792
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
793
- };
794
- runState.llmCallCount = 0;
795
- runState.stopReason = "stop";
796
- runState.errorMessage = undefined;
797
- // Create queue for this run
798
- let queueChain = Promise.resolve();
799
- runState.queue = {
800
- enqueue(fn, errorContext) {
801
- queueChain = queueChain.then(async () => {
802
- try {
803
- await fn();
804
- }
805
- catch (err) {
806
- const errMsg = err instanceof Error ? err.message : String(err);
807
- log.logWarning(`API error (${errorContext})`, errMsg);
808
- try {
809
- // Split long error messages to avoid msg_too_long
810
- const errParts = splitForSlack(`_Error: ${errMsg}_`);
811
- for (const part of errParts) {
812
- await responseCtx.respondInThread(part);
813
- }
814
- }
815
- catch {
816
- // Ignore
817
- }
818
- }
819
- });
820
- },
821
- enqueueMessage(text, target, errorContext, _doLog = true) {
822
- const parts = splitForSlack(text);
823
- for (const part of parts) {
824
- this.enqueue(() => target === "main" ? responseCtx.respond(part) : responseCtx.respondInThread(part), errorContext);
825
- }
826
- },
827
- };
828
- // Log context info
829
- log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
830
- log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
831
- // Build user message with timestamp and username prefix
832
- // Format: "[YYYY-MM-DD HH:MM:SS+HH:MM] [username]: message" so LLM knows when and who
833
- const now = new Date();
834
- const pad = (n) => n.toString().padStart(2, "0");
835
- const offset = -now.getTimezoneOffset();
836
- const offsetSign = offset >= 0 ? "+" : "-";
837
- const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
838
- const offsetMins = pad(Math.abs(offset) % 60);
839
- const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
840
- const threadContext = message.threadTs ? ` [in-thread:${message.threadTs}]` : "";
841
- let userMessage = `[${timestamp}] [${message.userName || "unknown"}]${threadContext}: ${message.text}`;
842
- const imageAttachments = [];
843
- const nonImagePaths = [];
844
- for (const a of message.attachments || []) {
845
- // a.localPath is the path relative to the workspace.
846
- const fullPath = `${workspacePath}/${a.localPath}`;
847
- const mimeType = getImageMimeType(a.localPath);
848
- if (mimeType && existsSync(fullPath)) {
849
- try {
850
- imageAttachments.push({
851
- type: "image",
852
- mimeType,
853
- data: readFileSync(fullPath).toString("base64"),
854
- });
855
- }
856
- catch {
857
- nonImagePaths.push(fullPath);
858
- }
859
- }
860
- else {
861
- nonImagePaths.push(fullPath);
862
- }
863
- }
864
- if (nonImagePaths.length > 0) {
865
- userMessage += `\n\n<slack_attachments>\n${nonImagePaths.join("\n")}\n</slack_attachments>`;
866
- }
867
- // Debug: write context to last_prompt.jsonl
868
- const debugContext = {
869
- systemPrompt,
870
- messages: session.messages,
871
- newUserMessage: userMessage,
872
- imageAttachmentCount: imageAttachments.length,
873
- };
874
- await writeFile(join(conversationDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
1080
+ pathContext = prepared.pathContext;
875
1081
  addLifecycleBreadcrumb("agent.prompt.sent", {
876
1082
  provider: model.provider,
877
1083
  model: agentConfig.model,
878
- channel_id: sessionConversation,
1084
+ channel_id: prepared.sessionConversation,
879
1085
  session_id: sessionUuid,
880
1086
  attachment_count: message.attachments?.length ?? 0,
881
- image_attachment_count: imageAttachments.length,
1087
+ image_attachment_count: prepared.imageAttachments.length,
882
1088
  });
883
- await session.prompt(userMessage, imageAttachments.length > 0 ? { images: imageAttachments } : undefined);
1089
+ await session.prompt(prepared.userMessage, prepared.imageAttachments.length > 0 ? { images: prepared.imageAttachments } : undefined);
884
1090
  // Wait for queued messages
885
- await queueChain;
886
- // Handle error case - update main message and post error to thread
887
- if (runState.stopReason === "error" && runState.errorMessage) {
888
- try {
889
- await responseCtx.replaceResponse("_Sorry, something went wrong_");
890
- // Split long error messages to avoid msg_too_long
891
- const errorParts = splitForSlack(`_Error: ${runState.errorMessage}_`);
892
- for (const part of errorParts) {
893
- await responseCtx.respondInThread(part);
894
- }
895
- }
896
- catch (err) {
897
- const errMsg = err instanceof Error ? err.message : String(err);
898
- log.logWarning("Failed to post error message", errMsg);
899
- }
900
- }
901
- else {
902
- // Final message update
903
- const messages = session.messages;
904
- const lastAssistant = messages.filter((m) => m.role === "assistant").pop();
905
- const finalText = lastAssistant?.content
906
- .filter((c) => c.type === "text")
907
- .map((c) => c.text)
908
- .join("\n") || "";
909
- // Check for [SILENT] marker - delete message and thread instead of posting
910
- if (finalText.trim() === "[SILENT]" || finalText.trim().startsWith("[SILENT]")) {
911
- try {
912
- await responseCtx.deleteResponse();
913
- log.logInfo("Silent response - deleted message and thread");
914
- }
915
- catch (err) {
916
- const errMsg = err instanceof Error ? err.message : String(err);
917
- log.logWarning("Failed to delete message for silent response", errMsg);
918
- }
1091
+ await prepared.runQueue.wait();
1092
+ const sessionViewTokenStore = sessionView?.tokenStore;
1093
+ const sessionViewPortalBaseUrl = sessionView?.portalBaseUrl;
1094
+ const createOverflowLink = sessionViewTokenStore && sessionViewPortalBaseUrl
1095
+ ? () => {
1096
+ const token = sessionViewTokenStore.create(platform.name, message.userId, conversationId, message.sessionKey, contextFile, message.userName);
1097
+ return `${sessionViewPortalBaseUrl}/session?token=${token.token}`;
919
1098
  }
920
- else if (finalText.trim()) {
921
- try {
922
- const mainText = finalText.length > SLACK_MAX_LENGTH
923
- ? `${finalText.substring(0, SLACK_MAX_LENGTH - 50)}\n\n_(see thread for full response)_`
924
- : finalText;
925
- await responseCtx.replaceResponse(mainText);
926
- }
927
- catch (err) {
928
- const errMsg = err instanceof Error ? err.message : String(err);
929
- log.logWarning("Failed to replace message with final text", errMsg);
930
- }
931
- }
932
- }
933
- // Log usage summary with context info
934
- if (runState.totalUsage.cost.total > 0) {
935
- // Get last non-aborted assistant message for context calculation
936
- const messages = session.messages;
937
- const lastAssistantMessage = messages
938
- .slice()
939
- .reverse()
940
- .find((m) => m.role === "assistant" && m.stopReason !== "aborted");
941
- const contextTokens = lastAssistantMessage
942
- ? lastAssistantMessage.usage.input +
943
- lastAssistantMessage.usage.output +
944
- lastAssistantMessage.usage.cacheRead +
945
- lastAssistantMessage.usage.cacheWrite
946
- : 0;
947
- const contextWindow = model.contextWindow || 200000;
948
- // Run-level Sentry metrics
949
- const { totalUsage } = runState;
950
- const runMetricAttributes = metricAttributes({
951
- provider: model.provider,
952
- model: agentConfig.model,
953
- channel_id: sessionConversation,
954
- session_id: sessionUuid,
955
- stop_reason: runState.stopReason,
956
- llm_calls: runState.llmCallCount,
957
- });
958
- Sentry.metrics.distribution("agent.run.tokens_in", totalUsage.input, {
959
- attributes: runMetricAttributes,
960
- });
961
- Sentry.metrics.distribution("agent.run.tokens_out", totalUsage.output, {
962
- attributes: runMetricAttributes,
963
- });
964
- Sentry.metrics.distribution("agent.run.cache_read", totalUsage.cacheRead, {
965
- attributes: runMetricAttributes,
966
- });
967
- Sentry.metrics.distribution("agent.run.cache_write", totalUsage.cacheWrite, {
968
- attributes: runMetricAttributes,
969
- });
970
- Sentry.metrics.distribution("agent.run.cost", totalUsage.cost.total, {
971
- attributes: runMetricAttributes,
972
- });
973
- Sentry.metrics.gauge("agent.context.utilization", contextTokens / contextWindow, {
974
- unit: "ratio",
975
- attributes: runMetricAttributes,
976
- });
977
- const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
978
- // Split long summaries to avoid msg_too_long
979
- const summaryParts = splitForSlack(summary);
980
- for (const part of summaryParts) {
981
- runState.queue.enqueue(() => responseCtx.respondInThread(part, { style: "muted" }), "usage summary");
982
- }
983
- await queueChain;
984
- }
1099
+ : undefined;
1100
+ await finalizeRunResponse(responseCtx, session, runState, {
1101
+ triggerAttribution: prepared.triggerAttribution,
1102
+ createOverflowLink,
1103
+ platform: platform.name,
1104
+ model,
1105
+ sessionConversation: prepared.sessionConversation,
1106
+ sessionUuid,
1107
+ });
1108
+ await reportUsageSummary({
1109
+ session,
1110
+ runState,
1111
+ responseCtx,
1112
+ platform,
1113
+ model,
1114
+ agentConfig,
1115
+ sessionConversation: prepared.sessionConversation,
1116
+ sessionUuid,
1117
+ waitForQueue: () => prepared.runQueue.wait(),
1118
+ });
985
1119
  // Clear run state
986
1120
  runState.responseCtx = null;
987
1121
  runState.logCtx = null;
@@ -1006,19 +1140,10 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
1006
1140
  },
1007
1141
  };
1008
1142
  }
1009
- /**
1010
- * Translate container path back to host path for file operations
1011
- */
1012
- function translateToHostPath(containerPath, conversationDir, workspacePath, conversationId) {
1013
- if (workspacePath === "/workspace") {
1014
- const prefix = `/workspace/${conversationId}/`;
1015
- if (containerPath.startsWith(prefix)) {
1016
- return join(conversationDir, containerPath.slice(prefix.length));
1017
- }
1018
- if (containerPath.startsWith("/workspace/")) {
1019
- return join(conversationDir, "..", containerPath.slice("/workspace/".length));
1020
- }
1021
- }
1022
- return containerPath;
1143
+ export function translateRuntimePathToHost(runtimePath, pathContext) {
1144
+ return pathContext.runtimeToHostPath?.(runtimePath) ?? runtimePath;
1145
+ }
1146
+ export function buildInitialPathContextForTest(sandboxConfig, hostWorkspaceRoot) {
1147
+ return getUnresolvedSandboxPathContext(sandboxConfig, hostWorkspaceRoot);
1023
1148
  }
1024
1149
  //# sourceMappingURL=agent.js.map