@geminixiang/mama 0.2.0-beta.0 → 0.2.0-beta.1
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 +59 -19
- package/dist/adapter.d.ts +9 -7
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +2 -2
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +33 -21
- 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 +20 -13
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +4 -3
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +68 -30
- 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 +22 -12
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +2 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +54 -33
- 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 +61 -10
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/agent.d.ts +7 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +152 -96
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +63 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +94 -0
- package/dist/bindings.js.map +1 -0
- package/dist/config.d.ts +32 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +71 -44
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +6 -6
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +8 -8
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +4 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +20 -5
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +20 -0
- package/dist/execution-resolver.d.ts.map +1 -0
- package/dist/execution-resolver.js +51 -0
- package/dist/execution-resolver.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +11 -4
- package/dist/instrument.js.map +1 -1
- package/dist/link-server.d.ts +16 -0
- package/dist/link-server.d.ts.map +1 -0
- package/dist/link-server.js +839 -0
- package/dist/link-server.js.map +1 -0
- package/dist/link-token.d.ts +32 -0
- package/dist/link-token.d.ts.map +1 -0
- package/dist/link-token.js +68 -0
- package/dist/link-token.js.map +1 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +7 -7
- package/dist/log.js.map +1 -1
- package/dist/login.d.ts +29 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +164 -0
- package/dist/login.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +243 -56
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +93 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +336 -0
- package/dist/provisioner.js.map +1 -0
- package/dist/sandbox/container.d.ts +15 -0
- package/dist/sandbox/container.d.ts.map +1 -0
- package/dist/sandbox/container.js +122 -0
- package/dist/sandbox/container.js.map +1 -0
- package/dist/sandbox/errors.d.ts +6 -0
- package/dist/sandbox/errors.d.ts.map +1 -0
- package/dist/sandbox/errors.js +11 -0
- package/dist/sandbox/errors.js.map +1 -0
- package/dist/sandbox/firecracker.d.ts +16 -0
- package/dist/sandbox/firecracker.d.ts.map +1 -0
- package/dist/sandbox/firecracker.js +206 -0
- package/dist/sandbox/firecracker.js.map +1 -0
- package/dist/sandbox/host.d.ts +12 -0
- package/dist/sandbox/host.d.ts.map +1 -0
- package/dist/sandbox/host.js +89 -0
- package/dist/sandbox/host.js.map +1 -0
- package/dist/sandbox/image.d.ts +5 -0
- package/dist/sandbox/image.d.ts.map +1 -0
- package/dist/sandbox/image.js +30 -0
- package/dist/sandbox/image.js.map +1 -0
- package/dist/sandbox/index.d.ts +20 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +51 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/types.d.ts +51 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +2 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/sandbox/utils.d.ts +4 -0
- package/dist/sandbox/utils.d.ts.map +1 -0
- package/dist/sandbox/utils.js +51 -0
- package/dist/sandbox/utils.js.map +1 -0
- package/dist/sandbox.d.ts +1 -39
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +1 -286
- package/dist/sandbox.js.map +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +2 -2
- package/dist/sentry.js.map +1 -1
- package/dist/session-store.d.ts +1 -5
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +7 -10
- package/dist/session-store.js.map +1 -1
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +8 -8
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +21 -0
- package/dist/tools/event.d.ts.map +1 -0
- package/dist/tools/event.js +103 -0
- package/dist/tools/event.js.map +1 -0
- package/dist/tools/index.d.ts +6 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/ui-copy.d.ts +11 -0
- package/dist/ui-copy.d.ts.map +1 -0
- package/dist/ui-copy.js +33 -0
- package/dist/ui-copy.js.map +1 -0
- package/dist/vault-routing.d.ts +10 -0
- package/dist/vault-routing.d.ts.map +1 -0
- package/dist/vault-routing.js +58 -0
- package/dist/vault-routing.js.map +1 -0
- package/dist/vault.d.ts +106 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +389 -0
- package/dist/vault.js.map +1 -0
- package/dist/vault.test.d.ts +2 -0
- package/dist/vault.test.d.ts.map +1 -0
- package/dist/vault.test.js +67 -0
- package/dist/vault.test.js.map +1 -0
- package/package.json +12 -11
package/dist/main.js
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./instrument.js";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { mkdirSync, readFileSync, statSync } from "fs";
|
|
5
6
|
import { fileURLToPath } from "url";
|
|
6
7
|
import { dirname, join as pathJoin } from "path";
|
|
7
8
|
import { DiscordBot } from "./adapters/discord/index.js";
|
|
8
9
|
import { TelegramBot } from "./adapters/telegram/index.js";
|
|
9
10
|
import { SlackBot as SlackBotClass } from "./adapters/slack/index.js";
|
|
10
11
|
import { createRunner } from "./agent.js";
|
|
11
|
-
import { createManagedSessionFile, createManagedSessionFileAtPath,
|
|
12
|
+
import { createManagedSessionFile, createManagedSessionFileAtPath, getChannelSessionDir, getThreadSessionFile, } from "./session-store.js";
|
|
12
13
|
import { downloadChannel } from "./download.js";
|
|
13
14
|
import { createEventsWatcher } from "./events.js";
|
|
14
15
|
import * as log from "./log.js";
|
|
15
|
-
import {
|
|
16
|
+
import { FileUserBindingStore } from "./bindings.js";
|
|
17
|
+
import { startLinkServer } from "./link-server.js";
|
|
18
|
+
import { parseLoginCommand } from "./login.js";
|
|
19
|
+
import { InMemoryLinkTokenStore } from "./link-token.js";
|
|
20
|
+
import { DockerContainerManager } from "./provisioner.js";
|
|
21
|
+
import { SandboxError, parseSandboxArg, validateSandbox } from "./sandbox.js";
|
|
22
|
+
import { formatNothingRunning, formatStopping } from "./ui-copy.js";
|
|
23
|
+
import { FileVaultManager } from "./vault.js";
|
|
24
|
+
import { ensureSettingsFile } from "./config.js";
|
|
25
|
+
import { createManagedVaultEntry, ensureSandboxVaultEntry, resolveActorVaultKey, } from "./vault-routing.js";
|
|
16
26
|
import { addLifecycleBreadcrumb, applyRunScope } from "./sentry.js";
|
|
17
27
|
import { ChannelStore } from "./store.js";
|
|
18
28
|
import * as Sentry from "@sentry/node";
|
|
@@ -43,10 +53,23 @@ const MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;
|
|
|
43
53
|
const MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;
|
|
44
54
|
const MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;
|
|
45
55
|
const MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;
|
|
56
|
+
/** Base URL of the web login portal, e.g. https://platform.trygemini.xyz */
|
|
57
|
+
const MOM_LINK_URL = process.env.MOM_LINK_URL;
|
|
58
|
+
/**
|
|
59
|
+
* Port for the link callback HTTP server.
|
|
60
|
+
* Defaults to 8181 when MOM_LINK_URL is set (behind a reverse proxy).
|
|
61
|
+
* If neither is set, the server is not started.
|
|
62
|
+
*/
|
|
63
|
+
const MOM_LINK_PORT = process.env.MOM_LINK_PORT
|
|
64
|
+
? parseInt(process.env.MOM_LINK_PORT, 10)
|
|
65
|
+
: MOM_LINK_URL
|
|
66
|
+
? 8181
|
|
67
|
+
: undefined;
|
|
46
68
|
function parseArgs() {
|
|
47
69
|
const args = process.argv.slice(2);
|
|
48
70
|
let sandbox = { type: "host" };
|
|
49
71
|
let workingDir;
|
|
72
|
+
let stateDirArg;
|
|
50
73
|
let downloadChannelId;
|
|
51
74
|
let showVersion = false;
|
|
52
75
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -60,6 +83,12 @@ function parseArgs() {
|
|
|
60
83
|
else if (arg === "--sandbox") {
|
|
61
84
|
sandbox = parseSandboxArg(args[++i] || "");
|
|
62
85
|
}
|
|
86
|
+
else if (arg.startsWith("--state-dir=")) {
|
|
87
|
+
stateDirArg = arg.slice("--state-dir=".length);
|
|
88
|
+
}
|
|
89
|
+
else if (arg === "--state-dir") {
|
|
90
|
+
stateDirArg = args[++i];
|
|
91
|
+
}
|
|
63
92
|
else if (arg.startsWith("--download=")) {
|
|
64
93
|
downloadChannelId = arg.slice("--download=".length);
|
|
65
94
|
}
|
|
@@ -72,12 +101,66 @@ function parseArgs() {
|
|
|
72
101
|
}
|
|
73
102
|
return {
|
|
74
103
|
workingDir: workingDir ? resolve(workingDir) : undefined,
|
|
104
|
+
stateDir: stateDirArg ? resolve(stateDirArg) : undefined,
|
|
75
105
|
sandbox,
|
|
76
106
|
downloadChannel: downloadChannelId,
|
|
77
107
|
showVersion,
|
|
78
108
|
};
|
|
79
109
|
}
|
|
80
|
-
const
|
|
110
|
+
const WORLD_WRITABLE_MODE = 0o002;
|
|
111
|
+
/**
|
|
112
|
+
* Create stateDir if missing and refuse to use it if another local user could
|
|
113
|
+
* tamper with its contents. stateDir holds vaults, bindings, and settings —
|
|
114
|
+
* a world-writable or foreign-owned directory there would let a local attacker
|
|
115
|
+
* swap in credentials or change routing.
|
|
116
|
+
*/
|
|
117
|
+
function ensureSecureStateDir(path) {
|
|
118
|
+
let stat;
|
|
119
|
+
try {
|
|
120
|
+
stat = statSync(path);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
const code = err.code;
|
|
124
|
+
if (code === "ENOENT") {
|
|
125
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.error(`Error: cannot access --state-dir ${path}: ${err.message}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
if (!stat.isDirectory()) {
|
|
132
|
+
console.error(`Error: --state-dir ${path} exists but is not a directory`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
if (stat.mode & WORLD_WRITABLE_MODE) {
|
|
136
|
+
console.error(`Error: --state-dir ${path} is world-writable (mode ${(stat.mode & 0o777).toString(8)}). ` +
|
|
137
|
+
`Credentials stored there would be exposed to other local users. ` +
|
|
138
|
+
`Fix with: chmod 0700 ${path}`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
const euid = typeof process.geteuid === "function" ? process.geteuid() : undefined;
|
|
142
|
+
if (euid !== undefined && stat.uid !== euid) {
|
|
143
|
+
console.error(`Error: --state-dir ${path} is owned by uid ${stat.uid} but mama is running as uid ${euid}. ` +
|
|
144
|
+
`Run mama as the directory owner or point --state-dir at a directory you own.`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function handleStartupError(error) {
|
|
149
|
+
if (error instanceof SandboxError) {
|
|
150
|
+
for (const line of error.formatForCli()) {
|
|
151
|
+
console.error(line);
|
|
152
|
+
}
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
let parsedArgs;
|
|
158
|
+
try {
|
|
159
|
+
parsedArgs = parseArgs();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
handleStartupError(error);
|
|
163
|
+
}
|
|
81
164
|
// Handle --version
|
|
82
165
|
if (parsedArgs.showVersion) {
|
|
83
166
|
console.log(getVersion());
|
|
@@ -94,11 +177,27 @@ if (parsedArgs.downloadChannel) {
|
|
|
94
177
|
}
|
|
95
178
|
// Normal bot mode - require working dir
|
|
96
179
|
if (!parsedArgs.workingDir) {
|
|
97
|
-
console.error("Usage: mama [--sandbox=host|
|
|
180
|
+
console.error("Usage: mama [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>] [--state-dir=<path>] <working-directory>");
|
|
98
181
|
console.error(" mama --download <channel-id>");
|
|
99
182
|
process.exit(1);
|
|
100
183
|
}
|
|
101
184
|
const { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };
|
|
185
|
+
// stateDir holds operator-managed files (vaults, settings, bindings).
|
|
186
|
+
// Defaults to ~/.mama to keep secrets outside the project workspace mounted into sandboxes.
|
|
187
|
+
const stateDir = parsedArgs.stateDir ?? join(homedir(), ".mama");
|
|
188
|
+
ensureSecureStateDir(stateDir);
|
|
189
|
+
// Share stateDir with instrument.ts (for Sentry config loading)
|
|
190
|
+
process.env.MAMA_STATE_DIR = stateDir;
|
|
191
|
+
// Ensure settings.json exists; create a template if first run.
|
|
192
|
+
const { created: settingsCreated, config: agentSettings } = ensureSettingsFile(stateDir);
|
|
193
|
+
if (settingsCreated) {
|
|
194
|
+
console.log(`Created default settings: ${join(stateDir, "settings.json")}`);
|
|
195
|
+
console.log("Review and update provider/model before starting.");
|
|
196
|
+
}
|
|
197
|
+
if (!agentSettings.provider || !agentSettings.model) {
|
|
198
|
+
console.error(`Error: 'provider' and 'model' must be set in ${join(stateDir, "settings.json")}`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
102
201
|
// Validate platform tokens
|
|
103
202
|
const hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);
|
|
104
203
|
const hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;
|
|
@@ -110,28 +209,74 @@ if (!hasSlack && !hasTelegram && !hasDiscord) {
|
|
|
110
209
|
" Discord: MOM_DISCORD_BOT_TOKEN");
|
|
111
210
|
process.exit(1);
|
|
112
211
|
}
|
|
113
|
-
|
|
114
|
-
|
|
212
|
+
try {
|
|
213
|
+
await validateSandbox(sandbox);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
handleStartupError(error);
|
|
217
|
+
}
|
|
218
|
+
const vaultManager = new FileVaultManager(stateDir);
|
|
219
|
+
if (vaultManager.isEnabled()) {
|
|
220
|
+
console.log(sandbox.type === "container"
|
|
221
|
+
? " Vault system enabled. Shared container vault active."
|
|
222
|
+
: " Vault system enabled. Per-user credential routing active.");
|
|
223
|
+
}
|
|
224
|
+
const bindingStore = new FileUserBindingStore(stateDir);
|
|
225
|
+
if (bindingStore.isEnabled()) {
|
|
226
|
+
console.log(sandbox.type === "container"
|
|
227
|
+
? " Binding store enabled. Shared container mode ignores per-user vault bindings."
|
|
228
|
+
: " Binding store enabled. Platform user → vault routing active.");
|
|
229
|
+
}
|
|
230
|
+
const provisioner = sandbox.type === "image" ? new DockerContainerManager(sandbox.image, workingDir) : undefined;
|
|
231
|
+
const linkTokenStore = new InMemoryLinkTokenStore();
|
|
232
|
+
// Purge expired link tokens every 5 minutes
|
|
233
|
+
setInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();
|
|
234
|
+
const conversationStates = new Map();
|
|
115
235
|
/** Track in-flight runs for graceful shutdown */
|
|
116
236
|
const inFlightRuns = new Set();
|
|
117
237
|
/** Flag to stop accepting new events during shutdown */
|
|
118
238
|
let isShuttingDown = false;
|
|
119
239
|
/** Maximum number of cached sessions */
|
|
120
240
|
const MAX_SESSIONS = 500;
|
|
121
|
-
/** Idle timeout before a non-running session can be evicted (
|
|
122
|
-
const IDLE_TIMEOUT_MS =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
241
|
+
/** Idle timeout before a non-running session can be evicted (10 minutes) */
|
|
242
|
+
const IDLE_TIMEOUT_MS = 600000;
|
|
243
|
+
if (provisioner) {
|
|
244
|
+
await provisioner.reconcile();
|
|
245
|
+
await provisioner.stopIdle(IDLE_TIMEOUT_MS);
|
|
246
|
+
}
|
|
247
|
+
// Stop idle containers every hour (same cadence as session eviction)
|
|
248
|
+
if (provisioner) {
|
|
249
|
+
setInterval(() => provisioner.stopIdle(IDLE_TIMEOUT_MS), IDLE_TIMEOUT_MS).unref();
|
|
250
|
+
}
|
|
251
|
+
function normalizeLoginBaseUrl() {
|
|
252
|
+
if (MOM_LINK_URL) {
|
|
253
|
+
return MOM_LINK_URL.replace(/\/+$/, "");
|
|
254
|
+
}
|
|
255
|
+
if (MOM_LINK_PORT) {
|
|
256
|
+
return `http://localhost:${MOM_LINK_PORT}`;
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
function ensureLoginVault(platform, platformUserId) {
|
|
261
|
+
const vaultId = resolveActorVaultKey(sandbox, vaultManager, bindingStore, platform, platformUserId);
|
|
262
|
+
ensureSandboxVaultEntry(sandbox, vaultManager, platform, platformUserId, vaultId);
|
|
263
|
+
if (sandbox.type !== "image" && sandbox.type !== "container") {
|
|
264
|
+
vaultManager.addEntry(vaultId, createManagedVaultEntry(platform, platformUserId, vaultId, false));
|
|
265
|
+
}
|
|
266
|
+
return vaultId;
|
|
267
|
+
}
|
|
268
|
+
async function getState(conversationId, sessionKey) {
|
|
269
|
+
const key = sessionKey ?? conversationId;
|
|
270
|
+
let state = conversationStates.get(key);
|
|
126
271
|
if (!state) {
|
|
127
|
-
const
|
|
272
|
+
const conversationDir = join(workingDir, conversationId);
|
|
128
273
|
state = {
|
|
129
274
|
running: false,
|
|
130
|
-
runner: await createRunner(sandbox, key,
|
|
275
|
+
runner: await createRunner(sandbox, key, conversationId, conversationDir, workingDir, vaultManager, bindingStore, provisioner, stateDir),
|
|
131
276
|
stopRequested: false,
|
|
132
277
|
lastAccessedAt: Date.now(),
|
|
133
278
|
};
|
|
134
|
-
|
|
279
|
+
conversationStates.set(key, state);
|
|
135
280
|
}
|
|
136
281
|
else {
|
|
137
282
|
state.lastAccessedAt = Date.now();
|
|
@@ -139,27 +284,27 @@ async function getState(channelId, sessionKey) {
|
|
|
139
284
|
return state;
|
|
140
285
|
}
|
|
141
286
|
/**
|
|
142
|
-
* Evict idle sessions from
|
|
287
|
+
* Evict idle sessions from conversationStates to bound memory usage.
|
|
143
288
|
* Called after each handleEvent completes.
|
|
144
289
|
*/
|
|
145
290
|
function evictIdleSessions() {
|
|
146
291
|
const now = Date.now();
|
|
147
|
-
for (const [key, state] of
|
|
292
|
+
for (const [key, state] of conversationStates) {
|
|
148
293
|
if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {
|
|
149
|
-
|
|
294
|
+
conversationStates.delete(key);
|
|
150
295
|
}
|
|
151
296
|
}
|
|
152
|
-
if (
|
|
297
|
+
if (conversationStates.size > MAX_SESSIONS) {
|
|
153
298
|
const idleSessions = [];
|
|
154
|
-
for (const [key, state] of
|
|
299
|
+
for (const [key, state] of conversationStates) {
|
|
155
300
|
if (!state.running) {
|
|
156
301
|
idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });
|
|
157
302
|
}
|
|
158
303
|
}
|
|
159
304
|
idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
|
|
160
|
-
const toEvict =
|
|
305
|
+
const toEvict = conversationStates.size - MAX_SESSIONS;
|
|
161
306
|
for (let i = 0; i < toEvict && i < idleSessions.length; i++) {
|
|
162
|
-
|
|
307
|
+
conversationStates.delete(idleSessions[i].key);
|
|
163
308
|
}
|
|
164
309
|
}
|
|
165
310
|
}
|
|
@@ -168,12 +313,12 @@ function evictIdleSessions() {
|
|
|
168
313
|
// ============================================================================
|
|
169
314
|
const handler = {
|
|
170
315
|
isRunning(sessionKey) {
|
|
171
|
-
const state =
|
|
316
|
+
const state = conversationStates.get(sessionKey);
|
|
172
317
|
return !!state?.running;
|
|
173
318
|
},
|
|
174
319
|
getRunningSessions() {
|
|
175
320
|
const sessions = [];
|
|
176
|
-
for (const [sessionKey, state] of
|
|
321
|
+
for (const [sessionKey, state] of conversationStates) {
|
|
177
322
|
if (state.running && state.startedAt) {
|
|
178
323
|
// Get current step from runner
|
|
179
324
|
const currentStep = state.runner.getCurrentStep();
|
|
@@ -187,20 +332,20 @@ const handler = {
|
|
|
187
332
|
}
|
|
188
333
|
return sessions;
|
|
189
334
|
},
|
|
190
|
-
async handleStop(sessionKey,
|
|
191
|
-
const state =
|
|
335
|
+
async handleStop(sessionKey, conversationId, bot) {
|
|
336
|
+
const state = conversationStates.get(sessionKey);
|
|
192
337
|
if (state?.running) {
|
|
193
338
|
state.stopRequested = true;
|
|
194
339
|
state.runner.abort();
|
|
195
|
-
const ts = await bot.postMessage(
|
|
340
|
+
const ts = await bot.postMessage(conversationId, formatStopping(bot));
|
|
196
341
|
state.stopMessageTs = ts;
|
|
197
342
|
}
|
|
198
343
|
else {
|
|
199
|
-
await bot.postMessage(
|
|
344
|
+
await bot.postMessage(conversationId, formatNothingRunning(bot));
|
|
200
345
|
}
|
|
201
346
|
},
|
|
202
347
|
forceStop(sessionKey) {
|
|
203
|
-
const state =
|
|
348
|
+
const state = conversationStates.get(sessionKey);
|
|
204
349
|
if (state?.running) {
|
|
205
350
|
log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);
|
|
206
351
|
state.stopRequested = true;
|
|
@@ -208,49 +353,81 @@ const handler = {
|
|
|
208
353
|
state.running = false;
|
|
209
354
|
}
|
|
210
355
|
},
|
|
211
|
-
async handleNew(sessionKey,
|
|
212
|
-
const state =
|
|
356
|
+
async handleNew(sessionKey, conversationId, bot) {
|
|
357
|
+
const state = conversationStates.get(sessionKey);
|
|
213
358
|
if (state?.running) {
|
|
214
359
|
state.stopRequested = true;
|
|
215
360
|
state.runner.abort();
|
|
216
361
|
}
|
|
217
362
|
// Channel sessions rotate via current pointer. Thread sessions reset in place.
|
|
218
|
-
const
|
|
363
|
+
const conversationDir = join(workingDir, conversationId);
|
|
219
364
|
if (sessionKey.includes(":")) {
|
|
220
|
-
createManagedSessionFileAtPath(getThreadSessionFile(
|
|
365
|
+
createManagedSessionFileAtPath(getThreadSessionFile(conversationDir, sessionKey), conversationDir);
|
|
221
366
|
}
|
|
222
367
|
else {
|
|
223
|
-
createManagedSessionFile(
|
|
368
|
+
createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);
|
|
224
369
|
}
|
|
225
370
|
// Remove from in-memory cache
|
|
226
|
-
|
|
227
|
-
log.logInfo(`[${
|
|
228
|
-
await bot.postMessage(
|
|
371
|
+
conversationStates.delete(sessionKey);
|
|
372
|
+
log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);
|
|
373
|
+
await bot.postMessage(conversationId, "Conversation reset. Send a new message to start fresh.");
|
|
374
|
+
},
|
|
375
|
+
async handleLogin(platform, platformUserId, conversationId, bot, commandText, isPrivateConversation) {
|
|
376
|
+
const parsed = parseLoginCommand(commandText);
|
|
377
|
+
if (!parsed) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (!isPrivateConversation) {
|
|
381
|
+
await bot.postMessage(conversationId, "为了保护你的凭证,`/login` 只能在与机器人的私聊中使用。请先私信机器人,再重新执行 `/login`。");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const baseUrl = normalizeLoginBaseUrl();
|
|
385
|
+
if (!baseUrl) {
|
|
386
|
+
await bot.postMessage(conversationId, "Login is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
let vaultId;
|
|
390
|
+
try {
|
|
391
|
+
vaultId = ensureLoginVault(platform, platformUserId);
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
log.logWarning(`[${conversationId}] Failed to prepare login vault for ${platform}/${platformUserId}`, error instanceof Error ? error.message : String(error));
|
|
395
|
+
await bot.postMessage(conversationId, "Login setup failed on the server. 请稍后重试,或联系管理员检查 vault 存储权限。");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const loginLabel = "credential";
|
|
399
|
+
const vaultLabel = sandbox.type === "container" ? "the shared container vault" : "your vault";
|
|
400
|
+
await bot.postMessage(conversationId, `Open this link to store ${loginLabel} in ${vaultLabel} ` +
|
|
401
|
+
`(expires in 15 minutes):\n${baseUrl}/link?token=${linkTokenStore.create(platform, platformUserId, conversationId, vaultId, "").token}`);
|
|
229
402
|
},
|
|
230
403
|
async handleEvent(event, bot, adapters, _isEvent) {
|
|
231
404
|
// Don't accept new events during shutdown
|
|
232
405
|
if (isShuttingDown) {
|
|
233
|
-
log.logInfo(`[${event.
|
|
406
|
+
log.logInfo(`[${event.conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);
|
|
234
407
|
return;
|
|
235
408
|
}
|
|
236
|
-
const sessionKey = event.sessionKey ?? `${event.
|
|
237
|
-
const state = await getState(event.
|
|
409
|
+
const sessionKey = event.sessionKey ?? `${event.conversationId}:${event.thread_ts ?? event.ts}`;
|
|
410
|
+
const state = await getState(event.conversationId, sessionKey);
|
|
238
411
|
// Start run
|
|
239
412
|
state.running = true;
|
|
240
413
|
state.stopRequested = false;
|
|
241
414
|
state.startedAt = Date.now();
|
|
242
415
|
state.lastActivityAt = Date.now();
|
|
243
|
-
log.logInfo(`[${event.
|
|
416
|
+
log.logInfo(`[${event.conversationId}] Starting run: ${event.text.substring(0, 50)}`);
|
|
244
417
|
// Wrap in-flight run tracking
|
|
245
418
|
Sentry.metrics.count("agent.run.started", 1, {
|
|
246
|
-
attributes: { channel: event.
|
|
419
|
+
attributes: { channel: event.conversationId },
|
|
247
420
|
});
|
|
248
421
|
Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size + 1);
|
|
249
|
-
const runPromise = Sentry.startSpan({
|
|
422
|
+
const runPromise = Sentry.startSpan({
|
|
423
|
+
name: "agent.run",
|
|
424
|
+
op: "agent",
|
|
425
|
+
attributes: { channelId: event.conversationId, sessionKey },
|
|
426
|
+
}, async () => {
|
|
250
427
|
return Sentry.withScope(async (scope) => {
|
|
251
428
|
const { message, responseCtx, platform } = adapters;
|
|
252
429
|
applyRunScope(scope, {
|
|
253
|
-
|
|
430
|
+
conversationId: event.conversationId,
|
|
254
431
|
sessionKey,
|
|
255
432
|
messageId: message.id,
|
|
256
433
|
platform: platform.name,
|
|
@@ -260,7 +437,7 @@ const handler = {
|
|
|
260
437
|
isEvent: _isEvent,
|
|
261
438
|
});
|
|
262
439
|
addLifecycleBreadcrumb("agent.run.started", {
|
|
263
|
-
channel_id: event.
|
|
440
|
+
channel_id: event.conversationId,
|
|
264
441
|
platform: platform.name,
|
|
265
442
|
has_attachments: (message.attachments?.length ?? 0) > 0,
|
|
266
443
|
});
|
|
@@ -273,37 +450,37 @@ const handler = {
|
|
|
273
450
|
Sentry.metrics.distribution("agent.run.duration", durationMs, {
|
|
274
451
|
unit: "millisecond",
|
|
275
452
|
attributes: {
|
|
276
|
-
channel: event.
|
|
453
|
+
channel: event.conversationId,
|
|
277
454
|
platform: platform.name,
|
|
278
455
|
stop_reason: result.stopReason,
|
|
279
456
|
},
|
|
280
457
|
});
|
|
281
458
|
Sentry.metrics.count("agent.run.completed", 1, {
|
|
282
459
|
attributes: {
|
|
283
|
-
channel: event.
|
|
460
|
+
channel: event.conversationId,
|
|
284
461
|
platform: platform.name,
|
|
285
462
|
stop_reason: result.stopReason,
|
|
286
463
|
},
|
|
287
464
|
});
|
|
288
465
|
addLifecycleBreadcrumb("agent.run.completed", {
|
|
289
|
-
channel_id: event.
|
|
466
|
+
channel_id: event.conversationId,
|
|
290
467
|
platform: platform.name,
|
|
291
468
|
stop_reason: result.stopReason,
|
|
292
469
|
duration_ms: durationMs,
|
|
293
470
|
});
|
|
294
471
|
if (result.stopReason === "aborted" && state.stopRequested) {
|
|
295
472
|
if (state.stopMessageTs) {
|
|
296
|
-
await bot.updateMessage(event.
|
|
473
|
+
await bot.updateMessage(event.conversationId, state.stopMessageTs, "_Stopped_");
|
|
297
474
|
state.stopMessageTs = undefined;
|
|
298
475
|
}
|
|
299
476
|
else {
|
|
300
|
-
await bot.postMessage(event.
|
|
477
|
+
await bot.postMessage(event.conversationId, "_Stopped_");
|
|
301
478
|
}
|
|
302
479
|
}
|
|
303
480
|
}
|
|
304
481
|
catch (err) {
|
|
305
482
|
scope.setContext("agent_run_error", {
|
|
306
|
-
|
|
483
|
+
conversationId: event.conversationId,
|
|
307
484
|
sessionKey,
|
|
308
485
|
platform: adapters.platform.name,
|
|
309
486
|
messageId: adapters.message.id,
|
|
@@ -311,9 +488,9 @@ const handler = {
|
|
|
311
488
|
});
|
|
312
489
|
Sentry.captureException(err);
|
|
313
490
|
Sentry.metrics.count("agent.run.errors", 1, {
|
|
314
|
-
attributes: { channel: event.
|
|
491
|
+
attributes: { channel: event.conversationId, platform: adapters.platform.name },
|
|
315
492
|
});
|
|
316
|
-
log.logWarning(`[${event.
|
|
493
|
+
log.logWarning(`[${event.conversationId}] Run error`, err instanceof Error ? err.message : String(err));
|
|
317
494
|
}
|
|
318
495
|
finally {
|
|
319
496
|
state.running = false;
|
|
@@ -337,10 +514,20 @@ const handler = {
|
|
|
337
514
|
// ============================================================================
|
|
338
515
|
const sandboxDesc = sandbox.type === "host"
|
|
339
516
|
? "host"
|
|
340
|
-
: sandbox.type === "
|
|
341
|
-
? `
|
|
342
|
-
:
|
|
517
|
+
: sandbox.type === "container"
|
|
518
|
+
? `container:${sandbox.container}`
|
|
519
|
+
: sandbox.type === "image"
|
|
520
|
+
? `image:${sandbox.image}`
|
|
521
|
+
: `firecracker:${sandbox.vmId}`;
|
|
343
522
|
log.logStartup(workingDir, sandboxDesc);
|
|
523
|
+
// Start link callback server if port is configured
|
|
524
|
+
if (MOM_LINK_PORT) {
|
|
525
|
+
startLinkServer(MOM_LINK_PORT, linkTokenStore, vaultManager, async (platform, conversationId, msg) => {
|
|
526
|
+
const bot = botsByPlatform[platform];
|
|
527
|
+
if (bot)
|
|
528
|
+
await bot.postMessage(conversationId, msg);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
344
531
|
// Create platform bots
|
|
345
532
|
const bots = [];
|
|
346
533
|
const botsByPlatform = {};
|