@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.
- package/README.md +100 -421
- 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 +4 -1
- 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/slack/bot.d.ts +1 -0
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +9 -3
- 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/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 +13 -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.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 +34 -40
- 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/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 +62 -386
- 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-view/portal.d.ts.map +1 -1
- package/dist/session-view/portal.js +10 -1
- 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 +36 -26
- 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 +101 -50
- package/dist/vault.js.map +1 -1
- 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 {
|
|
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,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.
|
|
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
227
|
function portalBaseUrl() {
|
|
225
|
-
if (
|
|
226
|
-
return
|
|
227
|
-
if (
|
|
228
|
-
return `http://localhost:${
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
:
|
|
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:
|
|
268
|
+
const sharedStore = new ChannelStore({ workingDir, botToken: MAMA_SLACK_BOT_TOKEN });
|
|
583
269
|
const slackBot = new SlackBotClass(handler, {
|
|
584
|
-
appToken:
|
|
585
|
-
botToken:
|
|
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:
|
|
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:
|
|
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 (
|
|
612
|
-
startLinkServer(
|
|
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
|
-
|
|
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);
|