@geminixiang/mama 0.2.0-beta.3 → 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.
- package/README.md +101 -422
- package/dist/adapter.d.ts +9 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +1 -0
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +62 -73
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +9 -2
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/shared.d.ts +48 -0
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/shared.js +111 -0
- package/dist/adapters/shared.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +3 -19
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +58 -188
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +13 -3
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +78 -100
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +9 -2
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -5
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +2 -1
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +3 -2
- package/dist/bindings.js.map +1 -1
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +8 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +37 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/registry.d.ts +7 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +14 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/session-view.d.ts +5 -0
- package/dist/commands/session-view.d.ts.map +1 -0
- package/dist/commands/session-view.js +38 -0
- package/dist/commands/session-view.js.map +1 -0
- package/dist/commands/types.d.ts +41 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/utils.d.ts +5 -0
- package/dist/commands/utils.d.ts.map +1 -0
- package/dist/commands/utils.js +9 -0
- package/dist/commands/utils.js.map +1 -0
- package/dist/config.d.ts +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +37 -42
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +74 -68
- package/dist/context.js.map +1 -1
- package/dist/execution-resolver.d.ts +6 -3
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +47 -14
- package/dist/execution-resolver.js.map +1 -1
- package/dist/fs-atomic.d.ts +10 -0
- package/dist/fs-atomic.d.ts.map +1 -0
- package/dist/fs-atomic.js +45 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +2 -3
- package/dist/instrument.js.map +1 -1
- package/dist/login/index.d.ts.map +1 -1
- package/dist/login/index.js +19 -8
- package/dist/login/index.js.map +1 -1
- package/dist/login/portal.d.ts.map +1 -1
- package/dist/login/portal.js +7 -7
- package/dist/login/portal.js.map +1 -1
- package/dist/login/session.d.ts +3 -2
- package/dist/login/session.d.ts.map +1 -1
- package/dist/login/session.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +63 -389
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +11 -9
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +125 -87
- package/dist/provisioner.js.map +1 -1
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/session-runtime.d.ts +26 -0
- package/dist/runtime/session-runtime.d.ts.map +1 -0
- package/dist/runtime/session-runtime.js +285 -0
- package/dist/runtime/session-runtime.js.map +1 -0
- package/dist/sandbox/cloudflare.d.ts +14 -0
- package/dist/sandbox/cloudflare.d.ts.map +1 -0
- package/dist/sandbox/cloudflare.js +131 -0
- package/dist/sandbox/cloudflare.js.map +1 -0
- package/dist/sandbox/index.d.ts +6 -4
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +6 -3
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/types.d.ts +5 -1
- package/dist/sandbox/types.d.ts.map +1 -1
- package/dist/sandbox/types.js.map +1 -1
- package/dist/session-store.d.ts +5 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +14 -9
- package/dist/session-store.js.map +1 -1
- package/dist/session-view/portal.d.ts +2 -0
- package/dist/session-view/portal.d.ts.map +1 -1
- package/dist/session-view/portal.js +45 -7
- package/dist/session-view/portal.js.map +1 -1
- package/dist/session-view/service.d.ts.map +1 -1
- package/dist/session-view/service.js +94 -48
- package/dist/session-view/service.js.map +1 -1
- package/dist/session-view/store.d.ts +3 -2
- package/dist/session-view/store.d.ts.map +1 -1
- package/dist/session-view/store.js.map +1 -1
- package/dist/vault-routing.d.ts +3 -5
- package/dist/vault-routing.d.ts.map +1 -1
- package/dist/vault-routing.js +8 -20
- package/dist/vault-routing.js.map +1 -1
- package/dist/vault.d.ts +7 -5
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +111 -104
- package/dist/vault.js.map +1 -1
- package/package.json +7 -9
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 {
|
|
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
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
? parseInt(process.env.
|
|
63
|
-
:
|
|
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 (!
|
|
164
|
-
console.error("Missing env:
|
|
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,
|
|
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 = !!(
|
|
182
|
-
const hasTelegram = !!
|
|
183
|
-
const hasDiscord = !!
|
|
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:
|
|
187
|
-
" Telegram:
|
|
188
|
-
" Discord:
|
|
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,128 +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.
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
function
|
|
225
|
-
if (
|
|
226
|
-
return
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return `http://localhost:${MOM_LINK_PORT}`;
|
|
230
|
-
}
|
|
227
|
+
function portalBaseUrl() {
|
|
228
|
+
if (MAMA_LINK_URL)
|
|
229
|
+
return MAMA_LINK_URL.replace(/\/+$/, "");
|
|
230
|
+
if (MAMA_LINK_PORT)
|
|
231
|
+
return `http://localhost:${MAMA_LINK_PORT}`;
|
|
231
232
|
return undefined;
|
|
232
233
|
}
|
|
233
|
-
function isPrivateConversation(event) {
|
|
234
|
-
return event.conversationKind === "direct" || event.type === "dm";
|
|
235
|
-
}
|
|
236
|
-
function ensureLoginVault(platform, platformUserId) {
|
|
237
|
-
const vaultId = resolveActorVaultKey(sandbox, vaultManager, bindingStore, platform, platformUserId);
|
|
238
|
-
ensureSandboxVaultEntry(sandbox, vaultManager, platform, platformUserId, vaultId);
|
|
239
|
-
if (sandbox.type !== "container" && sandbox.type !== "image") {
|
|
240
|
-
vaultManager.addEntry(vaultId, createManagedVaultEntry(platform, platformUserId, vaultId));
|
|
241
|
-
}
|
|
242
|
-
return vaultId;
|
|
243
|
-
}
|
|
244
|
-
async function replyWithContext(responseCtx, text) {
|
|
245
|
-
await responseCtx.setTyping(false);
|
|
246
|
-
await responseCtx.setWorking(false);
|
|
247
|
-
await responseCtx.respond(text);
|
|
248
|
-
}
|
|
249
|
-
async function handleLoginCommand(platform, platformUserId, conversationId, responseCtx, commandText, privateConversation) {
|
|
250
|
-
const parsed = parseLoginCommand(commandText);
|
|
251
|
-
if (!parsed)
|
|
252
|
-
return false;
|
|
253
|
-
if (!privateConversation) {
|
|
254
|
-
await replyWithContext(responseCtx, "為了保護你的憑證,`/login` 只能在與機器人的私訊中使用。請先私訊機器人,再重新執行 `/login`。");
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
const baseUrl = normalizePortalBaseUrl();
|
|
258
|
-
if (!baseUrl) {
|
|
259
|
-
await replyWithContext(responseCtx, "Login is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.");
|
|
260
|
-
return true;
|
|
261
|
-
}
|
|
262
|
-
let vaultId;
|
|
263
|
-
try {
|
|
264
|
-
vaultId = ensureLoginVault(platform, platformUserId);
|
|
265
|
-
}
|
|
266
|
-
catch (error) {
|
|
267
|
-
log.logWarning(`[${conversationId}] Failed to prepare login vault for ${platform}/${platformUserId}`, error instanceof Error ? error.message : String(error));
|
|
268
|
-
await replyWithContext(responseCtx, "Login setup failed on the server. 請稍後重試,或聯絡管理員檢查 vault 儲存權限。");
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
const token = linkTokenStore.create(platform, platformUserId, conversationId, vaultId, "");
|
|
272
|
-
const vaultLabel = sandbox.type === "container" ? `container vault (${vaultId})` : "your vault";
|
|
273
|
-
await replyWithContext(responseCtx, `Open this link to store credentials in ${vaultLabel} (expires in 15 minutes):\n${baseUrl}/link?token=${token.token}`);
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
async function handleSessionViewCommand(platform, platformUserId, conversationId, sessionKey, bot, responseCtx, commandText, privateConversation) {
|
|
277
|
-
if (!parseSessionViewCommand(commandText))
|
|
278
|
-
return false;
|
|
279
|
-
const allowSharedPrivateDelivery = platform === "slack" || platform === "discord";
|
|
280
|
-
const sendSessionViewReply = async (text) => {
|
|
281
|
-
if (privateConversation) {
|
|
282
|
-
await replyWithContext(responseCtx, text);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (platform === "slack" && bot instanceof SlackBotClass) {
|
|
286
|
-
await bot.postEphemeral(conversationId, platformUserId, text);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (platform === "discord" && bot instanceof DiscordBot) {
|
|
290
|
-
await bot.sendDirectMessage(platformUserId, text);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
await replyWithContext(responseCtx, text);
|
|
294
|
-
};
|
|
295
|
-
if (!privateConversation && !allowSharedPrivateDelivery) {
|
|
296
|
-
await sendSessionViewReply("為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。");
|
|
297
|
-
return true;
|
|
298
|
-
}
|
|
299
|
-
const baseUrl = normalizePortalBaseUrl();
|
|
300
|
-
if (!baseUrl) {
|
|
301
|
-
await sendSessionViewReply("Session viewer is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.");
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
const sessionFile = resolveExistingSessionFile(workingDir, conversationId, sessionKey);
|
|
305
|
-
if (!sessionFile) {
|
|
306
|
-
await sendSessionViewReply("目前還沒有可查看的 session。先和機器人對話一次,建立 session 後再試。");
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
const token = sessionViewTokenStore.create(platform, platformUserId, conversationId, sessionKey, sessionFile);
|
|
310
|
-
const linkText = `Open this read-only session link (expires in 24 hours):\n${baseUrl}/session?token=${token.token}`;
|
|
311
|
-
await sendSessionViewReply(linkText);
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
const conversationStates = new Map();
|
|
315
|
-
/** Track in-flight runs for graceful shutdown */
|
|
316
|
-
const inFlightRuns = new Set();
|
|
317
|
-
/** Flag to stop accepting new events during shutdown */
|
|
318
|
-
let isShuttingDown = false;
|
|
319
|
-
/** Maximum number of cached sessions */
|
|
320
|
-
const MAX_SESSIONS = 500;
|
|
321
|
-
/** Idle timeout before a non-running session can be evicted (1 hour) */
|
|
322
|
-
const IDLE_TIMEOUT_MS = 3600000;
|
|
323
234
|
/** Idle timeout for managed image containers (10 minutes) */
|
|
324
235
|
const IMAGE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
325
236
|
if (provisioner) {
|
|
@@ -327,245 +238,16 @@ if (provisioner) {
|
|
|
327
238
|
await provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS);
|
|
328
239
|
setInterval(() => provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS), IMAGE_IDLE_TIMEOUT_MS).unref();
|
|
329
240
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const conversationDir = join(workingDir, conversationId);
|
|
341
|
-
const sessionScope = await resolveSessionScope(platformName, conversationDir, key);
|
|
342
|
-
state = {
|
|
343
|
-
running: false,
|
|
344
|
-
runner: await createRunner(sandbox, key, conversationId, conversationDir, workingDir, sessionScope, vaultManager, bindingStore, provisioner),
|
|
345
|
-
stopRequested: false,
|
|
346
|
-
lastAccessedAt: Date.now(),
|
|
347
|
-
};
|
|
348
|
-
conversationStates.set(key, state);
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
state.lastAccessedAt = Date.now();
|
|
352
|
-
}
|
|
353
|
-
return state;
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Evict idle sessions from conversationStates to bound memory usage.
|
|
357
|
-
* Called after each handleEvent completes.
|
|
358
|
-
*/
|
|
359
|
-
function evictIdleSessions() {
|
|
360
|
-
const now = Date.now();
|
|
361
|
-
for (const [key, state] of conversationStates) {
|
|
362
|
-
if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {
|
|
363
|
-
conversationStates.delete(key);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
if (conversationStates.size > MAX_SESSIONS) {
|
|
367
|
-
const idleSessions = [];
|
|
368
|
-
for (const [key, state] of conversationStates) {
|
|
369
|
-
if (!state.running) {
|
|
370
|
-
idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
|
|
374
|
-
const toEvict = conversationStates.size - MAX_SESSIONS;
|
|
375
|
-
for (let i = 0; i < toEvict && i < idleSessions.length; i++) {
|
|
376
|
-
conversationStates.delete(idleSessions[i].key);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
// ============================================================================
|
|
381
|
-
// Handler
|
|
382
|
-
// ============================================================================
|
|
383
|
-
const handler = {
|
|
384
|
-
isRunning(sessionKey) {
|
|
385
|
-
const state = conversationStates.get(sessionKey);
|
|
386
|
-
return !!state?.running;
|
|
387
|
-
},
|
|
388
|
-
getRunningSessions() {
|
|
389
|
-
const sessions = [];
|
|
390
|
-
for (const [sessionKey, state] of conversationStates) {
|
|
391
|
-
if (state.running && state.startedAt) {
|
|
392
|
-
// Get current step from runner
|
|
393
|
-
const currentStep = state.runner.getCurrentStep();
|
|
394
|
-
sessions.push({
|
|
395
|
-
sessionKey,
|
|
396
|
-
startedAt: state.startedAt,
|
|
397
|
-
lastActivityAt: state.lastActivityAt,
|
|
398
|
-
currentTool: currentStep?.label || currentStep?.toolName,
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return sessions;
|
|
403
|
-
},
|
|
404
|
-
async handleStop(sessionKey, conversationId, bot) {
|
|
405
|
-
const state = conversationStates.get(sessionKey);
|
|
406
|
-
if (state?.running) {
|
|
407
|
-
state.stopRequested = true;
|
|
408
|
-
state.runner.abort();
|
|
409
|
-
const ts = await bot.postMessage(conversationId, formatStopping(bot));
|
|
410
|
-
state.stopMessageTs = ts;
|
|
411
|
-
}
|
|
412
|
-
else {
|
|
413
|
-
await bot.postMessage(conversationId, formatNothingRunning(bot));
|
|
414
|
-
}
|
|
415
|
-
},
|
|
416
|
-
forceStop(sessionKey) {
|
|
417
|
-
const state = conversationStates.get(sessionKey);
|
|
418
|
-
if (state?.running) {
|
|
419
|
-
log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);
|
|
420
|
-
state.stopRequested = true;
|
|
421
|
-
state.runner.abort();
|
|
422
|
-
state.running = false;
|
|
423
|
-
}
|
|
424
|
-
},
|
|
425
|
-
async handleNew(sessionKey, conversationId, bot) {
|
|
426
|
-
const state = conversationStates.get(sessionKey);
|
|
427
|
-
if (state?.running) {
|
|
428
|
-
state.stopRequested = true;
|
|
429
|
-
state.runner.abort();
|
|
430
|
-
}
|
|
431
|
-
// Conversation sessions rotate via current pointer. Thread sessions reset in place.
|
|
432
|
-
const conversationDir = join(workingDir, conversationId);
|
|
433
|
-
if (sessionKey.includes(":")) {
|
|
434
|
-
createManagedSessionFileAtPath(getThreadSessionFile(conversationDir, sessionKey), conversationDir);
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);
|
|
438
|
-
}
|
|
439
|
-
// Remove from in-memory cache
|
|
440
|
-
conversationStates.delete(sessionKey);
|
|
441
|
-
log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);
|
|
442
|
-
await bot.postMessage(conversationId, "Conversation reset. Send a new message to start fresh.");
|
|
443
|
-
},
|
|
444
|
-
async handleEvent(event, bot, adapters, _isEvent) {
|
|
445
|
-
const conversationId = event.conversationId;
|
|
446
|
-
// Don't accept new events during shutdown
|
|
447
|
-
if (isShuttingDown) {
|
|
448
|
-
log.logInfo(`[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;
|
|
452
|
-
const privateConversation = isPrivateConversation(event);
|
|
453
|
-
const handledLogin = await handleLoginCommand(adapters.platform.name, event.user, conversationId, adapters.responseCtx, event.text, privateConversation);
|
|
454
|
-
if (handledLogin)
|
|
455
|
-
return;
|
|
456
|
-
const handledSessionView = await handleSessionViewCommand(adapters.platform.name, event.user, conversationId, sessionKey, bot, adapters.responseCtx, event.text, privateConversation);
|
|
457
|
-
if (handledSessionView)
|
|
458
|
-
return;
|
|
459
|
-
const conversationDir = join(workingDir, conversationId);
|
|
460
|
-
const waitedForParent = adapters.platform.name === "slack"
|
|
461
|
-
? await waitForSlackBranchBootstrap({
|
|
462
|
-
parentSessionKey: conversationId,
|
|
463
|
-
sessionKey,
|
|
464
|
-
hasThreadSession: () => hasMaterializedSlackBranchSession(conversationDir, sessionKey),
|
|
465
|
-
isParentRunning: () => conversationStates.get(conversationId)?.running === true,
|
|
466
|
-
})
|
|
467
|
-
: false;
|
|
468
|
-
if (waitedForParent) {
|
|
469
|
-
log.logInfo(`[${conversationId}] Delayed thread bootstrap until parent session sealed: ${sessionKey}`);
|
|
470
|
-
}
|
|
471
|
-
const state = await getState(conversationId, adapters.platform.name, sessionKey);
|
|
472
|
-
// Start run
|
|
473
|
-
state.running = true;
|
|
474
|
-
state.stopRequested = false;
|
|
475
|
-
state.startedAt = Date.now();
|
|
476
|
-
state.lastActivityAt = Date.now();
|
|
477
|
-
log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);
|
|
478
|
-
// Wrap in-flight run tracking
|
|
479
|
-
Sentry.metrics.count("agent.run.started", 1, {
|
|
480
|
-
attributes: { channel: conversationId },
|
|
481
|
-
});
|
|
482
|
-
Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size + 1);
|
|
483
|
-
const runPromise = Sentry.startSpan({ name: "agent.run", op: "agent", attributes: { conversationId, sessionKey } }, async () => {
|
|
484
|
-
return Sentry.withScope(async (scope) => {
|
|
485
|
-
const { message, responseCtx, platform } = adapters;
|
|
486
|
-
applyRunScope(scope, {
|
|
487
|
-
conversationId,
|
|
488
|
-
sessionKey,
|
|
489
|
-
messageId: message.id,
|
|
490
|
-
platform: platform.name,
|
|
491
|
-
userId: message.userId,
|
|
492
|
-
userName: message.userName,
|
|
493
|
-
threadTs: message.threadTs,
|
|
494
|
-
isEvent: _isEvent,
|
|
495
|
-
});
|
|
496
|
-
addLifecycleBreadcrumb("agent.run.started", {
|
|
497
|
-
channel_id: conversationId,
|
|
498
|
-
platform: platform.name,
|
|
499
|
-
has_attachments: (message.attachments?.length ?? 0) > 0,
|
|
500
|
-
});
|
|
501
|
-
try {
|
|
502
|
-
await responseCtx.setTyping(true);
|
|
503
|
-
await responseCtx.setWorking(true);
|
|
504
|
-
const result = await state.runner.run(message, responseCtx, platform);
|
|
505
|
-
await responseCtx.setWorking(false);
|
|
506
|
-
const durationMs = Date.now() - state.startedAt;
|
|
507
|
-
Sentry.metrics.distribution("agent.run.duration", durationMs, {
|
|
508
|
-
unit: "millisecond",
|
|
509
|
-
attributes: {
|
|
510
|
-
channel: conversationId,
|
|
511
|
-
platform: platform.name,
|
|
512
|
-
stop_reason: result.stopReason,
|
|
513
|
-
},
|
|
514
|
-
});
|
|
515
|
-
Sentry.metrics.count("agent.run.completed", 1, {
|
|
516
|
-
attributes: {
|
|
517
|
-
channel: conversationId,
|
|
518
|
-
platform: platform.name,
|
|
519
|
-
stop_reason: result.stopReason,
|
|
520
|
-
},
|
|
521
|
-
});
|
|
522
|
-
addLifecycleBreadcrumb("agent.run.completed", {
|
|
523
|
-
channel_id: conversationId,
|
|
524
|
-
platform: platform.name,
|
|
525
|
-
stop_reason: result.stopReason,
|
|
526
|
-
duration_ms: durationMs,
|
|
527
|
-
});
|
|
528
|
-
if (result.stopReason === "aborted" && state.stopRequested) {
|
|
529
|
-
if (state.stopMessageTs) {
|
|
530
|
-
await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));
|
|
531
|
-
state.stopMessageTs = undefined;
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
await bot.postMessage(conversationId, formatStopped(bot));
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
catch (err) {
|
|
539
|
-
scope.setContext("agent_run_error", {
|
|
540
|
-
conversationId,
|
|
541
|
-
sessionKey,
|
|
542
|
-
platform: adapters.platform.name,
|
|
543
|
-
messageId: adapters.message.id,
|
|
544
|
-
threadTs: adapters.message.threadTs,
|
|
545
|
-
});
|
|
546
|
-
Sentry.captureException(err);
|
|
547
|
-
Sentry.metrics.count("agent.run.errors", 1, {
|
|
548
|
-
attributes: { channel: conversationId, platform: adapters.platform.name },
|
|
549
|
-
});
|
|
550
|
-
log.logWarning(`[${conversationId}] Run error`, err instanceof Error ? err.message : String(err));
|
|
551
|
-
}
|
|
552
|
-
finally {
|
|
553
|
-
state.running = false;
|
|
554
|
-
state.lastAccessedAt = Date.now();
|
|
555
|
-
Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size - 1);
|
|
556
|
-
evictIdleSessions();
|
|
557
|
-
}
|
|
558
|
-
});
|
|
559
|
-
});
|
|
560
|
-
inFlightRuns.add(runPromise);
|
|
561
|
-
try {
|
|
562
|
-
await runPromise;
|
|
563
|
-
}
|
|
564
|
-
finally {
|
|
565
|
-
inFlightRuns.delete(runPromise);
|
|
566
|
-
}
|
|
567
|
-
},
|
|
568
|
-
};
|
|
241
|
+
const handler = createSessionRuntime({
|
|
242
|
+
workingDir,
|
|
243
|
+
sandbox,
|
|
244
|
+
vaultManager,
|
|
245
|
+
bindingStore,
|
|
246
|
+
provisioner,
|
|
247
|
+
linkTokenStore,
|
|
248
|
+
sessionViewTokenStore,
|
|
249
|
+
portalBaseUrl: portalBaseUrl(),
|
|
250
|
+
});
|
|
569
251
|
// ============================================================================
|
|
570
252
|
// Start
|
|
571
253
|
// ============================================================================
|
|
@@ -575,16 +257,18 @@ const sandboxDesc = sandbox.type === "host"
|
|
|
575
257
|
? `container:${sandbox.container}`
|
|
576
258
|
: sandbox.type === "image"
|
|
577
259
|
? `image:${sandbox.image}`
|
|
578
|
-
:
|
|
260
|
+
: sandbox.type === "firecracker"
|
|
261
|
+
? `firecracker:${sandbox.vmId}`
|
|
262
|
+
: `cloudflare:${sandbox.sandboxId}`;
|
|
579
263
|
log.logStartup(workingDir, sandboxDesc);
|
|
580
264
|
// Create platform bots
|
|
581
265
|
const bots = [];
|
|
582
266
|
const botsByPlatform = {};
|
|
583
267
|
if (hasSlack) {
|
|
584
|
-
const sharedStore = new ChannelStore({ workingDir, botToken:
|
|
268
|
+
const sharedStore = new ChannelStore({ workingDir, botToken: MAMA_SLACK_BOT_TOKEN });
|
|
585
269
|
const slackBot = new SlackBotClass(handler, {
|
|
586
|
-
appToken:
|
|
587
|
-
botToken:
|
|
270
|
+
appToken: MAMA_SLACK_APP_TOKEN,
|
|
271
|
+
botToken: MAMA_SLACK_BOT_TOKEN,
|
|
588
272
|
workingDir,
|
|
589
273
|
store: sharedStore,
|
|
590
274
|
});
|
|
@@ -594,7 +278,7 @@ if (hasSlack) {
|
|
|
594
278
|
}
|
|
595
279
|
if (hasTelegram) {
|
|
596
280
|
const telegramBot = new TelegramBot(handler, {
|
|
597
|
-
token:
|
|
281
|
+
token: MAMA_TELEGRAM_BOT_TOKEN,
|
|
598
282
|
workingDir,
|
|
599
283
|
});
|
|
600
284
|
bots.push(telegramBot);
|
|
@@ -603,15 +287,15 @@ if (hasTelegram) {
|
|
|
603
287
|
}
|
|
604
288
|
if (hasDiscord) {
|
|
605
289
|
const discordBot = new DiscordBot(handler, {
|
|
606
|
-
token:
|
|
290
|
+
token: MAMA_DISCORD_BOT_TOKEN,
|
|
607
291
|
workingDir,
|
|
608
292
|
});
|
|
609
293
|
bots.push(discordBot);
|
|
610
294
|
botsByPlatform.discord = discordBot;
|
|
611
295
|
log.logInfo("Platform: Discord");
|
|
612
296
|
}
|
|
613
|
-
if (
|
|
614
|
-
startLinkServer(
|
|
297
|
+
if (MAMA_LINK_PORT) {
|
|
298
|
+
startLinkServer(MAMA_LINK_PORT, linkTokenStore, vaultManager, async (platform, conversationId, message) => {
|
|
615
299
|
const bot = botsByPlatform[platform];
|
|
616
300
|
if (bot)
|
|
617
301
|
await bot.postMessage(conversationId, message);
|
|
@@ -626,17 +310,7 @@ if (slackBot) {
|
|
|
626
310
|
eventsWatcher.start();
|
|
627
311
|
// Handle shutdown
|
|
628
312
|
async function shutdown() {
|
|
629
|
-
|
|
630
|
-
return;
|
|
631
|
-
isShuttingDown = true;
|
|
632
|
-
log.logInfo("Shutting down gracefully...");
|
|
633
|
-
const timeout = Date.now() + 30000;
|
|
634
|
-
while (inFlightRuns.size > 0 && Date.now() < timeout) {
|
|
635
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
636
|
-
}
|
|
637
|
-
if (inFlightRuns.size > 0) {
|
|
638
|
-
log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);
|
|
639
|
-
}
|
|
313
|
+
await handler.shutdown();
|
|
640
314
|
eventsWatcher.stop();
|
|
641
315
|
await Sentry.close(5000);
|
|
642
316
|
process.exit(0);
|