@geminixiang/mama 0.1.10 → 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 +80 -23
- package/dist/adapter.d.ts +11 -9
- 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 +13 -4
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +98 -43
- 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 +25 -20
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +4 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +143 -58
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +124 -29
- 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 +303 -89
- 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 +34 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +98 -38
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -6
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +23 -14
- 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 +2 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +14 -0
- package/dist/instrument.js.map +1 -0
- 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 +3 -2
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +10 -9
- 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 +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +322 -82
- 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 +31 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +205 -0
- package/dist/sentry.js.map +1 -0
- package/dist/session-store.d.ts +72 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +186 -0
- package/dist/session-store.js.map +1 -0
- 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 +13 -11
package/dist/main.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import "./instrument.js";
|
|
2
3
|
import { join, resolve } from "path";
|
|
3
|
-
import {
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { mkdirSync, readFileSync, statSync } from "fs";
|
|
4
6
|
import { fileURLToPath } from "url";
|
|
5
7
|
import { dirname, join as pathJoin } from "path";
|
|
6
8
|
import { DiscordBot } from "./adapters/discord/index.js";
|
|
7
9
|
import { TelegramBot } from "./adapters/telegram/index.js";
|
|
8
10
|
import { SlackBot as SlackBotClass } from "./adapters/slack/index.js";
|
|
9
11
|
import { createRunner } from "./agent.js";
|
|
12
|
+
import { createManagedSessionFile, createManagedSessionFileAtPath, getChannelSessionDir, getThreadSessionFile, } from "./session-store.js";
|
|
10
13
|
import { downloadChannel } from "./download.js";
|
|
11
14
|
import { createEventsWatcher } from "./events.js";
|
|
12
15
|
import * as log from "./log.js";
|
|
13
|
-
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";
|
|
26
|
+
import { addLifecycleBreadcrumb, applyRunScope } from "./sentry.js";
|
|
14
27
|
import { ChannelStore } from "./store.js";
|
|
28
|
+
import * as Sentry from "@sentry/node";
|
|
15
29
|
// ============================================================================
|
|
16
30
|
// Config
|
|
17
31
|
// ============================================================================
|
|
@@ -39,10 +53,23 @@ const MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;
|
|
|
39
53
|
const MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;
|
|
40
54
|
const MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;
|
|
41
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;
|
|
42
68
|
function parseArgs() {
|
|
43
69
|
const args = process.argv.slice(2);
|
|
44
70
|
let sandbox = { type: "host" };
|
|
45
71
|
let workingDir;
|
|
72
|
+
let stateDirArg;
|
|
46
73
|
let downloadChannelId;
|
|
47
74
|
let showVersion = false;
|
|
48
75
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -56,6 +83,12 @@ function parseArgs() {
|
|
|
56
83
|
else if (arg === "--sandbox") {
|
|
57
84
|
sandbox = parseSandboxArg(args[++i] || "");
|
|
58
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
|
+
}
|
|
59
92
|
else if (arg.startsWith("--download=")) {
|
|
60
93
|
downloadChannelId = arg.slice("--download=".length);
|
|
61
94
|
}
|
|
@@ -68,12 +101,66 @@ function parseArgs() {
|
|
|
68
101
|
}
|
|
69
102
|
return {
|
|
70
103
|
workingDir: workingDir ? resolve(workingDir) : undefined,
|
|
104
|
+
stateDir: stateDirArg ? resolve(stateDirArg) : undefined,
|
|
71
105
|
sandbox,
|
|
72
106
|
downloadChannel: downloadChannelId,
|
|
73
107
|
showVersion,
|
|
74
108
|
};
|
|
75
109
|
}
|
|
76
|
-
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
|
+
}
|
|
77
164
|
// Handle --version
|
|
78
165
|
if (parsedArgs.showVersion) {
|
|
79
166
|
console.log(getVersion());
|
|
@@ -90,11 +177,27 @@ if (parsedArgs.downloadChannel) {
|
|
|
90
177
|
}
|
|
91
178
|
// Normal bot mode - require working dir
|
|
92
179
|
if (!parsedArgs.workingDir) {
|
|
93
|
-
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>");
|
|
94
181
|
console.error(" mama --download <channel-id>");
|
|
95
182
|
process.exit(1);
|
|
96
183
|
}
|
|
97
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
|
+
}
|
|
98
201
|
// Validate platform tokens
|
|
99
202
|
const hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);
|
|
100
203
|
const hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;
|
|
@@ -106,36 +209,74 @@ if (!hasSlack && !hasTelegram && !hasDiscord) {
|
|
|
106
209
|
" Discord: MOM_DISCORD_BOT_TOKEN");
|
|
107
210
|
process.exit(1);
|
|
108
211
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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();
|
|
119
235
|
/** Track in-flight runs for graceful shutdown */
|
|
120
236
|
const inFlightRuns = new Set();
|
|
121
237
|
/** Flag to stop accepting new events during shutdown */
|
|
122
238
|
let isShuttingDown = false;
|
|
123
239
|
/** Maximum number of cached sessions */
|
|
124
240
|
const MAX_SESSIONS = 500;
|
|
125
|
-
/** Idle timeout before a non-running session can be evicted (
|
|
126
|
-
const IDLE_TIMEOUT_MS =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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);
|
|
130
271
|
if (!state) {
|
|
131
|
-
const
|
|
272
|
+
const conversationDir = join(workingDir, conversationId);
|
|
132
273
|
state = {
|
|
133
274
|
running: false,
|
|
134
|
-
runner: await createRunner(sandbox, key,
|
|
275
|
+
runner: await createRunner(sandbox, key, conversationId, conversationDir, workingDir, vaultManager, bindingStore, provisioner, stateDir),
|
|
135
276
|
stopRequested: false,
|
|
136
277
|
lastAccessedAt: Date.now(),
|
|
137
278
|
};
|
|
138
|
-
|
|
279
|
+
conversationStates.set(key, state);
|
|
139
280
|
}
|
|
140
281
|
else {
|
|
141
282
|
state.lastAccessedAt = Date.now();
|
|
@@ -143,37 +284,27 @@ async function getState(channelId, sessionKey) {
|
|
|
143
284
|
return state;
|
|
144
285
|
}
|
|
145
286
|
/**
|
|
146
|
-
* Evict idle sessions from
|
|
287
|
+
* Evict idle sessions from conversationStates to bound memory usage.
|
|
147
288
|
* Called after each handleEvent completes.
|
|
148
289
|
*/
|
|
149
290
|
function evictIdleSessions() {
|
|
150
291
|
const now = Date.now();
|
|
151
|
-
for (const [key, state] of
|
|
292
|
+
for (const [key, state] of conversationStates) {
|
|
152
293
|
if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {
|
|
153
|
-
|
|
154
|
-
// Clean up aliases pointing to this session
|
|
155
|
-
for (const [alias, target] of threadAliases) {
|
|
156
|
-
if (target === key)
|
|
157
|
-
threadAliases.delete(alias);
|
|
158
|
-
}
|
|
294
|
+
conversationStates.delete(key);
|
|
159
295
|
}
|
|
160
296
|
}
|
|
161
|
-
if (
|
|
297
|
+
if (conversationStates.size > MAX_SESSIONS) {
|
|
162
298
|
const idleSessions = [];
|
|
163
|
-
for (const [key, state] of
|
|
299
|
+
for (const [key, state] of conversationStates) {
|
|
164
300
|
if (!state.running) {
|
|
165
301
|
idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });
|
|
166
302
|
}
|
|
167
303
|
}
|
|
168
304
|
idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
|
|
169
|
-
const toEvict =
|
|
305
|
+
const toEvict = conversationStates.size - MAX_SESSIONS;
|
|
170
306
|
for (let i = 0; i < toEvict && i < idleSessions.length; i++) {
|
|
171
|
-
|
|
172
|
-
channelStates.delete(evictedKey);
|
|
173
|
-
for (const [alias, target] of threadAliases) {
|
|
174
|
-
if (target === evictedKey)
|
|
175
|
-
threadAliases.delete(alias);
|
|
176
|
-
}
|
|
307
|
+
conversationStates.delete(idleSessions[i].key);
|
|
177
308
|
}
|
|
178
309
|
}
|
|
179
310
|
}
|
|
@@ -182,12 +313,12 @@ function evictIdleSessions() {
|
|
|
182
313
|
// ============================================================================
|
|
183
314
|
const handler = {
|
|
184
315
|
isRunning(sessionKey) {
|
|
185
|
-
const state =
|
|
316
|
+
const state = conversationStates.get(sessionKey);
|
|
186
317
|
return !!state?.running;
|
|
187
318
|
},
|
|
188
319
|
getRunningSessions() {
|
|
189
320
|
const sessions = [];
|
|
190
|
-
for (const [sessionKey, state] of
|
|
321
|
+
for (const [sessionKey, state] of conversationStates) {
|
|
191
322
|
if (state.running && state.startedAt) {
|
|
192
323
|
// Get current step from runner
|
|
193
324
|
const currentStep = state.runner.getCurrentStep();
|
|
@@ -201,20 +332,20 @@ const handler = {
|
|
|
201
332
|
}
|
|
202
333
|
return sessions;
|
|
203
334
|
},
|
|
204
|
-
async handleStop(sessionKey,
|
|
205
|
-
const state =
|
|
335
|
+
async handleStop(sessionKey, conversationId, bot) {
|
|
336
|
+
const state = conversationStates.get(sessionKey);
|
|
206
337
|
if (state?.running) {
|
|
207
338
|
state.stopRequested = true;
|
|
208
339
|
state.runner.abort();
|
|
209
|
-
const ts = await bot.postMessage(
|
|
340
|
+
const ts = await bot.postMessage(conversationId, formatStopping(bot));
|
|
210
341
|
state.stopMessageTs = ts;
|
|
211
342
|
}
|
|
212
343
|
else {
|
|
213
|
-
await bot.postMessage(
|
|
344
|
+
await bot.postMessage(conversationId, formatNothingRunning(bot));
|
|
214
345
|
}
|
|
215
346
|
},
|
|
216
347
|
forceStop(sessionKey) {
|
|
217
|
-
const state =
|
|
348
|
+
const state = conversationStates.get(sessionKey);
|
|
218
349
|
if (state?.running) {
|
|
219
350
|
log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);
|
|
220
351
|
state.stopRequested = true;
|
|
@@ -222,55 +353,153 @@ const handler = {
|
|
|
222
353
|
state.running = false;
|
|
223
354
|
}
|
|
224
355
|
},
|
|
225
|
-
|
|
226
|
-
|
|
356
|
+
async handleNew(sessionKey, conversationId, bot) {
|
|
357
|
+
const state = conversationStates.get(sessionKey);
|
|
358
|
+
if (state?.running) {
|
|
359
|
+
state.stopRequested = true;
|
|
360
|
+
state.runner.abort();
|
|
361
|
+
}
|
|
362
|
+
// Channel sessions rotate via current pointer. Thread sessions reset in place.
|
|
363
|
+
const conversationDir = join(workingDir, conversationId);
|
|
364
|
+
if (sessionKey.includes(":")) {
|
|
365
|
+
createManagedSessionFileAtPath(getThreadSessionFile(conversationDir, sessionKey), conversationDir);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);
|
|
369
|
+
}
|
|
370
|
+
// Remove from in-memory cache
|
|
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.");
|
|
227
374
|
},
|
|
228
|
-
|
|
229
|
-
|
|
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}`);
|
|
230
402
|
},
|
|
231
403
|
async handleEvent(event, bot, adapters, _isEvent) {
|
|
232
404
|
// Don't accept new events during shutdown
|
|
233
405
|
if (isShuttingDown) {
|
|
234
|
-
log.logInfo(`[${event.
|
|
406
|
+
log.logInfo(`[${event.conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);
|
|
235
407
|
return;
|
|
236
408
|
}
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const state = await getState(event.channel, sessionKey);
|
|
409
|
+
const sessionKey = event.sessionKey ?? `${event.conversationId}:${event.thread_ts ?? event.ts}`;
|
|
410
|
+
const state = await getState(event.conversationId, sessionKey);
|
|
240
411
|
// Start run
|
|
241
412
|
state.running = true;
|
|
242
413
|
state.stopRequested = false;
|
|
243
414
|
state.startedAt = Date.now();
|
|
244
415
|
state.lastActivityAt = Date.now();
|
|
245
|
-
log.logInfo(`[${event.
|
|
416
|
+
log.logInfo(`[${event.conversationId}] Starting run: ${event.text.substring(0, 50)}`);
|
|
246
417
|
// Wrap in-flight run tracking
|
|
247
|
-
|
|
248
|
-
|
|
418
|
+
Sentry.metrics.count("agent.run.started", 1, {
|
|
419
|
+
attributes: { channel: event.conversationId },
|
|
420
|
+
});
|
|
421
|
+
Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size + 1);
|
|
422
|
+
const runPromise = Sentry.startSpan({
|
|
423
|
+
name: "agent.run",
|
|
424
|
+
op: "agent",
|
|
425
|
+
attributes: { channelId: event.conversationId, sessionKey },
|
|
426
|
+
}, async () => {
|
|
427
|
+
return Sentry.withScope(async (scope) => {
|
|
249
428
|
const { message, responseCtx, platform } = adapters;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
429
|
+
applyRunScope(scope, {
|
|
430
|
+
conversationId: event.conversationId,
|
|
431
|
+
sessionKey,
|
|
432
|
+
messageId: message.id,
|
|
433
|
+
platform: platform.name,
|
|
434
|
+
userId: message.userId,
|
|
435
|
+
userName: message.userName,
|
|
436
|
+
threadTs: message.threadTs,
|
|
437
|
+
isEvent: _isEvent,
|
|
438
|
+
});
|
|
439
|
+
addLifecycleBreadcrumb("agent.run.started", {
|
|
440
|
+
channel_id: event.conversationId,
|
|
441
|
+
platform: platform.name,
|
|
442
|
+
has_attachments: (message.attachments?.length ?? 0) > 0,
|
|
443
|
+
});
|
|
444
|
+
try {
|
|
445
|
+
await responseCtx.setTyping(true);
|
|
446
|
+
await responseCtx.setWorking(true);
|
|
447
|
+
const result = await state.runner.run(message, responseCtx, platform);
|
|
448
|
+
await responseCtx.setWorking(false);
|
|
449
|
+
const durationMs = Date.now() - state.startedAt;
|
|
450
|
+
Sentry.metrics.distribution("agent.run.duration", durationMs, {
|
|
451
|
+
unit: "millisecond",
|
|
452
|
+
attributes: {
|
|
453
|
+
channel: event.conversationId,
|
|
454
|
+
platform: platform.name,
|
|
455
|
+
stop_reason: result.stopReason,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
Sentry.metrics.count("agent.run.completed", 1, {
|
|
459
|
+
attributes: {
|
|
460
|
+
channel: event.conversationId,
|
|
461
|
+
platform: platform.name,
|
|
462
|
+
stop_reason: result.stopReason,
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
addLifecycleBreadcrumb("agent.run.completed", {
|
|
466
|
+
channel_id: event.conversationId,
|
|
467
|
+
platform: platform.name,
|
|
468
|
+
stop_reason: result.stopReason,
|
|
469
|
+
duration_ms: durationMs,
|
|
470
|
+
});
|
|
471
|
+
if (result.stopReason === "aborted" && state.stopRequested) {
|
|
472
|
+
if (state.stopMessageTs) {
|
|
473
|
+
await bot.updateMessage(event.conversationId, state.stopMessageTs, "_Stopped_");
|
|
474
|
+
state.stopMessageTs = undefined;
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
await bot.postMessage(event.conversationId, "_Stopped_");
|
|
478
|
+
}
|
|
262
479
|
}
|
|
263
480
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
481
|
+
catch (err) {
|
|
482
|
+
scope.setContext("agent_run_error", {
|
|
483
|
+
conversationId: event.conversationId,
|
|
484
|
+
sessionKey,
|
|
485
|
+
platform: adapters.platform.name,
|
|
486
|
+
messageId: adapters.message.id,
|
|
487
|
+
threadTs: adapters.message.threadTs,
|
|
488
|
+
});
|
|
489
|
+
Sentry.captureException(err);
|
|
490
|
+
Sentry.metrics.count("agent.run.errors", 1, {
|
|
491
|
+
attributes: { channel: event.conversationId, platform: adapters.platform.name },
|
|
492
|
+
});
|
|
493
|
+
log.logWarning(`[${event.conversationId}] Run error`, err instanceof Error ? err.message : String(err));
|
|
494
|
+
}
|
|
495
|
+
finally {
|
|
496
|
+
state.running = false;
|
|
497
|
+
state.lastAccessedAt = Date.now();
|
|
498
|
+
Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size - 1);
|
|
499
|
+
evictIdleSessions();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
});
|
|
274
503
|
inFlightRuns.add(runPromise);
|
|
275
504
|
try {
|
|
276
505
|
await runPromise;
|
|
@@ -285,10 +514,20 @@ const handler = {
|
|
|
285
514
|
// ============================================================================
|
|
286
515
|
const sandboxDesc = sandbox.type === "host"
|
|
287
516
|
? "host"
|
|
288
|
-
: sandbox.type === "
|
|
289
|
-
? `
|
|
290
|
-
:
|
|
517
|
+
: sandbox.type === "container"
|
|
518
|
+
? `container:${sandbox.container}`
|
|
519
|
+
: sandbox.type === "image"
|
|
520
|
+
? `image:${sandbox.image}`
|
|
521
|
+
: `firecracker:${sandbox.vmId}`;
|
|
291
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
|
+
}
|
|
292
531
|
// Create platform bots
|
|
293
532
|
const bots = [];
|
|
294
533
|
const botsByPlatform = {};
|
|
@@ -343,6 +582,7 @@ async function shutdown() {
|
|
|
343
582
|
log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);
|
|
344
583
|
}
|
|
345
584
|
eventsWatcher.stop();
|
|
585
|
+
await Sentry.close(5000);
|
|
346
586
|
process.exit(0);
|
|
347
587
|
}
|
|
348
588
|
process.on("SIGINT", shutdown);
|