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

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 (133) hide show
  1. package/README.md +151 -417
  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 +2 -1
  6. package/dist/adapters/discord/bot.d.ts.map +1 -1
  7. package/dist/adapters/discord/bot.js +71 -18
  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 +5 -0
  13. package/dist/adapters/slack/bot.d.ts.map +1 -1
  14. package/dist/adapters/slack/bot.js +69 -11
  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/bot.d.ts.map +1 -1
  20. package/dist/adapters/telegram/bot.js +1 -35
  21. package/dist/adapters/telegram/bot.js.map +1 -1
  22. package/dist/adapters/telegram/context.d.ts.map +1 -1
  23. package/dist/adapters/telegram/context.js +9 -2
  24. package/dist/adapters/telegram/context.js.map +1 -1
  25. package/dist/agent.d.ts.map +1 -1
  26. package/dist/agent.js +15 -7
  27. package/dist/agent.js.map +1 -1
  28. package/dist/bindings.d.ts +2 -1
  29. package/dist/bindings.d.ts.map +1 -1
  30. package/dist/bindings.js.map +1 -1
  31. package/dist/commands/index.d.ts +5 -0
  32. package/dist/commands/index.d.ts.map +1 -0
  33. package/dist/commands/index.js +15 -0
  34. package/dist/commands/index.js.map +1 -0
  35. package/dist/commands/login.d.ts +5 -0
  36. package/dist/commands/login.d.ts.map +1 -0
  37. package/dist/commands/login.js +37 -0
  38. package/dist/commands/login.js.map +1 -0
  39. package/dist/commands/model.d.ts +14 -0
  40. package/dist/commands/model.d.ts.map +1 -0
  41. package/dist/commands/model.js +94 -0
  42. package/dist/commands/model.js.map +1 -0
  43. package/dist/commands/new.d.ts +9 -0
  44. package/dist/commands/new.d.ts.map +1 -0
  45. package/dist/commands/new.js +28 -0
  46. package/dist/commands/new.js.map +1 -0
  47. package/dist/commands/registry.d.ts +7 -0
  48. package/dist/commands/registry.d.ts.map +1 -0
  49. package/dist/commands/registry.js +14 -0
  50. package/dist/commands/registry.js.map +1 -0
  51. package/dist/commands/session-view.d.ts +5 -0
  52. package/dist/commands/session-view.d.ts.map +1 -0
  53. package/dist/commands/session-view.js +38 -0
  54. package/dist/commands/session-view.js.map +1 -0
  55. package/dist/commands/types.d.ts +43 -0
  56. package/dist/commands/types.d.ts.map +1 -0
  57. package/dist/commands/types.js +2 -0
  58. package/dist/commands/types.js.map +1 -0
  59. package/dist/commands/utils.d.ts +5 -0
  60. package/dist/commands/utils.d.ts.map +1 -0
  61. package/dist/commands/utils.js +9 -0
  62. package/dist/commands/utils.js.map +1 -0
  63. package/dist/config.d.ts +15 -8
  64. package/dist/config.d.ts.map +1 -1
  65. package/dist/config.js +167 -56
  66. package/dist/config.js.map +1 -1
  67. package/dist/context.d.ts.map +1 -1
  68. package/dist/context.js +74 -68
  69. package/dist/context.js.map +1 -1
  70. package/dist/execution-resolver.d.ts +6 -3
  71. package/dist/execution-resolver.d.ts.map +1 -1
  72. package/dist/execution-resolver.js +47 -14
  73. package/dist/execution-resolver.js.map +1 -1
  74. package/dist/index.d.ts +7 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +4 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/instrument.d.ts.map +1 -1
  79. package/dist/instrument.js +2 -3
  80. package/dist/instrument.js.map +1 -1
  81. package/dist/login/index.d.ts.map +1 -1
  82. package/dist/login/index.js +19 -8
  83. package/dist/login/index.js.map +1 -1
  84. package/dist/login/portal.d.ts.map +1 -1
  85. package/dist/login/portal.js +7 -7
  86. package/dist/login/portal.js.map +1 -1
  87. package/dist/login/session.d.ts +3 -2
  88. package/dist/login/session.d.ts.map +1 -1
  89. package/dist/login/session.js.map +1 -1
  90. package/dist/main.d.ts.map +1 -1
  91. package/dist/main.js +107 -388
  92. package/dist/main.js.map +1 -1
  93. package/dist/provisioner.d.ts +11 -9
  94. package/dist/provisioner.d.ts.map +1 -1
  95. package/dist/provisioner.js +125 -87
  96. package/dist/provisioner.js.map +1 -1
  97. package/dist/runtime/index.d.ts +2 -0
  98. package/dist/runtime/index.d.ts.map +1 -0
  99. package/dist/runtime/index.js +2 -0
  100. package/dist/runtime/index.js.map +1 -0
  101. package/dist/runtime/session-runtime.d.ts +27 -0
  102. package/dist/runtime/session-runtime.d.ts.map +1 -0
  103. package/dist/runtime/session-runtime.js +303 -0
  104. package/dist/runtime/session-runtime.js.map +1 -0
  105. package/dist/sandbox/cloudflare.d.ts +14 -0
  106. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  107. package/dist/sandbox/cloudflare.js +131 -0
  108. package/dist/sandbox/cloudflare.js.map +1 -0
  109. package/dist/sandbox/index.d.ts +6 -4
  110. package/dist/sandbox/index.d.ts.map +1 -1
  111. package/dist/sandbox/index.js +6 -3
  112. package/dist/sandbox/index.js.map +1 -1
  113. package/dist/sandbox/types.d.ts +5 -1
  114. package/dist/sandbox/types.d.ts.map +1 -1
  115. package/dist/sandbox/types.js.map +1 -1
  116. package/dist/session-view/portal.d.ts.map +1 -1
  117. package/dist/session-view/portal.js +10 -1
  118. package/dist/session-view/portal.js.map +1 -1
  119. package/dist/session-view/service.d.ts.map +1 -1
  120. package/dist/session-view/service.js +36 -26
  121. package/dist/session-view/service.js.map +1 -1
  122. package/dist/session-view/store.d.ts +3 -2
  123. package/dist/session-view/store.d.ts.map +1 -1
  124. package/dist/session-view/store.js.map +1 -1
  125. package/dist/vault-routing.d.ts +3 -5
  126. package/dist/vault-routing.d.ts.map +1 -1
  127. package/dist/vault-routing.js +8 -20
  128. package/dist/vault-routing.js.map +1 -1
  129. package/dist/vault.d.ts +7 -5
  130. package/dist/vault.d.ts.map +1 -1
  131. package/dist/vault.js +101 -50
  132. package/dist/vault.js.map +1 -1
  133. 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
- import { loadAgentConfig } from "./config.js";
19
+ import { createGlobalSettingsFile, loadAgentConfig, MissingGlobalSettingsError } 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() {
@@ -69,12 +61,16 @@ function parseArgs() {
69
61
  let workingDir;
70
62
  let stateDirArg;
71
63
  let downloadChannelId;
64
+ let showOnboard = false;
72
65
  let showVersion = false;
73
66
  for (let i = 0; i < args.length; i++) {
74
67
  const arg = args[i];
75
68
  if (arg === "--version" || arg === "-v" || arg === "-V") {
76
69
  showVersion = true;
77
70
  }
71
+ else if (arg === "--onboard") {
72
+ showOnboard = true;
73
+ }
78
74
  else if (arg.startsWith("--sandbox=")) {
79
75
  sandbox = parseSandboxArg(arg.slice("--sandbox=".length));
80
76
  }
@@ -102,6 +98,7 @@ function parseArgs() {
102
98
  stateDir: stateDirArg ? resolve(stateDirArg) : undefined,
103
99
  sandbox,
104
100
  downloadChannel: downloadChannelId,
101
+ showOnboard,
105
102
  showVersion,
106
103
  };
107
104
  }
@@ -144,7 +141,21 @@ function handleStartupError(error) {
144
141
  }
145
142
  process.exit(1);
146
143
  }
147
- throw error;
144
+ if (error instanceof MissingGlobalSettingsError) {
145
+ console.error(`Missing global settings: ${error.settingsPath}`);
146
+ console.error("");
147
+ console.error("Run onboarding to create it:");
148
+ console.error(` mama --onboard --state-dir ${stateDir}`);
149
+ console.error("");
150
+ console.error("Then review the generated settings.json and start mama again.");
151
+ process.exit(1);
152
+ }
153
+ if (error instanceof Error) {
154
+ console.error(`Error: ${error.message}`);
155
+ process.exit(1);
156
+ }
157
+ console.error(String(error));
158
+ process.exit(1);
148
159
  }
149
160
  let parsedArgs;
150
161
  try {
@@ -158,18 +169,35 @@ if (parsedArgs.showVersion) {
158
169
  console.log(getVersion());
159
170
  process.exit(0);
160
171
  }
172
+ // Handle --onboard mode
173
+ if (parsedArgs.showOnboard) {
174
+ const stateDir = parsedArgs.stateDir ?? join(homedir(), ".mama");
175
+ process.env.MAMA_STATE_DIR = stateDir;
176
+ ensureSecureStateDir(stateDir);
177
+ try {
178
+ const settingsPath = createGlobalSettingsFile(stateDir);
179
+ console.log(`Created global settings at ${settingsPath}`);
180
+ console.log("Review the file, then start mama with your working directory.");
181
+ process.exit(0);
182
+ }
183
+ catch (err) {
184
+ console.error(err instanceof Error ? err.message : String(err));
185
+ process.exit(1);
186
+ }
187
+ }
161
188
  // Handle --download mode (Slack only)
162
189
  if (parsedArgs.downloadChannel) {
163
- if (!MOM_SLACK_BOT_TOKEN) {
164
- console.error("Missing env: MOM_SLACK_BOT_TOKEN");
190
+ if (!MAMA_SLACK_BOT_TOKEN) {
191
+ console.error("Missing env: MAMA_SLACK_BOT_TOKEN");
165
192
  process.exit(1);
166
193
  }
167
- await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);
194
+ await downloadChannel(parsedArgs.downloadChannel, MAMA_SLACK_BOT_TOKEN);
168
195
  process.exit(0);
169
196
  }
170
197
  // Normal bot mode - require working dir
171
198
  if (!parsedArgs.workingDir) {
172
- console.error("Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>] <working-directory>");
199
+ console.error("Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>|cloudflare:<sandbox-id>] <working-directory>");
200
+ console.error(" mama --onboard [--state-dir=<dir>]");
173
201
  console.error(" mama --download <channel-id>");
174
202
  process.exit(1);
175
203
  }
@@ -178,14 +206,14 @@ const stateDir = parsedArgs.stateDir ?? join(homedir(), ".mama");
178
206
  process.env.MAMA_STATE_DIR = stateDir;
179
207
  ensureSecureStateDir(stateDir);
180
208
  // 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;
209
+ const hasSlack = !!(MAMA_SLACK_APP_TOKEN && MAMA_SLACK_BOT_TOKEN);
210
+ const hasTelegram = !!MAMA_TELEGRAM_BOT_TOKEN;
211
+ const hasDiscord = !!MAMA_DISCORD_BOT_TOKEN;
184
212
  if (!hasSlack && !hasTelegram && !hasDiscord) {
185
213
  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");
214
+ " Slack: MAMA_SLACK_APP_TOKEN + MAMA_SLACK_BOT_TOKEN\n" +
215
+ " Telegram: MAMA_TELEGRAM_BOT_TOKEN\n" +
216
+ " Discord: MAMA_DISCORD_BOT_TOKEN");
189
217
  process.exit(1);
190
218
  }
191
219
  try {
@@ -198,126 +226,54 @@ const vaultManager = new FileVaultManager(stateDir);
198
226
  if (vaultManager.isEnabled()) {
199
227
  console.log(sandbox.type === "container"
200
228
  ? " Vault system enabled. Container vault active."
201
- : sandbox.type === "image" || sandbox.type === "firecracker"
202
- ? " Vault system enabled. Per-user credential routing active."
229
+ : sandbox.type === "image" || sandbox.type === "firecracker" || sandbox.type === "cloudflare"
230
+ ? " Vault system enabled. Conversation-scoped credential routing active."
203
231
  : " Vault system enabled. Host mode will not inject vault env.");
204
232
  }
205
233
  const bindingStore = new FileUserBindingStore(stateDir);
206
234
  if (bindingStore.isEnabled()) {
207
235
  console.log(sandbox.type === "container"
208
236
  ? " 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."
237
+ : sandbox.type === "image" || sandbox.type === "firecracker" || sandbox.type === "cloudflare"
238
+ ? " Binding store enabled, but conversation-scoped sandbox routing does not use it."
211
239
  : " Binding store enabled. Host mode will not inject vault env.");
212
240
  }
213
- const startupConfig = loadAgentConfig(workingDir);
241
+ const startupConfig = (() => {
242
+ try {
243
+ return loadAgentConfig();
244
+ }
245
+ catch (error) {
246
+ handleStartupError(error);
247
+ }
248
+ })();
214
249
  const sandboxLimits = startupConfig.sandboxCpus || startupConfig.sandboxMemory
215
250
  ? { cpus: startupConfig.sandboxCpus, memory: startupConfig.sandboxMemory }
216
251
  : undefined;
217
252
  const provisioner = sandbox.type === "image"
218
- ? new DockerContainerManager(sandbox.image, workingDir, { limits: sandboxLimits })
253
+ ? new DockerContainerManager(sandbox.image, { limits: sandboxLimits })
219
254
  : undefined;
255
+ if (sandbox.type === "image") {
256
+ mkdirSync(join(workingDir, "skills"), { recursive: true });
257
+ mkdirSync(join(workingDir, "events"), { recursive: true });
258
+ try {
259
+ writeFileSync(join(workingDir, "MEMORY.md"), "", { flag: "wx" });
260
+ }
261
+ catch (err) {
262
+ if (err.code !== "EEXIST")
263
+ throw err;
264
+ }
265
+ }
220
266
  const linkTokenStore = new InMemoryLinkTokenStore();
221
267
  const sessionViewTokenStore = new InMemorySessionViewTokenStore();
222
268
  setInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();
223
269
  setInterval(() => sessionViewTokenStore.purge(), 5 * 60 * 1000).unref();
224
270
  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}`;
271
+ if (MAMA_LINK_URL)
272
+ return MAMA_LINK_URL.replace(/\/+$/, "");
273
+ if (MAMA_LINK_PORT)
274
+ return `http://localhost:${MAMA_LINK_PORT}`;
229
275
  return undefined;
230
276
  }
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
277
  /** Idle timeout for managed image containers (10 minutes) */
322
278
  const IMAGE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
323
279
  if (provisioner) {
@@ -325,245 +281,16 @@ if (provisioner) {
325
281
  await provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS);
326
282
  setInterval(() => provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS), IMAGE_IDLE_TIMEOUT_MS).unref();
327
283
  }
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
- };
284
+ const handler = createSessionRuntime({
285
+ workingDir,
286
+ sandbox,
287
+ vaultManager,
288
+ bindingStore,
289
+ provisioner,
290
+ linkTokenStore,
291
+ sessionViewTokenStore,
292
+ portalBaseUrl: portalBaseUrl(),
293
+ });
567
294
  // ============================================================================
568
295
  // Start
569
296
  // ============================================================================
@@ -573,16 +300,18 @@ const sandboxDesc = sandbox.type === "host"
573
300
  ? `container:${sandbox.container}`
574
301
  : sandbox.type === "image"
575
302
  ? `image:${sandbox.image}`
576
- : `firecracker:${sandbox.vmId}`;
303
+ : sandbox.type === "firecracker"
304
+ ? `firecracker:${sandbox.vmId}`
305
+ : `cloudflare:${sandbox.sandboxId}`;
577
306
  log.logStartup(workingDir, sandboxDesc);
578
307
  // Create platform bots
579
308
  const bots = [];
580
309
  const botsByPlatform = {};
581
310
  if (hasSlack) {
582
- const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN });
311
+ const sharedStore = new ChannelStore({ workingDir, botToken: MAMA_SLACK_BOT_TOKEN });
583
312
  const slackBot = new SlackBotClass(handler, {
584
- appToken: MOM_SLACK_APP_TOKEN,
585
- botToken: MOM_SLACK_BOT_TOKEN,
313
+ appToken: MAMA_SLACK_APP_TOKEN,
314
+ botToken: MAMA_SLACK_BOT_TOKEN,
586
315
  workingDir,
587
316
  store: sharedStore,
588
317
  });
@@ -592,7 +321,7 @@ if (hasSlack) {
592
321
  }
593
322
  if (hasTelegram) {
594
323
  const telegramBot = new TelegramBot(handler, {
595
- token: MOM_TELEGRAM_BOT_TOKEN,
324
+ token: MAMA_TELEGRAM_BOT_TOKEN,
596
325
  workingDir,
597
326
  });
598
327
  bots.push(telegramBot);
@@ -601,15 +330,15 @@ if (hasTelegram) {
601
330
  }
602
331
  if (hasDiscord) {
603
332
  const discordBot = new DiscordBot(handler, {
604
- token: MOM_DISCORD_BOT_TOKEN,
333
+ token: MAMA_DISCORD_BOT_TOKEN,
605
334
  workingDir,
606
335
  });
607
336
  bots.push(discordBot);
608
337
  botsByPlatform.discord = discordBot;
609
338
  log.logInfo("Platform: Discord");
610
339
  }
611
- if (MOM_LINK_PORT) {
612
- startLinkServer(MOM_LINK_PORT, linkTokenStore, vaultManager, async (platform, conversationId, message) => {
340
+ if (MAMA_LINK_PORT) {
341
+ startLinkServer(MAMA_LINK_PORT, linkTokenStore, vaultManager, async (platform, conversationId, message) => {
613
342
  const bot = botsByPlatform[platform];
614
343
  if (bot)
615
344
  await bot.postMessage(conversationId, message);
@@ -624,17 +353,7 @@ if (slackBot) {
624
353
  eventsWatcher.start();
625
354
  // Handle shutdown
626
355
  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
- }
356
+ await handler.shutdown();
638
357
  eventsWatcher.stop();
639
358
  await Sentry.close(5000);
640
359
  process.exit(0);