@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.
- package/README.md +151 -417
- 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 +2 -1
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +71 -18
- 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 +5 -0
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +69 -11
- 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 +1 -35
- 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 -7
- 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 +15 -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/model.d.ts +14 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +94 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/new.d.ts +9 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +28 -0
- package/dist/commands/new.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 +43 -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 +15 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +167 -56
- 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 +107 -388
- 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 +27 -0
- package/dist/runtime/session-runtime.d.ts.map +1 -0
- package/dist/runtime/session-runtime.js +303 -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
|
-
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 {
|
|
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() {
|
|
@@ -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
|
-
|
|
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 (!
|
|
164
|
-
console.error("Missing env:
|
|
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,
|
|
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 = !!(
|
|
182
|
-
const hasTelegram = !!
|
|
183
|
-
const hasDiscord = !!
|
|
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:
|
|
187
|
-
" Telegram:
|
|
188
|
-
" Discord:
|
|
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.
|
|
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
|
|
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 =
|
|
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,
|
|
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 (
|
|
226
|
-
return
|
|
227
|
-
if (
|
|
228
|
-
return `http://localhost:${
|
|
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
|
-
|
|
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
|
-
};
|
|
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
|
-
:
|
|
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:
|
|
311
|
+
const sharedStore = new ChannelStore({ workingDir, botToken: MAMA_SLACK_BOT_TOKEN });
|
|
583
312
|
const slackBot = new SlackBotClass(handler, {
|
|
584
|
-
appToken:
|
|
585
|
-
botToken:
|
|
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:
|
|
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:
|
|
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 (
|
|
612
|
-
startLinkServer(
|
|
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
|
-
|
|
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);
|