@geminixiang/mama 0.2.0-beta.4 → 0.2.0-beta.5

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 (122) hide show
  1. package/README.md +100 -421
  2. package/dist/adapter.d.ts +9 -0
  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 +1 -0
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +4 -1
  8. package/dist/adapters/discord/bot.js.map +1 -1
  9. package/dist/adapters/discord/context.d.ts.map +1 -1
  10. package/dist/adapters/discord/context.js +9 -2
  11. package/dist/adapters/discord/context.js.map +1 -1
  12. package/dist/adapters/slack/bot.d.ts +1 -0
  13. package/dist/adapters/slack/bot.d.ts.map +1 -1
  14. package/dist/adapters/slack/bot.js +9 -3
  15. package/dist/adapters/slack/bot.js.map +1 -1
  16. package/dist/adapters/slack/context.d.ts.map +1 -1
  17. package/dist/adapters/slack/context.js +13 -3
  18. package/dist/adapters/slack/context.js.map +1 -1
  19. package/dist/adapters/telegram/context.d.ts.map +1 -1
  20. package/dist/adapters/telegram/context.js +9 -2
  21. package/dist/adapters/telegram/context.js.map +1 -1
  22. package/dist/agent.d.ts.map +1 -1
  23. package/dist/agent.js +13 -5
  24. package/dist/agent.js.map +1 -1
  25. package/dist/bindings.d.ts +2 -1
  26. package/dist/bindings.d.ts.map +1 -1
  27. package/dist/bindings.js.map +1 -1
  28. package/dist/commands/index.d.ts +5 -0
  29. package/dist/commands/index.d.ts.map +1 -0
  30. package/dist/commands/index.js +8 -0
  31. package/dist/commands/index.js.map +1 -0
  32. package/dist/commands/login.d.ts +5 -0
  33. package/dist/commands/login.d.ts.map +1 -0
  34. package/dist/commands/login.js +37 -0
  35. package/dist/commands/login.js.map +1 -0
  36. package/dist/commands/registry.d.ts +7 -0
  37. package/dist/commands/registry.d.ts.map +1 -0
  38. package/dist/commands/registry.js +14 -0
  39. package/dist/commands/registry.js.map +1 -0
  40. package/dist/commands/session-view.d.ts +5 -0
  41. package/dist/commands/session-view.d.ts.map +1 -0
  42. package/dist/commands/session-view.js +38 -0
  43. package/dist/commands/session-view.js.map +1 -0
  44. package/dist/commands/types.d.ts +41 -0
  45. package/dist/commands/types.d.ts.map +1 -0
  46. package/dist/commands/types.js +2 -0
  47. package/dist/commands/types.js.map +1 -0
  48. package/dist/commands/utils.d.ts +5 -0
  49. package/dist/commands/utils.d.ts.map +1 -0
  50. package/dist/commands/utils.js +9 -0
  51. package/dist/commands/utils.js.map +1 -0
  52. package/dist/config.d.ts +4 -4
  53. package/dist/config.d.ts.map +1 -1
  54. package/dist/config.js +34 -40
  55. package/dist/config.js.map +1 -1
  56. package/dist/context.d.ts.map +1 -1
  57. package/dist/context.js +74 -68
  58. package/dist/context.js.map +1 -1
  59. package/dist/execution-resolver.d.ts +6 -3
  60. package/dist/execution-resolver.d.ts.map +1 -1
  61. package/dist/execution-resolver.js +47 -14
  62. package/dist/execution-resolver.js.map +1 -1
  63. package/dist/index.d.ts +7 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +4 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/instrument.d.ts.map +1 -1
  68. package/dist/instrument.js +2 -3
  69. package/dist/instrument.js.map +1 -1
  70. package/dist/login/index.d.ts.map +1 -1
  71. package/dist/login/index.js +19 -8
  72. package/dist/login/index.js.map +1 -1
  73. package/dist/login/portal.d.ts.map +1 -1
  74. package/dist/login/portal.js +7 -7
  75. package/dist/login/portal.js.map +1 -1
  76. package/dist/login/session.d.ts +3 -2
  77. package/dist/login/session.d.ts.map +1 -1
  78. package/dist/login/session.js.map +1 -1
  79. package/dist/main.d.ts.map +1 -1
  80. package/dist/main.js +62 -386
  81. package/dist/main.js.map +1 -1
  82. package/dist/provisioner.d.ts +11 -9
  83. package/dist/provisioner.d.ts.map +1 -1
  84. package/dist/provisioner.js +125 -87
  85. package/dist/provisioner.js.map +1 -1
  86. package/dist/runtime/index.d.ts +2 -0
  87. package/dist/runtime/index.d.ts.map +1 -0
  88. package/dist/runtime/index.js +2 -0
  89. package/dist/runtime/index.js.map +1 -0
  90. package/dist/runtime/session-runtime.d.ts +26 -0
  91. package/dist/runtime/session-runtime.d.ts.map +1 -0
  92. package/dist/runtime/session-runtime.js +285 -0
  93. package/dist/runtime/session-runtime.js.map +1 -0
  94. package/dist/sandbox/cloudflare.d.ts +14 -0
  95. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  96. package/dist/sandbox/cloudflare.js +131 -0
  97. package/dist/sandbox/cloudflare.js.map +1 -0
  98. package/dist/sandbox/index.d.ts +6 -4
  99. package/dist/sandbox/index.d.ts.map +1 -1
  100. package/dist/sandbox/index.js +6 -3
  101. package/dist/sandbox/index.js.map +1 -1
  102. package/dist/sandbox/types.d.ts +5 -1
  103. package/dist/sandbox/types.d.ts.map +1 -1
  104. package/dist/sandbox/types.js.map +1 -1
  105. package/dist/session-view/portal.d.ts.map +1 -1
  106. package/dist/session-view/portal.js +10 -1
  107. package/dist/session-view/portal.js.map +1 -1
  108. package/dist/session-view/service.d.ts.map +1 -1
  109. package/dist/session-view/service.js +36 -26
  110. package/dist/session-view/service.js.map +1 -1
  111. package/dist/session-view/store.d.ts +3 -2
  112. package/dist/session-view/store.d.ts.map +1 -1
  113. package/dist/session-view/store.js.map +1 -1
  114. package/dist/vault-routing.d.ts +3 -5
  115. package/dist/vault-routing.d.ts.map +1 -1
  116. package/dist/vault-routing.js +8 -20
  117. package/dist/vault-routing.js.map +1 -1
  118. package/dist/vault.d.ts +7 -5
  119. package/dist/vault.d.ts.map +1 -1
  120. package/dist/vault.js +101 -50
  121. package/dist/vault.js.map +1 -1
  122. package/package.json +1 -2
package/dist/main.js CHANGED
@@ -1,34 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import "./instrument.js";
3
3
  import { join, resolve } from "path";
4
- import { mkdirSync, readFileSync, statSync } from "fs";
4
+ import { mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
5
5
  import { homedir } from "os";
6
6
  import { fileURLToPath } from "url";
7
7
  import { dirname, join as pathJoin } from "path";
8
8
  import { DiscordBot } from "./adapters/discord/index.js";
9
9
  import { TelegramBot } from "./adapters/telegram/index.js";
10
10
  import { SlackBot as SlackBotClass } from "./adapters/slack/index.js";
11
- import { createRunner } from "./agent.js";
12
- import { createManagedSessionFile, createManagedSessionFileAtPath, getChannelSessionDir, getThreadSessionFile, resolveGenericSessionScope, } from "./session-store.js";
13
11
  import { downloadChannel } from "./download.js";
14
12
  import { createEventsWatcher } from "./events.js";
15
13
  import * as log from "./log.js";
16
14
  import { FileUserBindingStore } from "./bindings.js";
17
- import { parseLoginCommand } from "./login/index.js";
18
15
  import { startLinkServer } from "./login/portal.js";
19
16
  import { InMemoryLinkTokenStore } from "./login/session.js";
20
- import { parseSessionViewCommand } from "./session-view/command.js";
21
- import { resolveExistingSessionFile } from "./session-view/service.js";
22
17
  import { InMemorySessionViewTokenStore } from "./session-view/store.js";
23
18
  import { DockerContainerManager } from "./provisioner.js";
24
19
  import { loadAgentConfig } from "./config.js";
25
20
  import { SandboxError, parseSandboxArg, validateSandbox } from "./sandbox.js";
26
21
  import { FileVaultManager } from "./vault.js";
27
- import { createManagedVaultEntry, ensureSandboxVaultEntry, resolveActorVaultKey, } from "./vault-routing.js";
28
- import { addLifecycleBreadcrumb, applyRunScope } from "./sentry.js";
22
+ import { createSessionRuntime } from "./runtime/index.js";
29
23
  import { ChannelStore } from "./store.js";
30
- import { formatNothingRunning, formatStopped, formatStopping } from "./ui-copy.js";
31
- import { hasMaterializedSlackBranchSession, resolveSlackSessionScope, waitForSlackBranchBootstrap, } from "./adapters/slack/branch-manager.js";
32
24
  import * as Sentry from "@sentry/node";
33
25
  // ============================================================================
34
26
  // Config
@@ -53,14 +45,14 @@ function getVersion() {
53
45
  }
54
46
  return "unknown";
55
47
  }
56
- const MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;
57
- const MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;
58
- const MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;
59
- const MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;
60
- const MOM_LINK_URL = process.env.MOM_LINK_URL;
61
- const MOM_LINK_PORT = process.env.MOM_LINK_PORT
62
- ? parseInt(process.env.MOM_LINK_PORT, 10)
63
- : MOM_LINK_URL
48
+ const MAMA_SLACK_APP_TOKEN = process.env.MAMA_SLACK_APP_TOKEN;
49
+ const MAMA_SLACK_BOT_TOKEN = process.env.MAMA_SLACK_BOT_TOKEN;
50
+ const MAMA_TELEGRAM_BOT_TOKEN = process.env.MAMA_TELEGRAM_BOT_TOKEN;
51
+ const MAMA_DISCORD_BOT_TOKEN = process.env.MAMA_DISCORD_BOT_TOKEN;
52
+ const MAMA_LINK_URL = process.env.MAMA_LINK_URL;
53
+ const MAMA_LINK_PORT = process.env.MAMA_LINK_PORT
54
+ ? parseInt(process.env.MAMA_LINK_PORT, 10)
55
+ : MAMA_LINK_URL
64
56
  ? 8181
65
57
  : undefined;
66
58
  function parseArgs() {
@@ -160,16 +152,16 @@ if (parsedArgs.showVersion) {
160
152
  }
161
153
  // Handle --download mode (Slack only)
162
154
  if (parsedArgs.downloadChannel) {
163
- if (!MOM_SLACK_BOT_TOKEN) {
164
- console.error("Missing env: MOM_SLACK_BOT_TOKEN");
155
+ if (!MAMA_SLACK_BOT_TOKEN) {
156
+ console.error("Missing env: MAMA_SLACK_BOT_TOKEN");
165
157
  process.exit(1);
166
158
  }
167
- await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);
159
+ await downloadChannel(parsedArgs.downloadChannel, MAMA_SLACK_BOT_TOKEN);
168
160
  process.exit(0);
169
161
  }
170
162
  // Normal bot mode - require working dir
171
163
  if (!parsedArgs.workingDir) {
172
- console.error("Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>] <working-directory>");
164
+ console.error("Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>|cloudflare:<sandbox-id>] <working-directory>");
173
165
  console.error(" mama --download <channel-id>");
174
166
  process.exit(1);
175
167
  }
@@ -178,14 +170,14 @@ const stateDir = parsedArgs.stateDir ?? join(homedir(), ".mama");
178
170
  process.env.MAMA_STATE_DIR = stateDir;
179
171
  ensureSecureStateDir(stateDir);
180
172
  // Validate platform tokens
181
- const hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);
182
- const hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;
183
- const hasDiscord = !!MOM_DISCORD_BOT_TOKEN;
173
+ const hasSlack = !!(MAMA_SLACK_APP_TOKEN && MAMA_SLACK_BOT_TOKEN);
174
+ const hasTelegram = !!MAMA_TELEGRAM_BOT_TOKEN;
175
+ const hasDiscord = !!MAMA_DISCORD_BOT_TOKEN;
184
176
  if (!hasSlack && !hasTelegram && !hasDiscord) {
185
177
  console.error("No platform tokens found. Set one of:\n" +
186
- " Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\n" +
187
- " Telegram: MOM_TELEGRAM_BOT_TOKEN\n" +
188
- " Discord: MOM_DISCORD_BOT_TOKEN");
178
+ " Slack: MAMA_SLACK_APP_TOKEN + MAMA_SLACK_BOT_TOKEN\n" +
179
+ " Telegram: MAMA_TELEGRAM_BOT_TOKEN\n" +
180
+ " Discord: MAMA_DISCORD_BOT_TOKEN");
189
181
  process.exit(1);
190
182
  }
191
183
  try {
@@ -198,126 +190,47 @@ const vaultManager = new FileVaultManager(stateDir);
198
190
  if (vaultManager.isEnabled()) {
199
191
  console.log(sandbox.type === "container"
200
192
  ? " Vault system enabled. Container vault active."
201
- : sandbox.type === "image" || sandbox.type === "firecracker"
202
- ? " Vault system enabled. Per-user credential routing active."
193
+ : sandbox.type === "image" || sandbox.type === "firecracker" || sandbox.type === "cloudflare"
194
+ ? " Vault system enabled. Conversation-scoped credential routing active."
203
195
  : " Vault system enabled. Host mode will not inject vault env.");
204
196
  }
205
197
  const bindingStore = new FileUserBindingStore(stateDir);
206
198
  if (bindingStore.isEnabled()) {
207
199
  console.log(sandbox.type === "container"
208
200
  ? " Binding store enabled. Container mode uses the container vault."
209
- : sandbox.type === "image" || sandbox.type === "firecracker"
210
- ? " Binding store enabled. Platform user vault routing active."
201
+ : sandbox.type === "image" || sandbox.type === "firecracker" || sandbox.type === "cloudflare"
202
+ ? " Binding store enabled, but conversation-scoped sandbox routing does not use it."
211
203
  : " Binding store enabled. Host mode will not inject vault env.");
212
204
  }
213
- const startupConfig = loadAgentConfig(workingDir);
205
+ const startupConfig = loadAgentConfig();
214
206
  const sandboxLimits = startupConfig.sandboxCpus || startupConfig.sandboxMemory
215
207
  ? { cpus: startupConfig.sandboxCpus, memory: startupConfig.sandboxMemory }
216
208
  : undefined;
217
209
  const provisioner = sandbox.type === "image"
218
- ? new DockerContainerManager(sandbox.image, workingDir, { limits: sandboxLimits })
210
+ ? new DockerContainerManager(sandbox.image, { limits: sandboxLimits })
219
211
  : undefined;
212
+ if (sandbox.type === "image") {
213
+ mkdirSync(join(workingDir, "skills"), { recursive: true });
214
+ mkdirSync(join(workingDir, "events"), { recursive: true });
215
+ try {
216
+ writeFileSync(join(workingDir, "MEMORY.md"), "", { flag: "wx" });
217
+ }
218
+ catch (err) {
219
+ if (err.code !== "EEXIST")
220
+ throw err;
221
+ }
222
+ }
220
223
  const linkTokenStore = new InMemoryLinkTokenStore();
221
224
  const sessionViewTokenStore = new InMemorySessionViewTokenStore();
222
225
  setInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();
223
226
  setInterval(() => sessionViewTokenStore.purge(), 5 * 60 * 1000).unref();
224
227
  function portalBaseUrl() {
225
- if (MOM_LINK_URL)
226
- return MOM_LINK_URL.replace(/\/+$/, "");
227
- if (MOM_LINK_PORT)
228
- return `http://localhost:${MOM_LINK_PORT}`;
228
+ if (MAMA_LINK_URL)
229
+ return MAMA_LINK_URL.replace(/\/+$/, "");
230
+ if (MAMA_LINK_PORT)
231
+ return `http://localhost:${MAMA_LINK_PORT}`;
229
232
  return undefined;
230
233
  }
231
- function isPrivateConversation(event) {
232
- return event.conversationKind === "direct" || event.type === "dm";
233
- }
234
- function ensureLoginVault(platform, platformUserId) {
235
- const vaultId = resolveActorVaultKey(sandbox, vaultManager, bindingStore, platform, platformUserId);
236
- ensureSandboxVaultEntry(sandbox, vaultManager, platform, platformUserId, vaultId);
237
- if (sandbox.type !== "container" && sandbox.type !== "image") {
238
- vaultManager.addEntry(vaultId, createManagedVaultEntry(platform, platformUserId, vaultId));
239
- }
240
- return vaultId;
241
- }
242
- async function replyWithContext(responseCtx, text) {
243
- await responseCtx.setTyping(false);
244
- await responseCtx.setWorking(false);
245
- await responseCtx.respond(text);
246
- }
247
- async function handleLoginCommand(platform, platformUserId, conversationId, responseCtx, commandText, privateConversation) {
248
- const parsed = parseLoginCommand(commandText);
249
- if (!parsed)
250
- return false;
251
- if (!privateConversation) {
252
- await replyWithContext(responseCtx, "為了保護你的憑證,`/login` 只能在與機器人的私訊中使用。請先私訊機器人,再重新執行 `/login`。");
253
- return true;
254
- }
255
- const baseUrl = portalBaseUrl();
256
- if (!baseUrl) {
257
- await replyWithContext(responseCtx, "Login is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.");
258
- return true;
259
- }
260
- let vaultId;
261
- try {
262
- vaultId = ensureLoginVault(platform, platformUserId);
263
- }
264
- catch (error) {
265
- log.logWarning(`[${conversationId}] Failed to prepare login vault for ${platform}/${platformUserId}`, error instanceof Error ? error.message : String(error));
266
- await replyWithContext(responseCtx, "Login setup failed on the server. 請稍後重試,或聯絡管理員檢查 vault 儲存權限。");
267
- return true;
268
- }
269
- const token = linkTokenStore.create(platform, platformUserId, conversationId, vaultId, "");
270
- const vaultLabel = sandbox.type === "container" ? `container vault (${vaultId})` : "your vault";
271
- await replyWithContext(responseCtx, `Open this link to store credentials in ${vaultLabel} (expires in 15 minutes):\n${baseUrl}/link?token=${token.token}`);
272
- return true;
273
- }
274
- async function handleSessionViewCommand(platform, platformUserId, conversationId, sessionKey, bot, responseCtx, commandText, privateConversation) {
275
- if (!parseSessionViewCommand(commandText))
276
- return false;
277
- const allowSharedPrivateDelivery = platform === "slack" || platform === "discord";
278
- const sendSessionViewReply = async (text) => {
279
- if (privateConversation) {
280
- await replyWithContext(responseCtx, text);
281
- return;
282
- }
283
- if (platform === "slack" && bot instanceof SlackBotClass) {
284
- await bot.postEphemeral(conversationId, platformUserId, text);
285
- return;
286
- }
287
- if (platform === "discord" && bot instanceof DiscordBot) {
288
- await bot.sendDirectMessage(platformUserId, text);
289
- return;
290
- }
291
- await replyWithContext(responseCtx, text);
292
- };
293
- if (!privateConversation && !allowSharedPrivateDelivery) {
294
- await sendSessionViewReply("為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。");
295
- return true;
296
- }
297
- const baseUrl = portalBaseUrl();
298
- if (!baseUrl) {
299
- await sendSessionViewReply("Session viewer is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.");
300
- return true;
301
- }
302
- const sessionFile = resolveExistingSessionFile(workingDir, conversationId, sessionKey);
303
- if (!sessionFile) {
304
- await sendSessionViewReply("目前還沒有可查看的 session。先和機器人對話一次,建立 session 後再試。");
305
- return true;
306
- }
307
- const token = sessionViewTokenStore.create(platform, platformUserId, conversationId, sessionKey, sessionFile);
308
- const linkText = `Open this read-only session link (expires in 24 hours):\n${baseUrl}/session?token=${token.token}`;
309
- await sendSessionViewReply(linkText);
310
- return true;
311
- }
312
- const conversationStates = new Map();
313
- /** Track in-flight runs for graceful shutdown */
314
- const inFlightRuns = new Set();
315
- /** Flag to stop accepting new events during shutdown */
316
- let isShuttingDown = false;
317
- /** Maximum number of cached sessions */
318
- const MAX_SESSIONS = 500;
319
- /** Idle timeout before a non-running session can be evicted (1 hour) */
320
- const IDLE_TIMEOUT_MS = 3600000;
321
234
  /** Idle timeout for managed image containers (10 minutes) */
322
235
  const IMAGE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
323
236
  if (provisioner) {
@@ -325,245 +238,16 @@ if (provisioner) {
325
238
  await provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS);
326
239
  setInterval(() => provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS), IMAGE_IDLE_TIMEOUT_MS).unref();
327
240
  }
328
- async function resolveSessionScope(platformName, conversationDir, sessionKey) {
329
- if (platformName === "slack") {
330
- return resolveSlackSessionScope({ conversationDir, sessionKey });
331
- }
332
- return resolveGenericSessionScope({ conversationDir, sessionKey });
333
- }
334
- async function getState(conversationId, platformName, sessionKey) {
335
- const key = sessionKey ?? conversationId;
336
- let state = conversationStates.get(key);
337
- if (!state) {
338
- const conversationDir = join(workingDir, conversationId);
339
- const sessionScope = await resolveSessionScope(platformName, conversationDir, key);
340
- state = {
341
- running: false,
342
- runner: await createRunner(sandbox, key, conversationId, conversationDir, workingDir, sessionScope, vaultManager, bindingStore, provisioner),
343
- stopRequested: false,
344
- lastAccessedAt: Date.now(),
345
- };
346
- conversationStates.set(key, state);
347
- }
348
- else {
349
- state.lastAccessedAt = Date.now();
350
- }
351
- return state;
352
- }
353
- /**
354
- * Evict idle sessions from conversationStates to bound memory usage.
355
- * Called after each handleEvent completes.
356
- */
357
- function evictIdleSessions() {
358
- const now = Date.now();
359
- for (const [key, state] of conversationStates) {
360
- if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {
361
- conversationStates.delete(key);
362
- }
363
- }
364
- if (conversationStates.size > MAX_SESSIONS) {
365
- const idleSessions = [];
366
- for (const [key, state] of conversationStates) {
367
- if (!state.running) {
368
- idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });
369
- }
370
- }
371
- idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
372
- const toEvict = conversationStates.size - MAX_SESSIONS;
373
- for (let i = 0; i < toEvict && i < idleSessions.length; i++) {
374
- conversationStates.delete(idleSessions[i].key);
375
- }
376
- }
377
- }
378
- // ============================================================================
379
- // Handler
380
- // ============================================================================
381
- const handler = {
382
- isRunning(sessionKey) {
383
- const state = conversationStates.get(sessionKey);
384
- return !!state?.running;
385
- },
386
- getRunningSessions() {
387
- const sessions = [];
388
- for (const [sessionKey, state] of conversationStates) {
389
- if (state.running && state.startedAt) {
390
- // Get current step from runner
391
- const currentStep = state.runner.getCurrentStep();
392
- sessions.push({
393
- sessionKey,
394
- startedAt: state.startedAt,
395
- lastActivityAt: state.lastActivityAt,
396
- currentTool: currentStep?.label || currentStep?.toolName,
397
- });
398
- }
399
- }
400
- return sessions;
401
- },
402
- async handleStop(sessionKey, conversationId, bot) {
403
- const state = conversationStates.get(sessionKey);
404
- if (state?.running) {
405
- state.stopRequested = true;
406
- state.runner.abort();
407
- const ts = await bot.postMessage(conversationId, formatStopping(bot));
408
- state.stopMessageTs = ts;
409
- }
410
- else {
411
- await bot.postMessage(conversationId, formatNothingRunning(bot));
412
- }
413
- },
414
- forceStop(sessionKey) {
415
- const state = conversationStates.get(sessionKey);
416
- if (state?.running) {
417
- log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);
418
- state.stopRequested = true;
419
- state.runner.abort();
420
- state.running = false;
421
- }
422
- },
423
- async handleNew(sessionKey, conversationId, bot) {
424
- const state = conversationStates.get(sessionKey);
425
- if (state?.running) {
426
- state.stopRequested = true;
427
- state.runner.abort();
428
- }
429
- // Conversation sessions rotate via current pointer. Thread sessions reset in place.
430
- const conversationDir = join(workingDir, conversationId);
431
- if (sessionKey.includes(":")) {
432
- createManagedSessionFileAtPath(getThreadSessionFile(conversationDir, sessionKey), conversationDir);
433
- }
434
- else {
435
- createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);
436
- }
437
- // Remove from in-memory cache
438
- conversationStates.delete(sessionKey);
439
- log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);
440
- await bot.postMessage(conversationId, "Conversation reset. Send a new message to start fresh.");
441
- },
442
- async handleEvent(event, bot, adapters, _isEvent) {
443
- const conversationId = event.conversationId;
444
- // Don't accept new events during shutdown
445
- if (isShuttingDown) {
446
- log.logInfo(`[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);
447
- return;
448
- }
449
- const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;
450
- const privateConversation = isPrivateConversation(event);
451
- const handledLogin = await handleLoginCommand(adapters.platform.name, event.user, conversationId, adapters.responseCtx, event.text, privateConversation);
452
- if (handledLogin)
453
- return;
454
- const handledSessionView = await handleSessionViewCommand(adapters.platform.name, event.user, conversationId, sessionKey, bot, adapters.responseCtx, event.text, privateConversation);
455
- if (handledSessionView)
456
- return;
457
- const conversationDir = join(workingDir, conversationId);
458
- const waitedForParent = adapters.platform.name === "slack"
459
- ? await waitForSlackBranchBootstrap({
460
- parentSessionKey: conversationId,
461
- sessionKey,
462
- hasThreadSession: () => hasMaterializedSlackBranchSession(conversationDir, sessionKey),
463
- isParentRunning: () => conversationStates.get(conversationId)?.running === true,
464
- })
465
- : false;
466
- if (waitedForParent) {
467
- log.logInfo(`[${conversationId}] Delayed thread bootstrap until parent session sealed: ${sessionKey}`);
468
- }
469
- const state = await getState(conversationId, adapters.platform.name, sessionKey);
470
- // Start run
471
- state.running = true;
472
- state.stopRequested = false;
473
- state.startedAt = Date.now();
474
- state.lastActivityAt = Date.now();
475
- log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);
476
- // Wrap in-flight run tracking
477
- Sentry.metrics.count("agent.run.started", 1, {
478
- attributes: { channel: conversationId },
479
- });
480
- Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size + 1);
481
- const runPromise = Sentry.startSpan({ name: "agent.run", op: "agent", attributes: { conversationId, sessionKey } }, async () => {
482
- return Sentry.withScope(async (scope) => {
483
- const { message, responseCtx, platform } = adapters;
484
- applyRunScope(scope, {
485
- conversationId,
486
- sessionKey,
487
- messageId: message.id,
488
- platform: platform.name,
489
- userId: message.userId,
490
- userName: message.userName,
491
- threadTs: message.threadTs,
492
- isEvent: _isEvent,
493
- });
494
- addLifecycleBreadcrumb("agent.run.started", {
495
- channel_id: conversationId,
496
- platform: platform.name,
497
- has_attachments: (message.attachments?.length ?? 0) > 0,
498
- });
499
- try {
500
- await responseCtx.setTyping(true);
501
- await responseCtx.setWorking(true);
502
- const result = await state.runner.run(message, responseCtx, platform);
503
- await responseCtx.setWorking(false);
504
- const durationMs = Date.now() - state.startedAt;
505
- Sentry.metrics.distribution("agent.run.duration", durationMs, {
506
- unit: "millisecond",
507
- attributes: {
508
- channel: conversationId,
509
- platform: platform.name,
510
- stop_reason: result.stopReason,
511
- },
512
- });
513
- Sentry.metrics.count("agent.run.completed", 1, {
514
- attributes: {
515
- channel: conversationId,
516
- platform: platform.name,
517
- stop_reason: result.stopReason,
518
- },
519
- });
520
- addLifecycleBreadcrumb("agent.run.completed", {
521
- channel_id: conversationId,
522
- platform: platform.name,
523
- stop_reason: result.stopReason,
524
- duration_ms: durationMs,
525
- });
526
- if (result.stopReason === "aborted" && state.stopRequested) {
527
- if (state.stopMessageTs) {
528
- await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));
529
- state.stopMessageTs = undefined;
530
- }
531
- else {
532
- await bot.postMessage(conversationId, formatStopped(bot));
533
- }
534
- }
535
- }
536
- catch (err) {
537
- scope.setContext("agent_run_error", {
538
- conversationId,
539
- sessionKey,
540
- platform: adapters.platform.name,
541
- messageId: adapters.message.id,
542
- threadTs: adapters.message.threadTs,
543
- });
544
- Sentry.captureException(err);
545
- Sentry.metrics.count("agent.run.errors", 1, {
546
- attributes: { channel: conversationId, platform: adapters.platform.name },
547
- });
548
- log.logWarning(`[${conversationId}] Run error`, err instanceof Error ? err.message : String(err));
549
- }
550
- finally {
551
- state.running = false;
552
- state.lastAccessedAt = Date.now();
553
- Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size - 1);
554
- evictIdleSessions();
555
- }
556
- });
557
- });
558
- inFlightRuns.add(runPromise);
559
- try {
560
- await runPromise;
561
- }
562
- finally {
563
- inFlightRuns.delete(runPromise);
564
- }
565
- },
566
- };
241
+ const handler = createSessionRuntime({
242
+ workingDir,
243
+ sandbox,
244
+ vaultManager,
245
+ bindingStore,
246
+ provisioner,
247
+ linkTokenStore,
248
+ sessionViewTokenStore,
249
+ portalBaseUrl: portalBaseUrl(),
250
+ });
567
251
  // ============================================================================
568
252
  // Start
569
253
  // ============================================================================
@@ -573,16 +257,18 @@ const sandboxDesc = sandbox.type === "host"
573
257
  ? `container:${sandbox.container}`
574
258
  : sandbox.type === "image"
575
259
  ? `image:${sandbox.image}`
576
- : `firecracker:${sandbox.vmId}`;
260
+ : sandbox.type === "firecracker"
261
+ ? `firecracker:${sandbox.vmId}`
262
+ : `cloudflare:${sandbox.sandboxId}`;
577
263
  log.logStartup(workingDir, sandboxDesc);
578
264
  // Create platform bots
579
265
  const bots = [];
580
266
  const botsByPlatform = {};
581
267
  if (hasSlack) {
582
- const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN });
268
+ const sharedStore = new ChannelStore({ workingDir, botToken: MAMA_SLACK_BOT_TOKEN });
583
269
  const slackBot = new SlackBotClass(handler, {
584
- appToken: MOM_SLACK_APP_TOKEN,
585
- botToken: MOM_SLACK_BOT_TOKEN,
270
+ appToken: MAMA_SLACK_APP_TOKEN,
271
+ botToken: MAMA_SLACK_BOT_TOKEN,
586
272
  workingDir,
587
273
  store: sharedStore,
588
274
  });
@@ -592,7 +278,7 @@ if (hasSlack) {
592
278
  }
593
279
  if (hasTelegram) {
594
280
  const telegramBot = new TelegramBot(handler, {
595
- token: MOM_TELEGRAM_BOT_TOKEN,
281
+ token: MAMA_TELEGRAM_BOT_TOKEN,
596
282
  workingDir,
597
283
  });
598
284
  bots.push(telegramBot);
@@ -601,15 +287,15 @@ if (hasTelegram) {
601
287
  }
602
288
  if (hasDiscord) {
603
289
  const discordBot = new DiscordBot(handler, {
604
- token: MOM_DISCORD_BOT_TOKEN,
290
+ token: MAMA_DISCORD_BOT_TOKEN,
605
291
  workingDir,
606
292
  });
607
293
  bots.push(discordBot);
608
294
  botsByPlatform.discord = discordBot;
609
295
  log.logInfo("Platform: Discord");
610
296
  }
611
- if (MOM_LINK_PORT) {
612
- startLinkServer(MOM_LINK_PORT, linkTokenStore, vaultManager, async (platform, conversationId, message) => {
297
+ if (MAMA_LINK_PORT) {
298
+ startLinkServer(MAMA_LINK_PORT, linkTokenStore, vaultManager, async (platform, conversationId, message) => {
613
299
  const bot = botsByPlatform[platform];
614
300
  if (bot)
615
301
  await bot.postMessage(conversationId, message);
@@ -624,17 +310,7 @@ if (slackBot) {
624
310
  eventsWatcher.start();
625
311
  // Handle shutdown
626
312
  async function shutdown() {
627
- if (isShuttingDown)
628
- return;
629
- isShuttingDown = true;
630
- log.logInfo("Shutting down gracefully...");
631
- const timeout = Date.now() + 30000;
632
- while (inFlightRuns.size > 0 && Date.now() < timeout) {
633
- await new Promise((resolve) => setTimeout(resolve, 500));
634
- }
635
- if (inFlightRuns.size > 0) {
636
- log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);
637
- }
313
+ await handler.shutdown();
638
314
  eventsWatcher.stop();
639
315
  await Sentry.close(5000);
640
316
  process.exit(0);