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

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