@geminixiang/mama 0.2.0-beta.0 → 0.2.0-beta.2
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 +94 -27
- package/dist/adapter.d.ts +9 -5
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +9 -6
- 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 +16 -13
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +10 -2
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +196 -32
- 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 +24 -17
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +2 -0
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +109 -29
- 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 +8 -43
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/adapters/telegram/html.d.ts +3 -0
- package/dist/adapters/telegram/html.d.ts.map +1 -0
- package/dist/adapters/telegram/html.js +98 -0
- package/dist/adapters/telegram/html.js.map +1 -0
- package/dist/agent.d.ts +4 -9
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +141 -92
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +44 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +74 -0
- package/dist/bindings.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +53 -12
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +7 -7
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +9 -9
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +14 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +45 -10
- 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 +49 -0
- package/dist/execution-resolver.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +2 -1
- package/dist/instrument.js.map +1 -1
- package/dist/link-server.d.ts +17 -0
- package/dist/link-server.d.ts.map +1 -0
- package/dist/link-server.js +899 -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 +226 -55
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +52 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +291 -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 +10 -0
- package/dist/sandbox/host.d.ts.map +1 -0
- package/dist/sandbox/host.js +85 -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 +4 -2
- package/dist/sentry.js.map +1 -1
- package/dist/session-store.d.ts +2 -6
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +3 -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 +22 -0
- package/dist/tools/event.d.ts.map +1 -0
- package/dist/tools/event.js +104 -0
- package/dist/tools/event.js.map +1 -0
- package/dist/tools/index.d.ts +7 -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 +12 -0
- package/dist/ui-copy.d.ts.map +1 -0
- package/dist/ui-copy.js +36 -0
- package/dist/ui-copy.js.map +1 -0
- package/dist/vault-routing.d.ts +9 -0
- package/dist/vault-routing.d.ts.map +1 -0
- package/dist/vault-routing.js +52 -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/package.json +12 -11
package/dist/main.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./instrument.js";
|
|
3
3
|
import { join, resolve } from "path";
|
|
4
|
-
import { readFileSync } from "fs";
|
|
4
|
+
import { mkdirSync, readFileSync, statSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
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 { FileVaultManager } from "./vault.js";
|
|
23
|
+
import { createManagedVaultEntry, ensureSandboxVaultEntry, resolveActorVaultKey, } from "./vault-routing.js";
|
|
16
24
|
import { addLifecycleBreadcrumb, applyRunScope } from "./sentry.js";
|
|
17
25
|
import { ChannelStore } from "./store.js";
|
|
26
|
+
import { formatNothingRunning, formatStopped, formatStopping } from "./ui-copy.js";
|
|
18
27
|
import * as Sentry from "@sentry/node";
|
|
19
28
|
// ============================================================================
|
|
20
29
|
// Config
|
|
@@ -43,10 +52,17 @@ const MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;
|
|
|
43
52
|
const MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;
|
|
44
53
|
const MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;
|
|
45
54
|
const MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;
|
|
55
|
+
const MOM_LINK_URL = process.env.MOM_LINK_URL;
|
|
56
|
+
const MOM_LINK_PORT = process.env.MOM_LINK_PORT
|
|
57
|
+
? parseInt(process.env.MOM_LINK_PORT, 10)
|
|
58
|
+
: MOM_LINK_URL
|
|
59
|
+
? 8181
|
|
60
|
+
: undefined;
|
|
46
61
|
function parseArgs() {
|
|
47
62
|
const args = process.argv.slice(2);
|
|
48
63
|
let sandbox = { type: "host" };
|
|
49
64
|
let workingDir;
|
|
65
|
+
let stateDirArg;
|
|
50
66
|
let downloadChannelId;
|
|
51
67
|
let showVersion = false;
|
|
52
68
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -60,6 +76,12 @@ function parseArgs() {
|
|
|
60
76
|
else if (arg === "--sandbox") {
|
|
61
77
|
sandbox = parseSandboxArg(args[++i] || "");
|
|
62
78
|
}
|
|
79
|
+
else if (arg.startsWith("--state-dir=")) {
|
|
80
|
+
stateDirArg = arg.slice("--state-dir=".length);
|
|
81
|
+
}
|
|
82
|
+
else if (arg === "--state-dir") {
|
|
83
|
+
stateDirArg = args[++i];
|
|
84
|
+
}
|
|
63
85
|
else if (arg.startsWith("--download=")) {
|
|
64
86
|
downloadChannelId = arg.slice("--download=".length);
|
|
65
87
|
}
|
|
@@ -72,12 +94,60 @@ function parseArgs() {
|
|
|
72
94
|
}
|
|
73
95
|
return {
|
|
74
96
|
workingDir: workingDir ? resolve(workingDir) : undefined,
|
|
97
|
+
stateDir: stateDirArg ? resolve(stateDirArg) : undefined,
|
|
75
98
|
sandbox,
|
|
76
99
|
downloadChannel: downloadChannelId,
|
|
77
100
|
showVersion,
|
|
78
101
|
};
|
|
79
102
|
}
|
|
80
|
-
const
|
|
103
|
+
const WORLD_WRITABLE_MODE = 0o002;
|
|
104
|
+
function ensureSecureStateDir(path) {
|
|
105
|
+
let stat;
|
|
106
|
+
try {
|
|
107
|
+
stat = statSync(path);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const code = err.code;
|
|
111
|
+
if (code === "ENOENT") {
|
|
112
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.error(`Error: cannot access --state-dir ${path}: ${err.message}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
if (!stat.isDirectory()) {
|
|
119
|
+
console.error(`Error: --state-dir ${path} exists but is not a directory`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
if (stat.mode & WORLD_WRITABLE_MODE) {
|
|
123
|
+
console.error(`Error: --state-dir ${path} is world-writable (mode ${(stat.mode & 0o777).toString(8)}). ` +
|
|
124
|
+
`Credentials stored there would be exposed to other local users. ` +
|
|
125
|
+
`Fix with: chmod 0700 ${path}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const euid = typeof process.geteuid === "function" ? process.geteuid() : undefined;
|
|
129
|
+
if (euid !== undefined && stat.uid !== euid) {
|
|
130
|
+
console.error(`Error: --state-dir ${path} is owned by uid ${stat.uid} but mama is running as uid ${euid}. ` +
|
|
131
|
+
`Run mama as the directory owner or point --state-dir at a directory you own.`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function handleStartupError(error) {
|
|
136
|
+
if (error instanceof SandboxError) {
|
|
137
|
+
for (const line of error.formatForCli()) {
|
|
138
|
+
console.error(line);
|
|
139
|
+
}
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
let parsedArgs;
|
|
145
|
+
try {
|
|
146
|
+
parsedArgs = parseArgs();
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
handleStartupError(error);
|
|
150
|
+
}
|
|
81
151
|
// Handle --version
|
|
82
152
|
if (parsedArgs.showVersion) {
|
|
83
153
|
console.log(getVersion());
|
|
@@ -94,11 +164,14 @@ if (parsedArgs.downloadChannel) {
|
|
|
94
164
|
}
|
|
95
165
|
// Normal bot mode - require working dir
|
|
96
166
|
if (!parsedArgs.workingDir) {
|
|
97
|
-
console.error("Usage: mama [--sandbox=host|
|
|
167
|
+
console.error("Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>] <working-directory>");
|
|
98
168
|
console.error(" mama --download <channel-id>");
|
|
99
169
|
process.exit(1);
|
|
100
170
|
}
|
|
101
171
|
const { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };
|
|
172
|
+
const stateDir = parsedArgs.stateDir ?? join(homedir(), ".mama");
|
|
173
|
+
process.env.MAMA_STATE_DIR = stateDir;
|
|
174
|
+
ensureSecureStateDir(stateDir);
|
|
102
175
|
// Validate platform tokens
|
|
103
176
|
const hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);
|
|
104
177
|
const hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;
|
|
@@ -110,8 +183,86 @@ if (!hasSlack && !hasTelegram && !hasDiscord) {
|
|
|
110
183
|
" Discord: MOM_DISCORD_BOT_TOKEN");
|
|
111
184
|
process.exit(1);
|
|
112
185
|
}
|
|
113
|
-
|
|
114
|
-
|
|
186
|
+
try {
|
|
187
|
+
await validateSandbox(sandbox);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
handleStartupError(error);
|
|
191
|
+
}
|
|
192
|
+
const vaultManager = new FileVaultManager(stateDir);
|
|
193
|
+
if (vaultManager.isEnabled()) {
|
|
194
|
+
console.log(sandbox.type === "container"
|
|
195
|
+
? " Vault system enabled. Container vault active."
|
|
196
|
+
: sandbox.type === "image" || sandbox.type === "firecracker"
|
|
197
|
+
? " Vault system enabled. Per-user credential routing active."
|
|
198
|
+
: " Vault system enabled. Host mode will not inject vault env.");
|
|
199
|
+
}
|
|
200
|
+
const bindingStore = new FileUserBindingStore(stateDir);
|
|
201
|
+
if (bindingStore.isEnabled()) {
|
|
202
|
+
console.log(sandbox.type === "container"
|
|
203
|
+
? " Binding store enabled. Container mode uses the container vault."
|
|
204
|
+
: sandbox.type === "image" || sandbox.type === "firecracker"
|
|
205
|
+
? " Binding store enabled. Platform user → vault routing active."
|
|
206
|
+
: " Binding store enabled. Host mode will not inject vault env.");
|
|
207
|
+
}
|
|
208
|
+
const provisioner = sandbox.type === "image" ? new DockerContainerManager(sandbox.image, workingDir) : undefined;
|
|
209
|
+
const linkTokenStore = new InMemoryLinkTokenStore();
|
|
210
|
+
setInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();
|
|
211
|
+
function normalizeLoginBaseUrl() {
|
|
212
|
+
if (MOM_LINK_URL) {
|
|
213
|
+
return MOM_LINK_URL.replace(/\/+$/, "");
|
|
214
|
+
}
|
|
215
|
+
if (MOM_LINK_PORT) {
|
|
216
|
+
return `http://localhost:${MOM_LINK_PORT}`;
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
function isPrivateConversation(event) {
|
|
221
|
+
return (event.conversationKind === "direct" ||
|
|
222
|
+
event.type === "dm" ||
|
|
223
|
+
event.sessionKey === event.conversationId);
|
|
224
|
+
}
|
|
225
|
+
function ensureLoginVault(platform, platformUserId) {
|
|
226
|
+
const vaultId = resolveActorVaultKey(sandbox, vaultManager, bindingStore, platform, platformUserId);
|
|
227
|
+
ensureSandboxVaultEntry(sandbox, vaultManager, platform, platformUserId, vaultId);
|
|
228
|
+
if (sandbox.type !== "container" && sandbox.type !== "image") {
|
|
229
|
+
vaultManager.addEntry(vaultId, createManagedVaultEntry(platform, platformUserId, vaultId));
|
|
230
|
+
}
|
|
231
|
+
return vaultId;
|
|
232
|
+
}
|
|
233
|
+
async function replyWithContext(responseCtx, text) {
|
|
234
|
+
await responseCtx.setTyping(false);
|
|
235
|
+
await responseCtx.setWorking(false);
|
|
236
|
+
await responseCtx.respond(text);
|
|
237
|
+
}
|
|
238
|
+
async function handleLoginCommand(platform, platformUserId, conversationId, responseCtx, commandText, privateConversation) {
|
|
239
|
+
const parsed = parseLoginCommand(commandText);
|
|
240
|
+
if (!parsed)
|
|
241
|
+
return false;
|
|
242
|
+
if (!privateConversation) {
|
|
243
|
+
await replyWithContext(responseCtx, "為了保護你的憑證,`/login` 只能在與機器人的私訊中使用。請先私訊機器人,再重新執行 `/login`。");
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
const baseUrl = normalizeLoginBaseUrl();
|
|
247
|
+
if (!baseUrl) {
|
|
248
|
+
await replyWithContext(responseCtx, "Login is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.");
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
let vaultId;
|
|
252
|
+
try {
|
|
253
|
+
vaultId = ensureLoginVault(platform, platformUserId);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
log.logWarning(`[${conversationId}] Failed to prepare login vault for ${platform}/${platformUserId}`, error instanceof Error ? error.message : String(error));
|
|
257
|
+
await replyWithContext(responseCtx, "Login setup failed on the server. 請稍後重試,或聯絡管理員檢查 vault 儲存權限。");
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
const token = linkTokenStore.create(platform, platformUserId, conversationId, vaultId, "");
|
|
261
|
+
const vaultLabel = sandbox.type === "container" ? `container vault (${vaultId})` : "your vault";
|
|
262
|
+
await replyWithContext(responseCtx, `Open this link to store credentials in ${vaultLabel} (expires in 15 minutes):\n${baseUrl}/link?token=${token.token}`);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
const conversationStates = new Map();
|
|
115
266
|
/** Track in-flight runs for graceful shutdown */
|
|
116
267
|
const inFlightRuns = new Set();
|
|
117
268
|
/** Flag to stop accepting new events during shutdown */
|
|
@@ -120,18 +271,25 @@ let isShuttingDown = false;
|
|
|
120
271
|
const MAX_SESSIONS = 500;
|
|
121
272
|
/** Idle timeout before a non-running session can be evicted (1 hour) */
|
|
122
273
|
const IDLE_TIMEOUT_MS = 3600000;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
274
|
+
/** Idle timeout for managed image containers (10 minutes) */
|
|
275
|
+
const IMAGE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
|
276
|
+
if (provisioner) {
|
|
277
|
+
await provisioner.reconcile();
|
|
278
|
+
await provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS);
|
|
279
|
+
setInterval(() => provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS), IMAGE_IDLE_TIMEOUT_MS).unref();
|
|
280
|
+
}
|
|
281
|
+
async function getState(conversationId, sessionKey) {
|
|
282
|
+
const key = sessionKey ?? conversationId;
|
|
283
|
+
let state = conversationStates.get(key);
|
|
126
284
|
if (!state) {
|
|
127
|
-
const
|
|
285
|
+
const conversationDir = join(workingDir, conversationId);
|
|
128
286
|
state = {
|
|
129
287
|
running: false,
|
|
130
|
-
runner: await createRunner(sandbox, key,
|
|
288
|
+
runner: await createRunner(sandbox, key, conversationId, conversationDir, workingDir, vaultManager, bindingStore, provisioner),
|
|
131
289
|
stopRequested: false,
|
|
132
290
|
lastAccessedAt: Date.now(),
|
|
133
291
|
};
|
|
134
|
-
|
|
292
|
+
conversationStates.set(key, state);
|
|
135
293
|
}
|
|
136
294
|
else {
|
|
137
295
|
state.lastAccessedAt = Date.now();
|
|
@@ -139,27 +297,27 @@ async function getState(channelId, sessionKey) {
|
|
|
139
297
|
return state;
|
|
140
298
|
}
|
|
141
299
|
/**
|
|
142
|
-
* Evict idle sessions from
|
|
300
|
+
* Evict idle sessions from conversationStates to bound memory usage.
|
|
143
301
|
* Called after each handleEvent completes.
|
|
144
302
|
*/
|
|
145
303
|
function evictIdleSessions() {
|
|
146
304
|
const now = Date.now();
|
|
147
|
-
for (const [key, state] of
|
|
305
|
+
for (const [key, state] of conversationStates) {
|
|
148
306
|
if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {
|
|
149
|
-
|
|
307
|
+
conversationStates.delete(key);
|
|
150
308
|
}
|
|
151
309
|
}
|
|
152
|
-
if (
|
|
310
|
+
if (conversationStates.size > MAX_SESSIONS) {
|
|
153
311
|
const idleSessions = [];
|
|
154
|
-
for (const [key, state] of
|
|
312
|
+
for (const [key, state] of conversationStates) {
|
|
155
313
|
if (!state.running) {
|
|
156
314
|
idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });
|
|
157
315
|
}
|
|
158
316
|
}
|
|
159
317
|
idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
|
|
160
|
-
const toEvict =
|
|
318
|
+
const toEvict = conversationStates.size - MAX_SESSIONS;
|
|
161
319
|
for (let i = 0; i < toEvict && i < idleSessions.length; i++) {
|
|
162
|
-
|
|
320
|
+
conversationStates.delete(idleSessions[i].key);
|
|
163
321
|
}
|
|
164
322
|
}
|
|
165
323
|
}
|
|
@@ -168,12 +326,12 @@ function evictIdleSessions() {
|
|
|
168
326
|
// ============================================================================
|
|
169
327
|
const handler = {
|
|
170
328
|
isRunning(sessionKey) {
|
|
171
|
-
const state =
|
|
329
|
+
const state = conversationStates.get(sessionKey);
|
|
172
330
|
return !!state?.running;
|
|
173
331
|
},
|
|
174
332
|
getRunningSessions() {
|
|
175
333
|
const sessions = [];
|
|
176
|
-
for (const [sessionKey, state] of
|
|
334
|
+
for (const [sessionKey, state] of conversationStates) {
|
|
177
335
|
if (state.running && state.startedAt) {
|
|
178
336
|
// Get current step from runner
|
|
179
337
|
const currentStep = state.runner.getCurrentStep();
|
|
@@ -187,20 +345,20 @@ const handler = {
|
|
|
187
345
|
}
|
|
188
346
|
return sessions;
|
|
189
347
|
},
|
|
190
|
-
async handleStop(sessionKey,
|
|
191
|
-
const state =
|
|
348
|
+
async handleStop(sessionKey, conversationId, bot) {
|
|
349
|
+
const state = conversationStates.get(sessionKey);
|
|
192
350
|
if (state?.running) {
|
|
193
351
|
state.stopRequested = true;
|
|
194
352
|
state.runner.abort();
|
|
195
|
-
const ts = await bot.postMessage(
|
|
353
|
+
const ts = await bot.postMessage(conversationId, formatStopping(bot));
|
|
196
354
|
state.stopMessageTs = ts;
|
|
197
355
|
}
|
|
198
356
|
else {
|
|
199
|
-
await bot.postMessage(
|
|
357
|
+
await bot.postMessage(conversationId, formatNothingRunning(bot));
|
|
200
358
|
}
|
|
201
359
|
},
|
|
202
360
|
forceStop(sessionKey) {
|
|
203
|
-
const state =
|
|
361
|
+
const state = conversationStates.get(sessionKey);
|
|
204
362
|
if (state?.running) {
|
|
205
363
|
log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);
|
|
206
364
|
state.stopRequested = true;
|
|
@@ -208,49 +366,53 @@ const handler = {
|
|
|
208
366
|
state.running = false;
|
|
209
367
|
}
|
|
210
368
|
},
|
|
211
|
-
async handleNew(sessionKey,
|
|
212
|
-
const state =
|
|
369
|
+
async handleNew(sessionKey, conversationId, bot) {
|
|
370
|
+
const state = conversationStates.get(sessionKey);
|
|
213
371
|
if (state?.running) {
|
|
214
372
|
state.stopRequested = true;
|
|
215
373
|
state.runner.abort();
|
|
216
374
|
}
|
|
217
|
-
//
|
|
218
|
-
const
|
|
375
|
+
// Conversation sessions rotate via current pointer. Thread sessions reset in place.
|
|
376
|
+
const conversationDir = join(workingDir, conversationId);
|
|
219
377
|
if (sessionKey.includes(":")) {
|
|
220
|
-
createManagedSessionFileAtPath(getThreadSessionFile(
|
|
378
|
+
createManagedSessionFileAtPath(getThreadSessionFile(conversationDir, sessionKey), conversationDir);
|
|
221
379
|
}
|
|
222
380
|
else {
|
|
223
|
-
createManagedSessionFile(
|
|
381
|
+
createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);
|
|
224
382
|
}
|
|
225
383
|
// Remove from in-memory cache
|
|
226
|
-
|
|
227
|
-
log.logInfo(`[${
|
|
228
|
-
await bot.postMessage(
|
|
384
|
+
conversationStates.delete(sessionKey);
|
|
385
|
+
log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);
|
|
386
|
+
await bot.postMessage(conversationId, "Conversation reset. Send a new message to start fresh.");
|
|
229
387
|
},
|
|
230
388
|
async handleEvent(event, bot, adapters, _isEvent) {
|
|
389
|
+
const conversationId = event.conversationId;
|
|
231
390
|
// Don't accept new events during shutdown
|
|
232
391
|
if (isShuttingDown) {
|
|
233
|
-
log.logInfo(`[${
|
|
392
|
+
log.logInfo(`[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`);
|
|
234
393
|
return;
|
|
235
394
|
}
|
|
236
|
-
const sessionKey = event.sessionKey ?? `${
|
|
237
|
-
const
|
|
395
|
+
const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;
|
|
396
|
+
const handledLogin = await handleLoginCommand(adapters.platform.name, event.user, conversationId, adapters.responseCtx, event.text, isPrivateConversation(event));
|
|
397
|
+
if (handledLogin)
|
|
398
|
+
return;
|
|
399
|
+
const state = await getState(conversationId, sessionKey);
|
|
238
400
|
// Start run
|
|
239
401
|
state.running = true;
|
|
240
402
|
state.stopRequested = false;
|
|
241
403
|
state.startedAt = Date.now();
|
|
242
404
|
state.lastActivityAt = Date.now();
|
|
243
|
-
log.logInfo(`[${
|
|
405
|
+
log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);
|
|
244
406
|
// Wrap in-flight run tracking
|
|
245
407
|
Sentry.metrics.count("agent.run.started", 1, {
|
|
246
|
-
attributes: { channel:
|
|
408
|
+
attributes: { channel: conversationId },
|
|
247
409
|
});
|
|
248
410
|
Sentry.metrics.gauge("agent.sessions.active", inFlightRuns.size + 1);
|
|
249
|
-
const runPromise = Sentry.startSpan({ name: "agent.run", op: "agent", attributes: {
|
|
411
|
+
const runPromise = Sentry.startSpan({ name: "agent.run", op: "agent", attributes: { conversationId, sessionKey } }, async () => {
|
|
250
412
|
return Sentry.withScope(async (scope) => {
|
|
251
413
|
const { message, responseCtx, platform } = adapters;
|
|
252
414
|
applyRunScope(scope, {
|
|
253
|
-
|
|
415
|
+
conversationId,
|
|
254
416
|
sessionKey,
|
|
255
417
|
messageId: message.id,
|
|
256
418
|
platform: platform.name,
|
|
@@ -260,7 +422,7 @@ const handler = {
|
|
|
260
422
|
isEvent: _isEvent,
|
|
261
423
|
});
|
|
262
424
|
addLifecycleBreadcrumb("agent.run.started", {
|
|
263
|
-
channel_id:
|
|
425
|
+
channel_id: conversationId,
|
|
264
426
|
platform: platform.name,
|
|
265
427
|
has_attachments: (message.attachments?.length ?? 0) > 0,
|
|
266
428
|
});
|
|
@@ -273,37 +435,37 @@ const handler = {
|
|
|
273
435
|
Sentry.metrics.distribution("agent.run.duration", durationMs, {
|
|
274
436
|
unit: "millisecond",
|
|
275
437
|
attributes: {
|
|
276
|
-
channel:
|
|
438
|
+
channel: conversationId,
|
|
277
439
|
platform: platform.name,
|
|
278
440
|
stop_reason: result.stopReason,
|
|
279
441
|
},
|
|
280
442
|
});
|
|
281
443
|
Sentry.metrics.count("agent.run.completed", 1, {
|
|
282
444
|
attributes: {
|
|
283
|
-
channel:
|
|
445
|
+
channel: conversationId,
|
|
284
446
|
platform: platform.name,
|
|
285
447
|
stop_reason: result.stopReason,
|
|
286
448
|
},
|
|
287
449
|
});
|
|
288
450
|
addLifecycleBreadcrumb("agent.run.completed", {
|
|
289
|
-
channel_id:
|
|
451
|
+
channel_id: conversationId,
|
|
290
452
|
platform: platform.name,
|
|
291
453
|
stop_reason: result.stopReason,
|
|
292
454
|
duration_ms: durationMs,
|
|
293
455
|
});
|
|
294
456
|
if (result.stopReason === "aborted" && state.stopRequested) {
|
|
295
457
|
if (state.stopMessageTs) {
|
|
296
|
-
await bot.updateMessage(
|
|
458
|
+
await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));
|
|
297
459
|
state.stopMessageTs = undefined;
|
|
298
460
|
}
|
|
299
461
|
else {
|
|
300
|
-
await bot.postMessage(
|
|
462
|
+
await bot.postMessage(conversationId, formatStopped(bot));
|
|
301
463
|
}
|
|
302
464
|
}
|
|
303
465
|
}
|
|
304
466
|
catch (err) {
|
|
305
467
|
scope.setContext("agent_run_error", {
|
|
306
|
-
|
|
468
|
+
conversationId,
|
|
307
469
|
sessionKey,
|
|
308
470
|
platform: adapters.platform.name,
|
|
309
471
|
messageId: adapters.message.id,
|
|
@@ -311,9 +473,9 @@ const handler = {
|
|
|
311
473
|
});
|
|
312
474
|
Sentry.captureException(err);
|
|
313
475
|
Sentry.metrics.count("agent.run.errors", 1, {
|
|
314
|
-
attributes: { channel:
|
|
476
|
+
attributes: { channel: conversationId, platform: adapters.platform.name },
|
|
315
477
|
});
|
|
316
|
-
log.logWarning(`[${
|
|
478
|
+
log.logWarning(`[${conversationId}] Run error`, err instanceof Error ? err.message : String(err));
|
|
317
479
|
}
|
|
318
480
|
finally {
|
|
319
481
|
state.running = false;
|
|
@@ -337,9 +499,11 @@ const handler = {
|
|
|
337
499
|
// ============================================================================
|
|
338
500
|
const sandboxDesc = sandbox.type === "host"
|
|
339
501
|
? "host"
|
|
340
|
-
: sandbox.type === "
|
|
341
|
-
? `
|
|
342
|
-
:
|
|
502
|
+
: sandbox.type === "container"
|
|
503
|
+
? `container:${sandbox.container}`
|
|
504
|
+
: sandbox.type === "image"
|
|
505
|
+
? `image:${sandbox.image}`
|
|
506
|
+
: `firecracker:${sandbox.vmId}`;
|
|
343
507
|
log.logStartup(workingDir, sandboxDesc);
|
|
344
508
|
// Create platform bots
|
|
345
509
|
const bots = [];
|
|
@@ -374,6 +538,13 @@ if (hasDiscord) {
|
|
|
374
538
|
botsByPlatform.discord = discordBot;
|
|
375
539
|
log.logInfo("Platform: Discord");
|
|
376
540
|
}
|
|
541
|
+
if (MOM_LINK_PORT) {
|
|
542
|
+
startLinkServer(MOM_LINK_PORT, linkTokenStore, vaultManager, async (platform, conversationId, message) => {
|
|
543
|
+
const bot = botsByPlatform[platform];
|
|
544
|
+
if (bot)
|
|
545
|
+
await bot.postMessage(conversationId, message);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
377
548
|
// Start events watcher with explicit platform routing
|
|
378
549
|
const eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);
|
|
379
550
|
const slackBot = botsByPlatform.slack;
|