@geminixiang/mama 0.2.0-beta.3 → 0.2.0-beta.4
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 +2 -2
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +58 -72
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/shared.d.ts +48 -0
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/shared.js +111 -0
- package/dist/adapters/shared.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +2 -19
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +49 -185
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +78 -100
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +2 -0
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +3 -2
- package/dist/bindings.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -2
- package/dist/config.js.map +1 -1
- package/dist/fs-atomic.d.ts +10 -0
- package/dist/fs-atomic.d.ts.map +1 -0
- package/dist/fs-atomic.js +45 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +5 -7
- package/dist/main.js.map +1 -1
- package/dist/session-store.d.ts +5 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +14 -9
- package/dist/session-store.js.map +1 -1
- package/dist/session-view/portal.d.ts +2 -0
- package/dist/session-view/portal.d.ts.map +1 -1
- package/dist/session-view/portal.js +35 -6
- 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 +58 -22
- package/dist/session-view/service.js.map +1 -1
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +11 -55
- package/dist/vault.js.map +1 -1
- package/package.json +7 -8
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { closeSync, constants as fsConstants, openSync, renameSync, unlinkSync, writeSync, } from "fs";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import { basename, dirname, join } from "path";
|
|
4
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
5
|
+
/**
|
|
6
|
+
* Write `content` to `targetPath` with mode 0600, even when `targetPath`
|
|
7
|
+
* already exists. Uses O_CREAT|O_EXCL on a temp sibling (so the kernel
|
|
8
|
+
* guarantees permissions at creation, not after a racy chmod) and then
|
|
9
|
+
* rename(2) into place for atomicity. Readers never see a torn write,
|
|
10
|
+
* and a crash mid-write leaves either the old file or a stray .tmp
|
|
11
|
+
* (cleaned by the next attempt or manually) — never a half-written target.
|
|
12
|
+
*/
|
|
13
|
+
export function atomicWritePrivateFile(targetPath, content) {
|
|
14
|
+
const dir = dirname(targetPath);
|
|
15
|
+
const tmpPath = join(dir, `.${basename(targetPath)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
|
|
16
|
+
const fd = openSync(tmpPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, PRIVATE_FILE_MODE);
|
|
17
|
+
try {
|
|
18
|
+
writeSync(fd, content);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
try {
|
|
22
|
+
unlinkSync(tmpPath);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// ignore — original error is more informative
|
|
26
|
+
}
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
closeSync(fd);
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
renameSync(tmpPath, targetPath);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
try {
|
|
37
|
+
unlinkSync(tmpPath);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=fs-atomic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-atomic.js","sourceRoot":"","sources":["../src/fs-atomic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,SAAS,IAAI,WAAW,EACxB,QAAQ,EACR,UAAU,EACV,UAAU,EACV,SAAS,GACV,MAAM,IAAI,CAAC;AACZ,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE/C,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEhC;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,UAAkB,EAAE,OAAe;IACxE,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAChC,MAAM,OAAO,GAAG,IAAI,CAClB,GAAG,EACH,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAChF,CAAC;IACF,MAAM,EAAE,GAAG,QAAQ,CACjB,OAAO,EACP,WAAW,CAAC,QAAQ,GAAG,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,MAAM,EAC/D,iBAAiB,CAClB,CAAC;IACF,IAAI,CAAC;QACH,SAAS,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC;YACH,UAAU,CAAC,OAAO,CAAC,CAAC;QACtB,CAAC;QAAC,MAAM,CAAC;YACP,8CAA8C;QAChD,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;YAAS,CAAC;QACT,SAAS,CAAC,EAAE,CAAC,CAAC;IAChB,CAAC;IACD,IAAI,CAAC;QACH,UAAU,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC;YACH,UAAU,CAAC,OAAO,CAAC,CAAC;QACtB,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC","sourcesContent":["import {\n closeSync,\n constants as fsConstants,\n openSync,\n renameSync,\n unlinkSync,\n writeSync,\n} from \"fs\";\nimport { randomBytes } from \"crypto\";\nimport { basename, dirname, join } from \"path\";\n\nconst PRIVATE_FILE_MODE = 0o600;\n\n/**\n * Write `content` to `targetPath` with mode 0600, even when `targetPath`\n * already exists. Uses O_CREAT|O_EXCL on a temp sibling (so the kernel\n * guarantees permissions at creation, not after a racy chmod) and then\n * rename(2) into place for atomicity. Readers never see a torn write,\n * and a crash mid-write leaves either the old file or a stray .tmp\n * (cleaned by the next attempt or manually) — never a half-written target.\n */\nexport function atomicWritePrivateFile(targetPath: string, content: string): void {\n const dir = dirname(targetPath);\n const tmpPath = join(\n dir,\n `.${basename(targetPath)}.${process.pid}.${randomBytes(8).toString(\"hex\")}.tmp`,\n );\n const fd = openSync(\n tmpPath,\n fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL,\n PRIVATE_FILE_MODE,\n );\n try {\n writeSync(fd, content);\n } catch (err) {\n try {\n unlinkSync(tmpPath);\n } catch {\n // ignore — original error is more informative\n }\n throw err;\n } finally {\n closeSync(fd);\n }\n try {\n renameSync(tmpPath, targetPath);\n } catch (err) {\n try {\n unlinkSync(tmpPath);\n } catch {\n // ignore\n }\n throw err;\n }\n}\n"]}
|
package/dist/main.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,iBAAiB,CAAC","sourcesContent":["#!/usr/bin/env node\n\nimport \"./instrument.js\";\n\nimport { join, resolve } from \"path\";\nimport { mkdirSync, readFileSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join as pathJoin } from \"path\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler } from \"./adapter.js\";\nimport { DiscordBot } from \"./adapters/discord/index.js\";\nimport { TelegramBot } from \"./adapters/telegram/index.js\";\nimport { SlackBot as SlackBotClass } from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n getChannelSessionDir,\n getThreadSessionFile,\n resolveGenericSessionScope,\n type ResolvedSessionScope,\n} from \"./session-store.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { FileUserBindingStore } from \"./bindings.js\";\nimport { parseLoginCommand } from \"./login/index.js\";\nimport { startLinkServer } from \"./login/portal.js\";\nimport { InMemoryLinkTokenStore } from \"./login/session.js\";\nimport { parseSessionViewCommand } from \"./session-view/command.js\";\nimport { resolveExistingSessionFile } from \"./session-view/service.js\";\nimport { InMemorySessionViewTokenStore } from \"./session-view/store.js\";\nimport { DockerContainerManager } from \"./provisioner.js\";\nimport { loadAgentConfig } from \"./config.js\";\nimport { SandboxError, parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { FileVaultManager } from \"./vault.js\";\nimport {\n createManagedVaultEntry,\n ensureSandboxVaultEntry,\n resolveActorVaultKey,\n} from \"./vault-routing.js\";\nimport { addLifecycleBreadcrumb, applyRunScope } from \"./sentry.js\";\nimport { ChannelStore } from \"./store.js\";\nimport { formatNothingRunning, formatStopped, formatStopping } from \"./ui-copy.js\";\nimport {\n hasMaterializedSlackBranchSession,\n resolveSlackSessionScope,\n waitForSlackBranchBootstrap,\n} from \"./adapters/slack/branch-manager.js\";\nimport * as Sentry from \"@sentry/node\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\n// Get version from package.json\nfunction getVersion(): string {\n // Try to find package.json in the dist directory or parent\n const possiblePaths = [\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"package.json\"),\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\"),\n pathJoin(process.cwd(), \"package.json\"),\n ];\n\n for (const pkgPath of possiblePaths) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n if (pkg.version) return pkg.version;\n } catch {\n // Continue to next path\n }\n }\n return \"unknown\";\n}\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;\nconst MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;\nconst MOM_LINK_URL = process.env.MOM_LINK_URL;\nconst MOM_LINK_PORT = process.env.MOM_LINK_PORT\n ? parseInt(process.env.MOM_LINK_PORT, 10)\n : MOM_LINK_URL\n ? 8181\n : undefined;\n\ninterface ParsedArgs {\n workingDir?: string;\n stateDir?: string;\n sandbox: SandboxConfig;\n downloadChannel?: string;\n showVersion?: boolean;\n}\n\nfunction parseArgs(): ParsedArgs {\n const args = process.argv.slice(2);\n let sandbox: SandboxConfig = { type: \"host\" };\n let workingDir: string | undefined;\n let stateDirArg: string | undefined;\n let downloadChannelId: string | undefined;\n let showVersion = false;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\") {\n showVersion = true;\n } else if (arg.startsWith(\"--sandbox=\")) {\n sandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n } else if (arg === \"--sandbox\") {\n sandbox = parseSandboxArg(args[++i] || \"\");\n } else if (arg.startsWith(\"--state-dir=\")) {\n stateDirArg = arg.slice(\"--state-dir=\".length);\n } else if (arg === \"--state-dir\") {\n stateDirArg = args[++i];\n } else if (arg.startsWith(\"--download=\")) {\n downloadChannelId = arg.slice(\"--download=\".length);\n } else if (arg === \"--download\") {\n downloadChannelId = args[++i];\n } else if (!arg.startsWith(\"-\")) {\n workingDir = arg;\n }\n }\n\n return {\n workingDir: workingDir ? resolve(workingDir) : undefined,\n stateDir: stateDirArg ? resolve(stateDirArg) : undefined,\n sandbox,\n downloadChannel: downloadChannelId,\n showVersion,\n };\n}\n\nconst WORLD_WRITABLE_MODE = 0o002;\n\nfunction ensureSecureStateDir(path: string): void {\n let stat;\n try {\n stat = statSync(path);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n mkdirSync(path, { recursive: true, mode: 0o700 });\n return;\n }\n console.error(`Error: cannot access --state-dir ${path}: ${(err as Error).message}`);\n process.exit(1);\n }\n\n if (!stat.isDirectory()) {\n console.error(`Error: --state-dir ${path} exists but is not a directory`);\n process.exit(1);\n }\n\n if (stat.mode & WORLD_WRITABLE_MODE) {\n console.error(\n `Error: --state-dir ${path} is world-writable (mode ${(stat.mode & 0o777).toString(8)}). ` +\n `Credentials stored there would be exposed to other local users. ` +\n `Fix with: chmod 0700 ${path}`,\n );\n process.exit(1);\n }\n\n const euid = typeof process.geteuid === \"function\" ? process.geteuid() : undefined;\n if (euid !== undefined && stat.uid !== euid) {\n console.error(\n `Error: --state-dir ${path} is owned by uid ${stat.uid} but mama is running as uid ${euid}. ` +\n `Run mama as the directory owner or point --state-dir at a directory you own.`,\n );\n process.exit(1);\n }\n}\n\nfunction handleStartupError(error: unknown): never {\n if (error instanceof SandboxError) {\n for (const line of error.formatForCli()) {\n console.error(line);\n }\n process.exit(1);\n }\n throw error;\n}\n\nlet parsedArgs: ParsedArgs;\ntry {\n parsedArgs = parseArgs();\n} catch (error) {\n handleStartupError(error);\n}\n\n// Handle --version\nif (parsedArgs.showVersion) {\n console.log(getVersion());\n process.exit(0);\n}\n\n// Handle --download mode (Slack only)\nif (parsedArgs.downloadChannel) {\n if (!MOM_SLACK_BOT_TOKEN) {\n console.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n process.exit(1);\n }\n await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n process.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n console.error(\n \"Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>] <working-directory>\",\n );\n console.error(\" mama --download <channel-id>\");\n process.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\nconst stateDir = parsedArgs.stateDir ?? join(homedir(), \".mama\");\nprocess.env.MAMA_STATE_DIR = stateDir;\nensureSecureStateDir(stateDir);\n\n// Validate platform tokens\nconst hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);\nconst hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;\nconst hasDiscord = !!MOM_DISCORD_BOT_TOKEN;\n\nif (!hasSlack && !hasTelegram && !hasDiscord) {\n console.error(\n \"No platform tokens found. Set one of:\\n\" +\n \" Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\\n\" +\n \" Telegram: MOM_TELEGRAM_BOT_TOKEN\\n\" +\n \" Discord: MOM_DISCORD_BOT_TOKEN\",\n );\n process.exit(1);\n}\n\ntry {\n await validateSandbox(sandbox);\n} catch (error) {\n handleStartupError(error);\n}\n\nconst vaultManager = new FileVaultManager(stateDir);\nif (vaultManager.isEnabled()) {\n console.log(\n sandbox.type === \"container\"\n ? \" Vault system enabled. Container vault active.\"\n : sandbox.type === \"image\" || sandbox.type === \"firecracker\"\n ? \" Vault system enabled. Per-user credential routing active.\"\n : \" Vault system enabled. Host mode will not inject vault env.\",\n );\n}\n\nconst bindingStore = new FileUserBindingStore(stateDir);\nif (bindingStore.isEnabled()) {\n console.log(\n sandbox.type === \"container\"\n ? \" Binding store enabled. Container mode uses the container vault.\"\n : sandbox.type === \"image\" || sandbox.type === \"firecracker\"\n ? \" Binding store enabled. Platform user → vault routing active.\"\n : \" Binding store enabled. Host mode will not inject vault env.\",\n );\n}\n\nconst startupConfig = loadAgentConfig(workingDir);\nconst sandboxLimits =\n startupConfig.sandboxCpus || startupConfig.sandboxMemory\n ? { cpus: startupConfig.sandboxCpus, memory: startupConfig.sandboxMemory }\n : undefined;\n\nconst provisioner =\n sandbox.type === \"image\"\n ? new DockerContainerManager(sandbox.image, workingDir, { limits: sandboxLimits })\n : undefined;\n\nconst linkTokenStore = new InMemoryLinkTokenStore();\nconst sessionViewTokenStore = new InMemorySessionViewTokenStore();\nsetInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();\nsetInterval(() => sessionViewTokenStore.purge(), 5 * 60 * 1000).unref();\n\nfunction normalizePortalBaseUrl(): string | undefined {\n if (MOM_LINK_URL) {\n return MOM_LINK_URL.replace(/\\/+$/, \"\");\n }\n if (MOM_LINK_PORT) {\n return `http://localhost:${MOM_LINK_PORT}`;\n }\n return undefined;\n}\n\nfunction isPrivateConversation(event: BotEvent): boolean {\n return event.conversationKind === \"direct\" || event.type === \"dm\";\n}\n\nfunction ensureLoginVault(platform: string, platformUserId: string): string {\n const vaultId = resolveActorVaultKey(\n sandbox,\n vaultManager,\n bindingStore,\n platform,\n platformUserId,\n );\n\n ensureSandboxVaultEntry(sandbox, vaultManager, platform, platformUserId, vaultId);\n if (sandbox.type !== \"container\" && sandbox.type !== \"image\") {\n vaultManager.addEntry(vaultId, createManagedVaultEntry(platform, platformUserId, vaultId));\n }\n\n return vaultId;\n}\n\nasync function replyWithContext(\n responseCtx: BotAdapters[\"responseCtx\"],\n text: string,\n): Promise<void> {\n await responseCtx.setTyping(false);\n await responseCtx.setWorking(false);\n await responseCtx.respond(text);\n}\n\nasync function handleLoginCommand(\n platform: string,\n platformUserId: string,\n conversationId: string,\n responseCtx: BotAdapters[\"responseCtx\"],\n commandText: string,\n privateConversation: boolean,\n): Promise<boolean> {\n const parsed = parseLoginCommand(commandText);\n if (!parsed) return false;\n\n if (!privateConversation) {\n await replyWithContext(\n responseCtx,\n \"為了保護你的憑證,`/login` 只能在與機器人的私訊中使用。請先私訊機器人,再重新執行 `/login`。\",\n );\n return true;\n }\n\n const baseUrl = normalizePortalBaseUrl();\n if (!baseUrl) {\n await replyWithContext(\n responseCtx,\n \"Login is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.\",\n );\n return true;\n }\n\n let vaultId: string;\n try {\n vaultId = ensureLoginVault(platform, platformUserId);\n } catch (error) {\n log.logWarning(\n `[${conversationId}] Failed to prepare login vault for ${platform}/${platformUserId}`,\n error instanceof Error ? error.message : String(error),\n );\n await replyWithContext(\n responseCtx,\n \"Login setup failed on the server. 請稍後重試,或聯絡管理員檢查 vault 儲存權限。\",\n );\n return true;\n }\n\n const token = linkTokenStore.create(\n platform as \"slack\" | \"discord\" | \"telegram\",\n platformUserId,\n conversationId,\n vaultId,\n \"\",\n );\n const vaultLabel = sandbox.type === \"container\" ? `container vault (${vaultId})` : \"your vault\";\n await replyWithContext(\n responseCtx,\n `Open this link to store credentials in ${vaultLabel} (expires in 15 minutes):\\n${baseUrl}/link?token=${token.token}`,\n );\n return true;\n}\n\nasync function handleSessionViewCommand(\n platform: string,\n platformUserId: string,\n conversationId: string,\n sessionKey: string,\n bot: Bot,\n responseCtx: BotAdapters[\"responseCtx\"],\n commandText: string,\n privateConversation: boolean,\n): Promise<boolean> {\n if (!parseSessionViewCommand(commandText)) return false;\n\n const allowSharedPrivateDelivery = platform === \"slack\" || platform === \"discord\";\n const sendSessionViewReply = async (text: string): Promise<void> => {\n if (privateConversation) {\n await replyWithContext(responseCtx, text);\n return;\n }\n\n if (platform === \"slack\" && bot instanceof SlackBotClass) {\n await bot.postEphemeral(conversationId, platformUserId, text);\n return;\n }\n\n if (platform === \"discord\" && bot instanceof DiscordBot) {\n await bot.sendDirectMessage(platformUserId, text);\n return;\n }\n\n await replyWithContext(responseCtx, text);\n };\n\n if (!privateConversation && !allowSharedPrivateDelivery) {\n await sendSessionViewReply(\n \"為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。\",\n );\n return true;\n }\n\n const baseUrl = normalizePortalBaseUrl();\n if (!baseUrl) {\n await sendSessionViewReply(\n \"Session viewer is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.\",\n );\n return true;\n }\n\n const sessionFile = resolveExistingSessionFile(workingDir, conversationId, sessionKey);\n if (!sessionFile) {\n await sendSessionViewReply(\n \"目前還沒有可查看的 session。先和機器人對話一次,建立 session 後再試。\",\n );\n return true;\n }\n\n const token = sessionViewTokenStore.create(\n platform as \"slack\" | \"discord\" | \"telegram\",\n platformUserId,\n conversationId,\n sessionKey,\n sessionFile,\n );\n\n const linkText = `Open this read-only session link (expires in 24 hours):\\n${baseUrl}/session?token=${token.token}`;\n await sendSessionViewReply(linkText);\n return true;\n}\n\n// ============================================================================\n// State (per conversation)\n// ============================================================================\n\ninterface ConversationState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: number;\n}\n\nconst conversationStates = new Map<string, ConversationState>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n/** Idle timeout for managed image containers (10 minutes) */\nconst IMAGE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;\n\nif (provisioner) {\n await provisioner.reconcile();\n await provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS);\n setInterval(() => provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS), IMAGE_IDLE_TIMEOUT_MS).unref();\n}\n\nasync function resolveSessionScope(\n platformName: string,\n conversationDir: string,\n sessionKey: string,\n): Promise<ResolvedSessionScope> {\n if (platformName === \"slack\") {\n return resolveSlackSessionScope({ conversationDir, sessionKey });\n }\n return resolveGenericSessionScope({ conversationDir, sessionKey });\n}\n\nasync function getState(\n conversationId: string,\n platformName: string,\n sessionKey?: string,\n): Promise<ConversationState> {\n const key = sessionKey ?? conversationId;\n let state = conversationStates.get(key);\n if (!state) {\n const conversationDir = join(workingDir, conversationId);\n const sessionScope = await resolveSessionScope(platformName, conversationDir, key);\n state = {\n running: false,\n runner: await createRunner(\n sandbox,\n key,\n conversationId,\n conversationDir,\n workingDir,\n sessionScope,\n vaultManager,\n bindingStore,\n provisioner,\n ),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n conversationStates.set(key, state);\n } else {\n state.lastAccessedAt = Date.now();\n }\n return state;\n}\n\n/**\n * Evict idle sessions from conversationStates to bound memory usage.\n * Called after each handleEvent completes.\n */\nfunction evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of conversationStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n conversationStates.delete(key);\n }\n }\n\n if (conversationStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of conversationStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = conversationStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n conversationStates.delete(idleSessions[i].key);\n }\n }\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: BotHandler = {\n isRunning(sessionKey: string): boolean {\n const state = conversationStates.get(sessionKey);\n return !!state?.running;\n },\n\n getRunningSessions() {\n const sessions: import(\"./adapter.js\").RunningSession[] = [];\n for (const [sessionKey, state] of conversationStates) {\n if (state.running && state.startedAt) {\n // Get current step from runner\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n },\n\n async handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(conversationId, formatStopping(bot));\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(conversationId, formatNothingRunning(bot));\n }\n },\n\n forceStop(sessionKey: string): void {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n },\n\n async handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n }\n\n // Conversation sessions rotate via current pointer. Thread sessions reset in place.\n const conversationDir = join(workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n createManagedSessionFileAtPath(\n getThreadSessionFile(conversationDir, sessionKey),\n conversationDir,\n );\n } else {\n createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);\n }\n\n // Remove from in-memory cache\n conversationStates.delete(sessionKey);\n\n log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);\n await bot.postMessage(conversationId, \"Conversation reset. Send a new message to start fresh.\");\n },\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n _isEvent?: boolean,\n ): Promise<void> {\n const conversationId = event.conversationId;\n\n // Don't accept new events during shutdown\n if (isShuttingDown) {\n log.logInfo(\n `[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;\n const privateConversation = isPrivateConversation(event);\n const handledLogin = await handleLoginCommand(\n adapters.platform.name,\n event.user,\n conversationId,\n adapters.responseCtx,\n event.text,\n privateConversation,\n );\n if (handledLogin) return;\n\n const handledSessionView = await handleSessionViewCommand(\n adapters.platform.name,\n event.user,\n conversationId,\n sessionKey,\n bot,\n adapters.responseCtx,\n event.text,\n privateConversation,\n );\n if (handledSessionView) return;\n\n const conversationDir = join(workingDir, conversationId);\n const waitedForParent =\n adapters.platform.name === \"slack\"\n ? await waitForSlackBranchBootstrap({\n parentSessionKey: conversationId,\n sessionKey,\n hasThreadSession: () => hasMaterializedSlackBranchSession(conversationDir, sessionKey),\n isParentRunning: () => conversationStates.get(conversationId)?.running === true,\n })\n : false;\n if (waitedForParent) {\n log.logInfo(\n `[${conversationId}] Delayed thread bootstrap until parent session sealed: ${sessionKey}`,\n );\n }\n\n const state = await getState(conversationId, adapters.platform.name, sessionKey);\n\n // Start run\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);\n\n // Wrap in-flight run tracking\n Sentry.metrics.count(\"agent.run.started\", 1, {\n attributes: { channel: conversationId },\n });\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size + 1);\n\n const runPromise = Sentry.startSpan(\n { name: \"agent.run\", op: \"agent\", attributes: { conversationId, sessionKey } },\n async () => {\n return Sentry.withScope(async (scope) => {\n const { message, responseCtx, platform } = adapters;\n applyRunScope(scope, {\n conversationId,\n sessionKey,\n messageId: message.id,\n platform: platform.name,\n userId: message.userId,\n userName: message.userName,\n threadTs: message.threadTs,\n isEvent: _isEvent,\n });\n addLifecycleBreadcrumb(\"agent.run.started\", {\n channel_id: conversationId,\n platform: platform.name,\n has_attachments: (message.attachments?.length ?? 0) > 0,\n });\n\n try {\n await responseCtx.setTyping(true);\n await responseCtx.setWorking(true);\n const result = await state.runner.run(message, responseCtx, platform);\n await responseCtx.setWorking(false);\n\n const durationMs = Date.now() - state.startedAt!;\n Sentry.metrics.distribution(\"agent.run.duration\", durationMs, {\n unit: \"millisecond\",\n attributes: {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n Sentry.metrics.count(\"agent.run.completed\", 1, {\n attributes: {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n addLifecycleBreadcrumb(\"agent.run.completed\", {\n channel_id: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n duration_ms: durationMs,\n });\n\n if (result.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(conversationId, formatStopped(bot));\n }\n }\n } catch (err) {\n scope.setContext(\"agent_run_error\", {\n conversationId,\n sessionKey,\n platform: adapters.platform.name,\n messageId: adapters.message.id,\n threadTs: adapters.message.threadTs,\n });\n Sentry.captureException(err);\n Sentry.metrics.count(\"agent.run.errors\", 1, {\n attributes: { channel: conversationId, platform: adapters.platform.name },\n });\n log.logWarning(\n `[${conversationId}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size - 1);\n evictIdleSessions();\n }\n });\n },\n );\n\n inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n inFlightRuns.delete(runPromise);\n }\n },\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nconst sandboxDesc =\n sandbox.type === \"host\"\n ? \"host\"\n : sandbox.type === \"container\"\n ? `container:${sandbox.container}`\n : sandbox.type === \"image\"\n ? `image:${sandbox.image}`\n : `firecracker:${sandbox.vmId}`;\nlog.logStartup(workingDir, sandboxDesc);\n\n// Create platform bots\nconst bots: Bot[] = [];\nconst botsByPlatform: Record<string, Bot> = {};\n\nif (hasSlack) {\n const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n const slackBot = new SlackBotClass(handler, {\n appToken: MOM_SLACK_APP_TOKEN!,\n botToken: MOM_SLACK_BOT_TOKEN!,\n workingDir,\n store: sharedStore,\n });\n bots.push(slackBot);\n botsByPlatform.slack = slackBot;\n log.logInfo(\"Platform: Slack\");\n}\nif (hasTelegram) {\n const telegramBot = new TelegramBot(handler, {\n token: MOM_TELEGRAM_BOT_TOKEN!,\n workingDir,\n });\n bots.push(telegramBot);\n botsByPlatform.telegram = telegramBot;\n log.logInfo(\"Platform: Telegram\");\n}\nif (hasDiscord) {\n const discordBot = new DiscordBot(handler, {\n token: MOM_DISCORD_BOT_TOKEN!,\n workingDir,\n });\n bots.push(discordBot);\n botsByPlatform.discord = discordBot;\n log.logInfo(\"Platform: Discord\");\n}\n\nif (MOM_LINK_PORT) {\n startLinkServer(\n MOM_LINK_PORT,\n linkTokenStore,\n vaultManager,\n async (platform, conversationId, message) => {\n const bot = botsByPlatform[platform];\n if (bot) await bot.postMessage(conversationId, message);\n },\n sessionViewTokenStore,\n );\n}\n\n// Start events watcher with explicit platform routing\nconst eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);\nconst slackBot = botsByPlatform.slack as SlackBotClass | undefined;\nif (slackBot) {\n slackBot.setEventsWatcher(eventsWatcher);\n}\neventsWatcher.start();\n\n// Handle shutdown\nasync function shutdown(): Promise<void> {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n await Sentry.close(5000);\n process.exit(0);\n}\n\nprocess.on(\"SIGINT\", shutdown);\nprocess.on(\"SIGTERM\", shutdown);\n\n// Start all bots\nawait Promise.all(\n bots.map((bot) =>\n bot.start().catch((err) => {\n log.logWarning(\"Failed to start bot\", err instanceof Error ? err.message : String(err));\n process.exit(1);\n }),\n ),\n);\n"]}
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,iBAAiB,CAAC","sourcesContent":["#!/usr/bin/env node\n\nimport \"./instrument.js\";\n\nimport { join, resolve } from \"path\";\nimport { mkdirSync, readFileSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join as pathJoin } from \"path\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler } from \"./adapter.js\";\nimport { DiscordBot } from \"./adapters/discord/index.js\";\nimport { TelegramBot } from \"./adapters/telegram/index.js\";\nimport { SlackBot as SlackBotClass } from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n getChannelSessionDir,\n getThreadSessionFile,\n resolveGenericSessionScope,\n type ResolvedSessionScope,\n} from \"./session-store.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { FileUserBindingStore } from \"./bindings.js\";\nimport { parseLoginCommand } from \"./login/index.js\";\nimport { startLinkServer } from \"./login/portal.js\";\nimport { InMemoryLinkTokenStore } from \"./login/session.js\";\nimport { parseSessionViewCommand } from \"./session-view/command.js\";\nimport { resolveExistingSessionFile } from \"./session-view/service.js\";\nimport { InMemorySessionViewTokenStore } from \"./session-view/store.js\";\nimport { DockerContainerManager } from \"./provisioner.js\";\nimport { loadAgentConfig } from \"./config.js\";\nimport { SandboxError, parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { FileVaultManager } from \"./vault.js\";\nimport {\n createManagedVaultEntry,\n ensureSandboxVaultEntry,\n resolveActorVaultKey,\n} from \"./vault-routing.js\";\nimport { addLifecycleBreadcrumb, applyRunScope } from \"./sentry.js\";\nimport { ChannelStore } from \"./store.js\";\nimport { formatNothingRunning, formatStopped, formatStopping } from \"./ui-copy.js\";\nimport {\n hasMaterializedSlackBranchSession,\n resolveSlackSessionScope,\n waitForSlackBranchBootstrap,\n} from \"./adapters/slack/branch-manager.js\";\nimport * as Sentry from \"@sentry/node\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\n// Get version from package.json\nfunction getVersion(): string {\n // Try to find package.json in the dist directory or parent\n const possiblePaths = [\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"package.json\"),\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\"),\n pathJoin(process.cwd(), \"package.json\"),\n ];\n\n for (const pkgPath of possiblePaths) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n if (pkg.version) return pkg.version;\n } catch {\n // Continue to next path\n }\n }\n return \"unknown\";\n}\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;\nconst MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;\nconst MOM_LINK_URL = process.env.MOM_LINK_URL;\nconst MOM_LINK_PORT = process.env.MOM_LINK_PORT\n ? parseInt(process.env.MOM_LINK_PORT, 10)\n : MOM_LINK_URL\n ? 8181\n : undefined;\n\ninterface ParsedArgs {\n workingDir?: string;\n stateDir?: string;\n sandbox: SandboxConfig;\n downloadChannel?: string;\n showVersion?: boolean;\n}\n\nfunction parseArgs(): ParsedArgs {\n const args = process.argv.slice(2);\n let sandbox: SandboxConfig = { type: \"host\" };\n let workingDir: string | undefined;\n let stateDirArg: string | undefined;\n let downloadChannelId: string | undefined;\n let showVersion = false;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\") {\n showVersion = true;\n } else if (arg.startsWith(\"--sandbox=\")) {\n sandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n } else if (arg === \"--sandbox\") {\n sandbox = parseSandboxArg(args[++i] || \"\");\n } else if (arg.startsWith(\"--state-dir=\")) {\n stateDirArg = arg.slice(\"--state-dir=\".length);\n } else if (arg === \"--state-dir\") {\n stateDirArg = args[++i];\n } else if (arg.startsWith(\"--download=\")) {\n downloadChannelId = arg.slice(\"--download=\".length);\n } else if (arg === \"--download\") {\n downloadChannelId = args[++i];\n } else if (!arg.startsWith(\"-\")) {\n workingDir = arg;\n }\n }\n\n return {\n workingDir: workingDir ? resolve(workingDir) : undefined,\n stateDir: stateDirArg ? resolve(stateDirArg) : undefined,\n sandbox,\n downloadChannel: downloadChannelId,\n showVersion,\n };\n}\n\nconst WORLD_WRITABLE_MODE = 0o002;\n\nfunction ensureSecureStateDir(path: string): void {\n let stat;\n try {\n stat = statSync(path);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n mkdirSync(path, { recursive: true, mode: 0o700 });\n return;\n }\n console.error(`Error: cannot access --state-dir ${path}: ${(err as Error).message}`);\n process.exit(1);\n }\n\n if (!stat.isDirectory()) {\n console.error(`Error: --state-dir ${path} exists but is not a directory`);\n process.exit(1);\n }\n\n if (stat.mode & WORLD_WRITABLE_MODE) {\n console.error(\n `Error: --state-dir ${path} is world-writable (mode ${(stat.mode & 0o777).toString(8)}). ` +\n `Credentials stored there would be exposed to other local users. ` +\n `Fix with: chmod 0700 ${path}`,\n );\n process.exit(1);\n }\n\n const euid = typeof process.geteuid === \"function\" ? process.geteuid() : undefined;\n if (euid !== undefined && stat.uid !== euid) {\n console.error(\n `Error: --state-dir ${path} is owned by uid ${stat.uid} but mama is running as uid ${euid}. ` +\n `Run mama as the directory owner or point --state-dir at a directory you own.`,\n );\n process.exit(1);\n }\n}\n\nfunction handleStartupError(error: unknown): never {\n if (error instanceof SandboxError) {\n for (const line of error.formatForCli()) {\n console.error(line);\n }\n process.exit(1);\n }\n throw error;\n}\n\nlet parsedArgs: ParsedArgs;\ntry {\n parsedArgs = parseArgs();\n} catch (error) {\n handleStartupError(error);\n}\n\n// Handle --version\nif (parsedArgs.showVersion) {\n console.log(getVersion());\n process.exit(0);\n}\n\n// Handle --download mode (Slack only)\nif (parsedArgs.downloadChannel) {\n if (!MOM_SLACK_BOT_TOKEN) {\n console.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n process.exit(1);\n }\n await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n process.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n console.error(\n \"Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>] <working-directory>\",\n );\n console.error(\" mama --download <channel-id>\");\n process.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\nconst stateDir = parsedArgs.stateDir ?? join(homedir(), \".mama\");\nprocess.env.MAMA_STATE_DIR = stateDir;\nensureSecureStateDir(stateDir);\n\n// Validate platform tokens\nconst hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);\nconst hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;\nconst hasDiscord = !!MOM_DISCORD_BOT_TOKEN;\n\nif (!hasSlack && !hasTelegram && !hasDiscord) {\n console.error(\n \"No platform tokens found. Set one of:\\n\" +\n \" Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\\n\" +\n \" Telegram: MOM_TELEGRAM_BOT_TOKEN\\n\" +\n \" Discord: MOM_DISCORD_BOT_TOKEN\",\n );\n process.exit(1);\n}\n\ntry {\n await validateSandbox(sandbox);\n} catch (error) {\n handleStartupError(error);\n}\n\nconst vaultManager = new FileVaultManager(stateDir);\nif (vaultManager.isEnabled()) {\n console.log(\n sandbox.type === \"container\"\n ? \" Vault system enabled. Container vault active.\"\n : sandbox.type === \"image\" || sandbox.type === \"firecracker\"\n ? \" Vault system enabled. Per-user credential routing active.\"\n : \" Vault system enabled. Host mode will not inject vault env.\",\n );\n}\n\nconst bindingStore = new FileUserBindingStore(stateDir);\nif (bindingStore.isEnabled()) {\n console.log(\n sandbox.type === \"container\"\n ? \" Binding store enabled. Container mode uses the container vault.\"\n : sandbox.type === \"image\" || sandbox.type === \"firecracker\"\n ? \" Binding store enabled. Platform user → vault routing active.\"\n : \" Binding store enabled. Host mode will not inject vault env.\",\n );\n}\n\nconst startupConfig = loadAgentConfig(workingDir);\nconst sandboxLimits =\n startupConfig.sandboxCpus || startupConfig.sandboxMemory\n ? { cpus: startupConfig.sandboxCpus, memory: startupConfig.sandboxMemory }\n : undefined;\n\nconst provisioner =\n sandbox.type === \"image\"\n ? new DockerContainerManager(sandbox.image, workingDir, { limits: sandboxLimits })\n : undefined;\n\nconst linkTokenStore = new InMemoryLinkTokenStore();\nconst sessionViewTokenStore = new InMemorySessionViewTokenStore();\nsetInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();\nsetInterval(() => sessionViewTokenStore.purge(), 5 * 60 * 1000).unref();\n\nfunction portalBaseUrl(): string | undefined {\n if (MOM_LINK_URL) return MOM_LINK_URL.replace(/\\/+$/, \"\");\n if (MOM_LINK_PORT) return `http://localhost:${MOM_LINK_PORT}`;\n return undefined;\n}\n\nfunction isPrivateConversation(event: BotEvent): boolean {\n return event.conversationKind === \"direct\" || event.type === \"dm\";\n}\n\nfunction ensureLoginVault(platform: string, platformUserId: string): string {\n const vaultId = resolveActorVaultKey(\n sandbox,\n vaultManager,\n bindingStore,\n platform,\n platformUserId,\n );\n\n ensureSandboxVaultEntry(sandbox, vaultManager, platform, platformUserId, vaultId);\n if (sandbox.type !== \"container\" && sandbox.type !== \"image\") {\n vaultManager.addEntry(vaultId, createManagedVaultEntry(platform, platformUserId, vaultId));\n }\n\n return vaultId;\n}\n\nasync function replyWithContext(\n responseCtx: BotAdapters[\"responseCtx\"],\n text: string,\n): Promise<void> {\n await responseCtx.setTyping(false);\n await responseCtx.setWorking(false);\n await responseCtx.respond(text);\n}\n\nasync function handleLoginCommand(\n platform: string,\n platformUserId: string,\n conversationId: string,\n responseCtx: BotAdapters[\"responseCtx\"],\n commandText: string,\n privateConversation: boolean,\n): Promise<boolean> {\n const parsed = parseLoginCommand(commandText);\n if (!parsed) return false;\n\n if (!privateConversation) {\n await replyWithContext(\n responseCtx,\n \"為了保護你的憑證,`/login` 只能在與機器人的私訊中使用。請先私訊機器人,再重新執行 `/login`。\",\n );\n return true;\n }\n\n const baseUrl = portalBaseUrl();\n if (!baseUrl) {\n await replyWithContext(\n responseCtx,\n \"Login is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.\",\n );\n return true;\n }\n\n let vaultId: string;\n try {\n vaultId = ensureLoginVault(platform, platformUserId);\n } catch (error) {\n log.logWarning(\n `[${conversationId}] Failed to prepare login vault for ${platform}/${platformUserId}`,\n error instanceof Error ? error.message : String(error),\n );\n await replyWithContext(\n responseCtx,\n \"Login setup failed on the server. 請稍後重試,或聯絡管理員檢查 vault 儲存權限。\",\n );\n return true;\n }\n\n const token = linkTokenStore.create(\n platform as \"slack\" | \"discord\" | \"telegram\",\n platformUserId,\n conversationId,\n vaultId,\n \"\",\n );\n const vaultLabel = sandbox.type === \"container\" ? `container vault (${vaultId})` : \"your vault\";\n await replyWithContext(\n responseCtx,\n `Open this link to store credentials in ${vaultLabel} (expires in 15 minutes):\\n${baseUrl}/link?token=${token.token}`,\n );\n return true;\n}\n\nasync function handleSessionViewCommand(\n platform: string,\n platformUserId: string,\n conversationId: string,\n sessionKey: string,\n bot: Bot,\n responseCtx: BotAdapters[\"responseCtx\"],\n commandText: string,\n privateConversation: boolean,\n): Promise<boolean> {\n if (!parseSessionViewCommand(commandText)) return false;\n\n const allowSharedPrivateDelivery = platform === \"slack\" || platform === \"discord\";\n const sendSessionViewReply = async (text: string): Promise<void> => {\n if (privateConversation) {\n await replyWithContext(responseCtx, text);\n return;\n }\n\n if (platform === \"slack\" && bot instanceof SlackBotClass) {\n await bot.postEphemeral(conversationId, platformUserId, text);\n return;\n }\n\n if (platform === \"discord\" && bot instanceof DiscordBot) {\n await bot.sendDirectMessage(platformUserId, text);\n return;\n }\n\n await replyWithContext(responseCtx, text);\n };\n\n if (!privateConversation && !allowSharedPrivateDelivery) {\n await sendSessionViewReply(\n \"為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。\",\n );\n return true;\n }\n\n const baseUrl = portalBaseUrl();\n if (!baseUrl) {\n await sendSessionViewReply(\n \"Session viewer is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.\",\n );\n return true;\n }\n\n const sessionFile = resolveExistingSessionFile(workingDir, conversationId, sessionKey);\n if (!sessionFile) {\n await sendSessionViewReply(\n \"目前還沒有可查看的 session。先和機器人對話一次,建立 session 後再試。\",\n );\n return true;\n }\n\n const token = sessionViewTokenStore.create(\n platform as \"slack\" | \"discord\" | \"telegram\",\n platformUserId,\n conversationId,\n sessionKey,\n sessionFile,\n );\n\n const linkText = `Open this read-only session link (expires in 24 hours):\\n${baseUrl}/session?token=${token.token}`;\n await sendSessionViewReply(linkText);\n return true;\n}\n\n// ============================================================================\n// State (per conversation)\n// ============================================================================\n\ninterface ConversationState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: number;\n}\n\nconst conversationStates = new Map<string, ConversationState>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n/** Idle timeout for managed image containers (10 minutes) */\nconst IMAGE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;\n\nif (provisioner) {\n await provisioner.reconcile();\n await provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS);\n setInterval(() => provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS), IMAGE_IDLE_TIMEOUT_MS).unref();\n}\n\nasync function resolveSessionScope(\n platformName: string,\n conversationDir: string,\n sessionKey: string,\n): Promise<ResolvedSessionScope> {\n if (platformName === \"slack\") {\n return resolveSlackSessionScope({ conversationDir, sessionKey });\n }\n return resolveGenericSessionScope({ conversationDir, sessionKey });\n}\n\nasync function getState(\n conversationId: string,\n platformName: string,\n sessionKey?: string,\n): Promise<ConversationState> {\n const key = sessionKey ?? conversationId;\n let state = conversationStates.get(key);\n if (!state) {\n const conversationDir = join(workingDir, conversationId);\n const sessionScope = await resolveSessionScope(platformName, conversationDir, key);\n state = {\n running: false,\n runner: await createRunner(\n sandbox,\n key,\n conversationId,\n conversationDir,\n workingDir,\n sessionScope,\n vaultManager,\n bindingStore,\n provisioner,\n ),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n conversationStates.set(key, state);\n } else {\n state.lastAccessedAt = Date.now();\n }\n return state;\n}\n\n/**\n * Evict idle sessions from conversationStates to bound memory usage.\n * Called after each handleEvent completes.\n */\nfunction evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of conversationStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n conversationStates.delete(key);\n }\n }\n\n if (conversationStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of conversationStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = conversationStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n conversationStates.delete(idleSessions[i].key);\n }\n }\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: BotHandler = {\n isRunning(sessionKey: string): boolean {\n const state = conversationStates.get(sessionKey);\n return !!state?.running;\n },\n\n getRunningSessions() {\n const sessions: import(\"./adapter.js\").RunningSession[] = [];\n for (const [sessionKey, state] of conversationStates) {\n if (state.running && state.startedAt) {\n // Get current step from runner\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n },\n\n async handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(conversationId, formatStopping(bot));\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(conversationId, formatNothingRunning(bot));\n }\n },\n\n forceStop(sessionKey: string): void {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n },\n\n async handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n }\n\n // Conversation sessions rotate via current pointer. Thread sessions reset in place.\n const conversationDir = join(workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n createManagedSessionFileAtPath(\n getThreadSessionFile(conversationDir, sessionKey),\n conversationDir,\n );\n } else {\n createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);\n }\n\n // Remove from in-memory cache\n conversationStates.delete(sessionKey);\n\n log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);\n await bot.postMessage(conversationId, \"Conversation reset. Send a new message to start fresh.\");\n },\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n _isEvent?: boolean,\n ): Promise<void> {\n const conversationId = event.conversationId;\n\n // Don't accept new events during shutdown\n if (isShuttingDown) {\n log.logInfo(\n `[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;\n const privateConversation = isPrivateConversation(event);\n const handledLogin = await handleLoginCommand(\n adapters.platform.name,\n event.user,\n conversationId,\n adapters.responseCtx,\n event.text,\n privateConversation,\n );\n if (handledLogin) return;\n\n const handledSessionView = await handleSessionViewCommand(\n adapters.platform.name,\n event.user,\n conversationId,\n sessionKey,\n bot,\n adapters.responseCtx,\n event.text,\n privateConversation,\n );\n if (handledSessionView) return;\n\n const conversationDir = join(workingDir, conversationId);\n const waitedForParent =\n adapters.platform.name === \"slack\"\n ? await waitForSlackBranchBootstrap({\n parentSessionKey: conversationId,\n sessionKey,\n hasThreadSession: () => hasMaterializedSlackBranchSession(conversationDir, sessionKey),\n isParentRunning: () => conversationStates.get(conversationId)?.running === true,\n })\n : false;\n if (waitedForParent) {\n log.logInfo(\n `[${conversationId}] Delayed thread bootstrap until parent session sealed: ${sessionKey}`,\n );\n }\n\n const state = await getState(conversationId, adapters.platform.name, sessionKey);\n\n // Start run\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);\n\n // Wrap in-flight run tracking\n Sentry.metrics.count(\"agent.run.started\", 1, {\n attributes: { channel: conversationId },\n });\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size + 1);\n\n const runPromise = Sentry.startSpan(\n { name: \"agent.run\", op: \"agent\", attributes: { conversationId, sessionKey } },\n async () => {\n return Sentry.withScope(async (scope) => {\n const { message, responseCtx, platform } = adapters;\n applyRunScope(scope, {\n conversationId,\n sessionKey,\n messageId: message.id,\n platform: platform.name,\n userId: message.userId,\n userName: message.userName,\n threadTs: message.threadTs,\n isEvent: _isEvent,\n });\n addLifecycleBreadcrumb(\"agent.run.started\", {\n channel_id: conversationId,\n platform: platform.name,\n has_attachments: (message.attachments?.length ?? 0) > 0,\n });\n\n try {\n await responseCtx.setTyping(true);\n await responseCtx.setWorking(true);\n const result = await state.runner.run(message, responseCtx, platform);\n await responseCtx.setWorking(false);\n\n const durationMs = Date.now() - state.startedAt!;\n Sentry.metrics.distribution(\"agent.run.duration\", durationMs, {\n unit: \"millisecond\",\n attributes: {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n Sentry.metrics.count(\"agent.run.completed\", 1, {\n attributes: {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n addLifecycleBreadcrumb(\"agent.run.completed\", {\n channel_id: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n duration_ms: durationMs,\n });\n\n if (result.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(conversationId, formatStopped(bot));\n }\n }\n } catch (err) {\n scope.setContext(\"agent_run_error\", {\n conversationId,\n sessionKey,\n platform: adapters.platform.name,\n messageId: adapters.message.id,\n threadTs: adapters.message.threadTs,\n });\n Sentry.captureException(err);\n Sentry.metrics.count(\"agent.run.errors\", 1, {\n attributes: { channel: conversationId, platform: adapters.platform.name },\n });\n log.logWarning(\n `[${conversationId}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size - 1);\n evictIdleSessions();\n }\n });\n },\n );\n\n inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n inFlightRuns.delete(runPromise);\n }\n },\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nconst sandboxDesc =\n sandbox.type === \"host\"\n ? \"host\"\n : sandbox.type === \"container\"\n ? `container:${sandbox.container}`\n : sandbox.type === \"image\"\n ? `image:${sandbox.image}`\n : `firecracker:${sandbox.vmId}`;\nlog.logStartup(workingDir, sandboxDesc);\n\n// Create platform bots\nconst bots: Bot[] = [];\nconst botsByPlatform: Record<string, Bot> = {};\n\nif (hasSlack) {\n const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n const slackBot = new SlackBotClass(handler, {\n appToken: MOM_SLACK_APP_TOKEN!,\n botToken: MOM_SLACK_BOT_TOKEN!,\n workingDir,\n store: sharedStore,\n });\n bots.push(slackBot);\n botsByPlatform.slack = slackBot;\n log.logInfo(\"Platform: Slack\");\n}\nif (hasTelegram) {\n const telegramBot = new TelegramBot(handler, {\n token: MOM_TELEGRAM_BOT_TOKEN!,\n workingDir,\n });\n bots.push(telegramBot);\n botsByPlatform.telegram = telegramBot;\n log.logInfo(\"Platform: Telegram\");\n}\nif (hasDiscord) {\n const discordBot = new DiscordBot(handler, {\n token: MOM_DISCORD_BOT_TOKEN!,\n workingDir,\n });\n bots.push(discordBot);\n botsByPlatform.discord = discordBot;\n log.logInfo(\"Platform: Discord\");\n}\n\nif (MOM_LINK_PORT) {\n startLinkServer(\n MOM_LINK_PORT,\n linkTokenStore,\n vaultManager,\n async (platform, conversationId, message) => {\n const bot = botsByPlatform[platform];\n if (bot) await bot.postMessage(conversationId, message);\n },\n sessionViewTokenStore,\n );\n}\n\n// Start events watcher with explicit platform routing\nconst eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);\nconst slackBot = botsByPlatform.slack as SlackBotClass | undefined;\nif (slackBot) {\n slackBot.setEventsWatcher(eventsWatcher);\n}\neventsWatcher.start();\n\n// Handle shutdown\nasync function shutdown(): Promise<void> {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n await Sentry.close(5000);\n process.exit(0);\n}\n\nprocess.on(\"SIGINT\", shutdown);\nprocess.on(\"SIGTERM\", shutdown);\n\n// Start all bots\nawait Promise.all(\n bots.map((bot) =>\n bot.start().catch((err) => {\n log.logWarning(\"Failed to start bot\", err instanceof Error ? err.message : String(err));\n process.exit(1);\n }),\n ),\n);\n"]}
|
package/dist/main.js
CHANGED
|
@@ -221,13 +221,11 @@ const linkTokenStore = new InMemoryLinkTokenStore();
|
|
|
221
221
|
const sessionViewTokenStore = new InMemorySessionViewTokenStore();
|
|
222
222
|
setInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();
|
|
223
223
|
setInterval(() => sessionViewTokenStore.purge(), 5 * 60 * 1000).unref();
|
|
224
|
-
function
|
|
225
|
-
if (MOM_LINK_URL)
|
|
224
|
+
function portalBaseUrl() {
|
|
225
|
+
if (MOM_LINK_URL)
|
|
226
226
|
return MOM_LINK_URL.replace(/\/+$/, "");
|
|
227
|
-
|
|
228
|
-
if (MOM_LINK_PORT) {
|
|
227
|
+
if (MOM_LINK_PORT)
|
|
229
228
|
return `http://localhost:${MOM_LINK_PORT}`;
|
|
230
|
-
}
|
|
231
229
|
return undefined;
|
|
232
230
|
}
|
|
233
231
|
function isPrivateConversation(event) {
|
|
@@ -254,7 +252,7 @@ async function handleLoginCommand(platform, platformUserId, conversationId, resp
|
|
|
254
252
|
await replyWithContext(responseCtx, "為了保護你的憑證,`/login` 只能在與機器人的私訊中使用。請先私訊機器人,再重新執行 `/login`。");
|
|
255
253
|
return true;
|
|
256
254
|
}
|
|
257
|
-
const baseUrl =
|
|
255
|
+
const baseUrl = portalBaseUrl();
|
|
258
256
|
if (!baseUrl) {
|
|
259
257
|
await replyWithContext(responseCtx, "Login is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.");
|
|
260
258
|
return true;
|
|
@@ -296,7 +294,7 @@ async function handleSessionViewCommand(platform, platformUserId, conversationId
|
|
|
296
294
|
await sendSessionViewReply("為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。");
|
|
297
295
|
return true;
|
|
298
296
|
}
|
|
299
|
-
const baseUrl =
|
|
297
|
+
const baseUrl = portalBaseUrl();
|
|
300
298
|
if (!baseUrl) {
|
|
301
299
|
await sendSessionViewReply("Session viewer is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.");
|
|
302
300
|
return true;
|
package/dist/main.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport \"./instrument.js\";\n\nimport { join, resolve } from \"path\";\nimport { mkdirSync, readFileSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join as pathJoin } from \"path\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler } from \"./adapter.js\";\nimport { DiscordBot } from \"./adapters/discord/index.js\";\nimport { TelegramBot } from \"./adapters/telegram/index.js\";\nimport { SlackBot as SlackBotClass } from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n getChannelSessionDir,\n getThreadSessionFile,\n resolveGenericSessionScope,\n type ResolvedSessionScope,\n} from \"./session-store.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { FileUserBindingStore } from \"./bindings.js\";\nimport { parseLoginCommand } from \"./login/index.js\";\nimport { startLinkServer } from \"./login/portal.js\";\nimport { InMemoryLinkTokenStore } from \"./login/session.js\";\nimport { parseSessionViewCommand } from \"./session-view/command.js\";\nimport { resolveExistingSessionFile } from \"./session-view/service.js\";\nimport { InMemorySessionViewTokenStore } from \"./session-view/store.js\";\nimport { DockerContainerManager } from \"./provisioner.js\";\nimport { loadAgentConfig } from \"./config.js\";\nimport { SandboxError, parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { FileVaultManager } from \"./vault.js\";\nimport {\n createManagedVaultEntry,\n ensureSandboxVaultEntry,\n resolveActorVaultKey,\n} from \"./vault-routing.js\";\nimport { addLifecycleBreadcrumb, applyRunScope } from \"./sentry.js\";\nimport { ChannelStore } from \"./store.js\";\nimport { formatNothingRunning, formatStopped, formatStopping } from \"./ui-copy.js\";\nimport {\n hasMaterializedSlackBranchSession,\n resolveSlackSessionScope,\n waitForSlackBranchBootstrap,\n} from \"./adapters/slack/branch-manager.js\";\nimport * as Sentry from \"@sentry/node\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\n// Get version from package.json\nfunction getVersion(): string {\n // Try to find package.json in the dist directory or parent\n const possiblePaths = [\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"package.json\"),\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\"),\n pathJoin(process.cwd(), \"package.json\"),\n ];\n\n for (const pkgPath of possiblePaths) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n if (pkg.version) return pkg.version;\n } catch {\n // Continue to next path\n }\n }\n return \"unknown\";\n}\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;\nconst MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;\nconst MOM_LINK_URL = process.env.MOM_LINK_URL;\nconst MOM_LINK_PORT = process.env.MOM_LINK_PORT\n ? parseInt(process.env.MOM_LINK_PORT, 10)\n : MOM_LINK_URL\n ? 8181\n : undefined;\n\ninterface ParsedArgs {\n workingDir?: string;\n stateDir?: string;\n sandbox: SandboxConfig;\n downloadChannel?: string;\n showVersion?: boolean;\n}\n\nfunction parseArgs(): ParsedArgs {\n const args = process.argv.slice(2);\n let sandbox: SandboxConfig = { type: \"host\" };\n let workingDir: string | undefined;\n let stateDirArg: string | undefined;\n let downloadChannelId: string | undefined;\n let showVersion = false;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\") {\n showVersion = true;\n } else if (arg.startsWith(\"--sandbox=\")) {\n sandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n } else if (arg === \"--sandbox\") {\n sandbox = parseSandboxArg(args[++i] || \"\");\n } else if (arg.startsWith(\"--state-dir=\")) {\n stateDirArg = arg.slice(\"--state-dir=\".length);\n } else if (arg === \"--state-dir\") {\n stateDirArg = args[++i];\n } else if (arg.startsWith(\"--download=\")) {\n downloadChannelId = arg.slice(\"--download=\".length);\n } else if (arg === \"--download\") {\n downloadChannelId = args[++i];\n } else if (!arg.startsWith(\"-\")) {\n workingDir = arg;\n }\n }\n\n return {\n workingDir: workingDir ? resolve(workingDir) : undefined,\n stateDir: stateDirArg ? resolve(stateDirArg) : undefined,\n sandbox,\n downloadChannel: downloadChannelId,\n showVersion,\n };\n}\n\nconst WORLD_WRITABLE_MODE = 0o002;\n\nfunction ensureSecureStateDir(path: string): void {\n let stat;\n try {\n stat = statSync(path);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n mkdirSync(path, { recursive: true, mode: 0o700 });\n return;\n }\n console.error(`Error: cannot access --state-dir ${path}: ${(err as Error).message}`);\n process.exit(1);\n }\n\n if (!stat.isDirectory()) {\n console.error(`Error: --state-dir ${path} exists but is not a directory`);\n process.exit(1);\n }\n\n if (stat.mode & WORLD_WRITABLE_MODE) {\n console.error(\n `Error: --state-dir ${path} is world-writable (mode ${(stat.mode & 0o777).toString(8)}). ` +\n `Credentials stored there would be exposed to other local users. ` +\n `Fix with: chmod 0700 ${path}`,\n );\n process.exit(1);\n }\n\n const euid = typeof process.geteuid === \"function\" ? process.geteuid() : undefined;\n if (euid !== undefined && stat.uid !== euid) {\n console.error(\n `Error: --state-dir ${path} is owned by uid ${stat.uid} but mama is running as uid ${euid}. ` +\n `Run mama as the directory owner or point --state-dir at a directory you own.`,\n );\n process.exit(1);\n }\n}\n\nfunction handleStartupError(error: unknown): never {\n if (error instanceof SandboxError) {\n for (const line of error.formatForCli()) {\n console.error(line);\n }\n process.exit(1);\n }\n throw error;\n}\n\nlet parsedArgs: ParsedArgs;\ntry {\n parsedArgs = parseArgs();\n} catch (error) {\n handleStartupError(error);\n}\n\n// Handle --version\nif (parsedArgs.showVersion) {\n console.log(getVersion());\n process.exit(0);\n}\n\n// Handle --download mode (Slack only)\nif (parsedArgs.downloadChannel) {\n if (!MOM_SLACK_BOT_TOKEN) {\n console.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n process.exit(1);\n }\n await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n process.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n console.error(\n \"Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>] <working-directory>\",\n );\n console.error(\" mama --download <channel-id>\");\n process.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\nconst stateDir = parsedArgs.stateDir ?? join(homedir(), \".mama\");\nprocess.env.MAMA_STATE_DIR = stateDir;\nensureSecureStateDir(stateDir);\n\n// Validate platform tokens\nconst hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);\nconst hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;\nconst hasDiscord = !!MOM_DISCORD_BOT_TOKEN;\n\nif (!hasSlack && !hasTelegram && !hasDiscord) {\n console.error(\n \"No platform tokens found. Set one of:\\n\" +\n \" Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\\n\" +\n \" Telegram: MOM_TELEGRAM_BOT_TOKEN\\n\" +\n \" Discord: MOM_DISCORD_BOT_TOKEN\",\n );\n process.exit(1);\n}\n\ntry {\n await validateSandbox(sandbox);\n} catch (error) {\n handleStartupError(error);\n}\n\nconst vaultManager = new FileVaultManager(stateDir);\nif (vaultManager.isEnabled()) {\n console.log(\n sandbox.type === \"container\"\n ? \" Vault system enabled. Container vault active.\"\n : sandbox.type === \"image\" || sandbox.type === \"firecracker\"\n ? \" Vault system enabled. Per-user credential routing active.\"\n : \" Vault system enabled. Host mode will not inject vault env.\",\n );\n}\n\nconst bindingStore = new FileUserBindingStore(stateDir);\nif (bindingStore.isEnabled()) {\n console.log(\n sandbox.type === \"container\"\n ? \" Binding store enabled. Container mode uses the container vault.\"\n : sandbox.type === \"image\" || sandbox.type === \"firecracker\"\n ? \" Binding store enabled. Platform user → vault routing active.\"\n : \" Binding store enabled. Host mode will not inject vault env.\",\n );\n}\n\nconst startupConfig = loadAgentConfig(workingDir);\nconst sandboxLimits =\n startupConfig.sandboxCpus || startupConfig.sandboxMemory\n ? { cpus: startupConfig.sandboxCpus, memory: startupConfig.sandboxMemory }\n : undefined;\n\nconst provisioner =\n sandbox.type === \"image\"\n ? new DockerContainerManager(sandbox.image, workingDir, { limits: sandboxLimits })\n : undefined;\n\nconst linkTokenStore = new InMemoryLinkTokenStore();\nconst sessionViewTokenStore = new InMemorySessionViewTokenStore();\nsetInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();\nsetInterval(() => sessionViewTokenStore.purge(), 5 * 60 * 1000).unref();\n\nfunction normalizePortalBaseUrl(): string | undefined {\n if (MOM_LINK_URL) {\n return MOM_LINK_URL.replace(/\\/+$/, \"\");\n }\n if (MOM_LINK_PORT) {\n return `http://localhost:${MOM_LINK_PORT}`;\n }\n return undefined;\n}\n\nfunction isPrivateConversation(event: BotEvent): boolean {\n return event.conversationKind === \"direct\" || event.type === \"dm\";\n}\n\nfunction ensureLoginVault(platform: string, platformUserId: string): string {\n const vaultId = resolveActorVaultKey(\n sandbox,\n vaultManager,\n bindingStore,\n platform,\n platformUserId,\n );\n\n ensureSandboxVaultEntry(sandbox, vaultManager, platform, platformUserId, vaultId);\n if (sandbox.type !== \"container\" && sandbox.type !== \"image\") {\n vaultManager.addEntry(vaultId, createManagedVaultEntry(platform, platformUserId, vaultId));\n }\n\n return vaultId;\n}\n\nasync function replyWithContext(\n responseCtx: BotAdapters[\"responseCtx\"],\n text: string,\n): Promise<void> {\n await responseCtx.setTyping(false);\n await responseCtx.setWorking(false);\n await responseCtx.respond(text);\n}\n\nasync function handleLoginCommand(\n platform: string,\n platformUserId: string,\n conversationId: string,\n responseCtx: BotAdapters[\"responseCtx\"],\n commandText: string,\n privateConversation: boolean,\n): Promise<boolean> {\n const parsed = parseLoginCommand(commandText);\n if (!parsed) return false;\n\n if (!privateConversation) {\n await replyWithContext(\n responseCtx,\n \"為了保護你的憑證,`/login` 只能在與機器人的私訊中使用。請先私訊機器人,再重新執行 `/login`。\",\n );\n return true;\n }\n\n const baseUrl = normalizePortalBaseUrl();\n if (!baseUrl) {\n await replyWithContext(\n responseCtx,\n \"Login is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.\",\n );\n return true;\n }\n\n let vaultId: string;\n try {\n vaultId = ensureLoginVault(platform, platformUserId);\n } catch (error) {\n log.logWarning(\n `[${conversationId}] Failed to prepare login vault for ${platform}/${platformUserId}`,\n error instanceof Error ? error.message : String(error),\n );\n await replyWithContext(\n responseCtx,\n \"Login setup failed on the server. 請稍後重試,或聯絡管理員檢查 vault 儲存權限。\",\n );\n return true;\n }\n\n const token = linkTokenStore.create(\n platform as \"slack\" | \"discord\" | \"telegram\",\n platformUserId,\n conversationId,\n vaultId,\n \"\",\n );\n const vaultLabel = sandbox.type === \"container\" ? `container vault (${vaultId})` : \"your vault\";\n await replyWithContext(\n responseCtx,\n `Open this link to store credentials in ${vaultLabel} (expires in 15 minutes):\\n${baseUrl}/link?token=${token.token}`,\n );\n return true;\n}\n\nasync function handleSessionViewCommand(\n platform: string,\n platformUserId: string,\n conversationId: string,\n sessionKey: string,\n bot: Bot,\n responseCtx: BotAdapters[\"responseCtx\"],\n commandText: string,\n privateConversation: boolean,\n): Promise<boolean> {\n if (!parseSessionViewCommand(commandText)) return false;\n\n const allowSharedPrivateDelivery = platform === \"slack\" || platform === \"discord\";\n const sendSessionViewReply = async (text: string): Promise<void> => {\n if (privateConversation) {\n await replyWithContext(responseCtx, text);\n return;\n }\n\n if (platform === \"slack\" && bot instanceof SlackBotClass) {\n await bot.postEphemeral(conversationId, platformUserId, text);\n return;\n }\n\n if (platform === \"discord\" && bot instanceof DiscordBot) {\n await bot.sendDirectMessage(platformUserId, text);\n return;\n }\n\n await replyWithContext(responseCtx, text);\n };\n\n if (!privateConversation && !allowSharedPrivateDelivery) {\n await sendSessionViewReply(\n \"為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。\",\n );\n return true;\n }\n\n const baseUrl = normalizePortalBaseUrl();\n if (!baseUrl) {\n await sendSessionViewReply(\n \"Session viewer is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.\",\n );\n return true;\n }\n\n const sessionFile = resolveExistingSessionFile(workingDir, conversationId, sessionKey);\n if (!sessionFile) {\n await sendSessionViewReply(\n \"目前還沒有可查看的 session。先和機器人對話一次,建立 session 後再試。\",\n );\n return true;\n }\n\n const token = sessionViewTokenStore.create(\n platform as \"slack\" | \"discord\" | \"telegram\",\n platformUserId,\n conversationId,\n sessionKey,\n sessionFile,\n );\n\n const linkText = `Open this read-only session link (expires in 24 hours):\\n${baseUrl}/session?token=${token.token}`;\n await sendSessionViewReply(linkText);\n return true;\n}\n\n// ============================================================================\n// State (per conversation)\n// ============================================================================\n\ninterface ConversationState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: number;\n}\n\nconst conversationStates = new Map<string, ConversationState>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n/** Idle timeout for managed image containers (10 minutes) */\nconst IMAGE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;\n\nif (provisioner) {\n await provisioner.reconcile();\n await provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS);\n setInterval(() => provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS), IMAGE_IDLE_TIMEOUT_MS).unref();\n}\n\nasync function resolveSessionScope(\n platformName: string,\n conversationDir: string,\n sessionKey: string,\n): Promise<ResolvedSessionScope> {\n if (platformName === \"slack\") {\n return resolveSlackSessionScope({ conversationDir, sessionKey });\n }\n return resolveGenericSessionScope({ conversationDir, sessionKey });\n}\n\nasync function getState(\n conversationId: string,\n platformName: string,\n sessionKey?: string,\n): Promise<ConversationState> {\n const key = sessionKey ?? conversationId;\n let state = conversationStates.get(key);\n if (!state) {\n const conversationDir = join(workingDir, conversationId);\n const sessionScope = await resolveSessionScope(platformName, conversationDir, key);\n state = {\n running: false,\n runner: await createRunner(\n sandbox,\n key,\n conversationId,\n conversationDir,\n workingDir,\n sessionScope,\n vaultManager,\n bindingStore,\n provisioner,\n ),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n conversationStates.set(key, state);\n } else {\n state.lastAccessedAt = Date.now();\n }\n return state;\n}\n\n/**\n * Evict idle sessions from conversationStates to bound memory usage.\n * Called after each handleEvent completes.\n */\nfunction evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of conversationStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n conversationStates.delete(key);\n }\n }\n\n if (conversationStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of conversationStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = conversationStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n conversationStates.delete(idleSessions[i].key);\n }\n }\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: BotHandler = {\n isRunning(sessionKey: string): boolean {\n const state = conversationStates.get(sessionKey);\n return !!state?.running;\n },\n\n getRunningSessions() {\n const sessions: import(\"./adapter.js\").RunningSession[] = [];\n for (const [sessionKey, state] of conversationStates) {\n if (state.running && state.startedAt) {\n // Get current step from runner\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n },\n\n async handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(conversationId, formatStopping(bot));\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(conversationId, formatNothingRunning(bot));\n }\n },\n\n forceStop(sessionKey: string): void {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n },\n\n async handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n }\n\n // Conversation sessions rotate via current pointer. Thread sessions reset in place.\n const conversationDir = join(workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n createManagedSessionFileAtPath(\n getThreadSessionFile(conversationDir, sessionKey),\n conversationDir,\n );\n } else {\n createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);\n }\n\n // Remove from in-memory cache\n conversationStates.delete(sessionKey);\n\n log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);\n await bot.postMessage(conversationId, \"Conversation reset. Send a new message to start fresh.\");\n },\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n _isEvent?: boolean,\n ): Promise<void> {\n const conversationId = event.conversationId;\n\n // Don't accept new events during shutdown\n if (isShuttingDown) {\n log.logInfo(\n `[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;\n const privateConversation = isPrivateConversation(event);\n const handledLogin = await handleLoginCommand(\n adapters.platform.name,\n event.user,\n conversationId,\n adapters.responseCtx,\n event.text,\n privateConversation,\n );\n if (handledLogin) return;\n\n const handledSessionView = await handleSessionViewCommand(\n adapters.platform.name,\n event.user,\n conversationId,\n sessionKey,\n bot,\n adapters.responseCtx,\n event.text,\n privateConversation,\n );\n if (handledSessionView) return;\n\n const conversationDir = join(workingDir, conversationId);\n const waitedForParent =\n adapters.platform.name === \"slack\"\n ? await waitForSlackBranchBootstrap({\n parentSessionKey: conversationId,\n sessionKey,\n hasThreadSession: () => hasMaterializedSlackBranchSession(conversationDir, sessionKey),\n isParentRunning: () => conversationStates.get(conversationId)?.running === true,\n })\n : false;\n if (waitedForParent) {\n log.logInfo(\n `[${conversationId}] Delayed thread bootstrap until parent session sealed: ${sessionKey}`,\n );\n }\n\n const state = await getState(conversationId, adapters.platform.name, sessionKey);\n\n // Start run\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);\n\n // Wrap in-flight run tracking\n Sentry.metrics.count(\"agent.run.started\", 1, {\n attributes: { channel: conversationId },\n });\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size + 1);\n\n const runPromise = Sentry.startSpan(\n { name: \"agent.run\", op: \"agent\", attributes: { conversationId, sessionKey } },\n async () => {\n return Sentry.withScope(async (scope) => {\n const { message, responseCtx, platform } = adapters;\n applyRunScope(scope, {\n conversationId,\n sessionKey,\n messageId: message.id,\n platform: platform.name,\n userId: message.userId,\n userName: message.userName,\n threadTs: message.threadTs,\n isEvent: _isEvent,\n });\n addLifecycleBreadcrumb(\"agent.run.started\", {\n channel_id: conversationId,\n platform: platform.name,\n has_attachments: (message.attachments?.length ?? 0) > 0,\n });\n\n try {\n await responseCtx.setTyping(true);\n await responseCtx.setWorking(true);\n const result = await state.runner.run(message, responseCtx, platform);\n await responseCtx.setWorking(false);\n\n const durationMs = Date.now() - state.startedAt!;\n Sentry.metrics.distribution(\"agent.run.duration\", durationMs, {\n unit: \"millisecond\",\n attributes: {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n Sentry.metrics.count(\"agent.run.completed\", 1, {\n attributes: {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n addLifecycleBreadcrumb(\"agent.run.completed\", {\n channel_id: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n duration_ms: durationMs,\n });\n\n if (result.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(conversationId, formatStopped(bot));\n }\n }\n } catch (err) {\n scope.setContext(\"agent_run_error\", {\n conversationId,\n sessionKey,\n platform: adapters.platform.name,\n messageId: adapters.message.id,\n threadTs: adapters.message.threadTs,\n });\n Sentry.captureException(err);\n Sentry.metrics.count(\"agent.run.errors\", 1, {\n attributes: { channel: conversationId, platform: adapters.platform.name },\n });\n log.logWarning(\n `[${conversationId}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size - 1);\n evictIdleSessions();\n }\n });\n },\n );\n\n inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n inFlightRuns.delete(runPromise);\n }\n },\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nconst sandboxDesc =\n sandbox.type === \"host\"\n ? \"host\"\n : sandbox.type === \"container\"\n ? `container:${sandbox.container}`\n : sandbox.type === \"image\"\n ? `image:${sandbox.image}`\n : `firecracker:${sandbox.vmId}`;\nlog.logStartup(workingDir, sandboxDesc);\n\n// Create platform bots\nconst bots: Bot[] = [];\nconst botsByPlatform: Record<string, Bot> = {};\n\nif (hasSlack) {\n const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n const slackBot = new SlackBotClass(handler, {\n appToken: MOM_SLACK_APP_TOKEN!,\n botToken: MOM_SLACK_BOT_TOKEN!,\n workingDir,\n store: sharedStore,\n });\n bots.push(slackBot);\n botsByPlatform.slack = slackBot;\n log.logInfo(\"Platform: Slack\");\n}\nif (hasTelegram) {\n const telegramBot = new TelegramBot(handler, {\n token: MOM_TELEGRAM_BOT_TOKEN!,\n workingDir,\n });\n bots.push(telegramBot);\n botsByPlatform.telegram = telegramBot;\n log.logInfo(\"Platform: Telegram\");\n}\nif (hasDiscord) {\n const discordBot = new DiscordBot(handler, {\n token: MOM_DISCORD_BOT_TOKEN!,\n workingDir,\n });\n bots.push(discordBot);\n botsByPlatform.discord = discordBot;\n log.logInfo(\"Platform: Discord\");\n}\n\nif (MOM_LINK_PORT) {\n startLinkServer(\n MOM_LINK_PORT,\n linkTokenStore,\n vaultManager,\n async (platform, conversationId, message) => {\n const bot = botsByPlatform[platform];\n if (bot) await bot.postMessage(conversationId, message);\n },\n sessionViewTokenStore,\n );\n}\n\n// Start events watcher with explicit platform routing\nconst eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);\nconst slackBot = botsByPlatform.slack as SlackBotClass | undefined;\nif (slackBot) {\n slackBot.setEventsWatcher(eventsWatcher);\n}\neventsWatcher.start();\n\n// Handle shutdown\nasync function shutdown(): Promise<void> {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n await Sentry.close(5000);\n process.exit(0);\n}\n\nprocess.on(\"SIGINT\", shutdown);\nprocess.on(\"SIGTERM\", shutdown);\n\n// Start all bots\nawait Promise.all(\n bots.map((bot) =>\n bot.start().catch((err) => {\n log.logWarning(\"Failed to start bot\", err instanceof Error ? err.message : String(err));\n process.exit(1);\n }),\n ),\n);\n"]}
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport \"./instrument.js\";\n\nimport { join, resolve } from \"path\";\nimport { mkdirSync, readFileSync, statSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { fileURLToPath } from \"url\";\nimport { dirname, join as pathJoin } from \"path\";\nimport type { Bot, BotAdapters, BotEvent, BotHandler } from \"./adapter.js\";\nimport { DiscordBot } from \"./adapters/discord/index.js\";\nimport { TelegramBot } from \"./adapters/telegram/index.js\";\nimport { SlackBot as SlackBotClass } from \"./adapters/slack/index.js\";\nimport { type AgentRunner, createRunner } from \"./agent.js\";\nimport {\n createManagedSessionFile,\n createManagedSessionFileAtPath,\n getChannelSessionDir,\n getThreadSessionFile,\n resolveGenericSessionScope,\n type ResolvedSessionScope,\n} from \"./session-store.js\";\nimport { downloadChannel } from \"./download.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport { FileUserBindingStore } from \"./bindings.js\";\nimport { parseLoginCommand } from \"./login/index.js\";\nimport { startLinkServer } from \"./login/portal.js\";\nimport { InMemoryLinkTokenStore } from \"./login/session.js\";\nimport { parseSessionViewCommand } from \"./session-view/command.js\";\nimport { resolveExistingSessionFile } from \"./session-view/service.js\";\nimport { InMemorySessionViewTokenStore } from \"./session-view/store.js\";\nimport { DockerContainerManager } from \"./provisioner.js\";\nimport { loadAgentConfig } from \"./config.js\";\nimport { SandboxError, parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { FileVaultManager } from \"./vault.js\";\nimport {\n createManagedVaultEntry,\n ensureSandboxVaultEntry,\n resolveActorVaultKey,\n} from \"./vault-routing.js\";\nimport { addLifecycleBreadcrumb, applyRunScope } from \"./sentry.js\";\nimport { ChannelStore } from \"./store.js\";\nimport { formatNothingRunning, formatStopped, formatStopping } from \"./ui-copy.js\";\nimport {\n hasMaterializedSlackBranchSession,\n resolveSlackSessionScope,\n waitForSlackBranchBootstrap,\n} from \"./adapters/slack/branch-manager.js\";\nimport * as Sentry from \"@sentry/node\";\n\n// ============================================================================\n// Config\n// ============================================================================\n\n// Get version from package.json\nfunction getVersion(): string {\n // Try to find package.json in the dist directory or parent\n const possiblePaths = [\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"package.json\"),\n pathJoin(dirname(fileURLToPath(import.meta.url)), \"..\", \"package.json\"),\n pathJoin(process.cwd(), \"package.json\"),\n ];\n\n for (const pkgPath of possiblePaths) {\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n if (pkg.version) return pkg.version;\n } catch {\n // Continue to next path\n }\n }\n return \"unknown\";\n}\n\nconst MOM_SLACK_APP_TOKEN = process.env.MOM_SLACK_APP_TOKEN;\nconst MOM_SLACK_BOT_TOKEN = process.env.MOM_SLACK_BOT_TOKEN;\nconst MOM_TELEGRAM_BOT_TOKEN = process.env.MOM_TELEGRAM_BOT_TOKEN;\nconst MOM_DISCORD_BOT_TOKEN = process.env.MOM_DISCORD_BOT_TOKEN;\nconst MOM_LINK_URL = process.env.MOM_LINK_URL;\nconst MOM_LINK_PORT = process.env.MOM_LINK_PORT\n ? parseInt(process.env.MOM_LINK_PORT, 10)\n : MOM_LINK_URL\n ? 8181\n : undefined;\n\ninterface ParsedArgs {\n workingDir?: string;\n stateDir?: string;\n sandbox: SandboxConfig;\n downloadChannel?: string;\n showVersion?: boolean;\n}\n\nfunction parseArgs(): ParsedArgs {\n const args = process.argv.slice(2);\n let sandbox: SandboxConfig = { type: \"host\" };\n let workingDir: string | undefined;\n let stateDirArg: string | undefined;\n let downloadChannelId: string | undefined;\n let showVersion = false;\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === \"--version\" || arg === \"-v\" || arg === \"-V\") {\n showVersion = true;\n } else if (arg.startsWith(\"--sandbox=\")) {\n sandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n } else if (arg === \"--sandbox\") {\n sandbox = parseSandboxArg(args[++i] || \"\");\n } else if (arg.startsWith(\"--state-dir=\")) {\n stateDirArg = arg.slice(\"--state-dir=\".length);\n } else if (arg === \"--state-dir\") {\n stateDirArg = args[++i];\n } else if (arg.startsWith(\"--download=\")) {\n downloadChannelId = arg.slice(\"--download=\".length);\n } else if (arg === \"--download\") {\n downloadChannelId = args[++i];\n } else if (!arg.startsWith(\"-\")) {\n workingDir = arg;\n }\n }\n\n return {\n workingDir: workingDir ? resolve(workingDir) : undefined,\n stateDir: stateDirArg ? resolve(stateDirArg) : undefined,\n sandbox,\n downloadChannel: downloadChannelId,\n showVersion,\n };\n}\n\nconst WORLD_WRITABLE_MODE = 0o002;\n\nfunction ensureSecureStateDir(path: string): void {\n let stat;\n try {\n stat = statSync(path);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n mkdirSync(path, { recursive: true, mode: 0o700 });\n return;\n }\n console.error(`Error: cannot access --state-dir ${path}: ${(err as Error).message}`);\n process.exit(1);\n }\n\n if (!stat.isDirectory()) {\n console.error(`Error: --state-dir ${path} exists but is not a directory`);\n process.exit(1);\n }\n\n if (stat.mode & WORLD_WRITABLE_MODE) {\n console.error(\n `Error: --state-dir ${path} is world-writable (mode ${(stat.mode & 0o777).toString(8)}). ` +\n `Credentials stored there would be exposed to other local users. ` +\n `Fix with: chmod 0700 ${path}`,\n );\n process.exit(1);\n }\n\n const euid = typeof process.geteuid === \"function\" ? process.geteuid() : undefined;\n if (euid !== undefined && stat.uid !== euid) {\n console.error(\n `Error: --state-dir ${path} is owned by uid ${stat.uid} but mama is running as uid ${euid}. ` +\n `Run mama as the directory owner or point --state-dir at a directory you own.`,\n );\n process.exit(1);\n }\n}\n\nfunction handleStartupError(error: unknown): never {\n if (error instanceof SandboxError) {\n for (const line of error.formatForCli()) {\n console.error(line);\n }\n process.exit(1);\n }\n throw error;\n}\n\nlet parsedArgs: ParsedArgs;\ntry {\n parsedArgs = parseArgs();\n} catch (error) {\n handleStartupError(error);\n}\n\n// Handle --version\nif (parsedArgs.showVersion) {\n console.log(getVersion());\n process.exit(0);\n}\n\n// Handle --download mode (Slack only)\nif (parsedArgs.downloadChannel) {\n if (!MOM_SLACK_BOT_TOKEN) {\n console.error(\"Missing env: MOM_SLACK_BOT_TOKEN\");\n process.exit(1);\n }\n await downloadChannel(parsedArgs.downloadChannel, MOM_SLACK_BOT_TOKEN);\n process.exit(0);\n}\n\n// Normal bot mode - require working dir\nif (!parsedArgs.workingDir) {\n console.error(\n \"Usage: mama [--state-dir=<dir>] [--sandbox=host|container:<name>|image:<image>|firecracker:<vm-id>:<host-path>] <working-directory>\",\n );\n console.error(\" mama --download <channel-id>\");\n process.exit(1);\n}\n\nconst { workingDir, sandbox } = { workingDir: parsedArgs.workingDir, sandbox: parsedArgs.sandbox };\nconst stateDir = parsedArgs.stateDir ?? join(homedir(), \".mama\");\nprocess.env.MAMA_STATE_DIR = stateDir;\nensureSecureStateDir(stateDir);\n\n// Validate platform tokens\nconst hasSlack = !!(MOM_SLACK_APP_TOKEN && MOM_SLACK_BOT_TOKEN);\nconst hasTelegram = !!MOM_TELEGRAM_BOT_TOKEN;\nconst hasDiscord = !!MOM_DISCORD_BOT_TOKEN;\n\nif (!hasSlack && !hasTelegram && !hasDiscord) {\n console.error(\n \"No platform tokens found. Set one of:\\n\" +\n \" Slack: MOM_SLACK_APP_TOKEN + MOM_SLACK_BOT_TOKEN\\n\" +\n \" Telegram: MOM_TELEGRAM_BOT_TOKEN\\n\" +\n \" Discord: MOM_DISCORD_BOT_TOKEN\",\n );\n process.exit(1);\n}\n\ntry {\n await validateSandbox(sandbox);\n} catch (error) {\n handleStartupError(error);\n}\n\nconst vaultManager = new FileVaultManager(stateDir);\nif (vaultManager.isEnabled()) {\n console.log(\n sandbox.type === \"container\"\n ? \" Vault system enabled. Container vault active.\"\n : sandbox.type === \"image\" || sandbox.type === \"firecracker\"\n ? \" Vault system enabled. Per-user credential routing active.\"\n : \" Vault system enabled. Host mode will not inject vault env.\",\n );\n}\n\nconst bindingStore = new FileUserBindingStore(stateDir);\nif (bindingStore.isEnabled()) {\n console.log(\n sandbox.type === \"container\"\n ? \" Binding store enabled. Container mode uses the container vault.\"\n : sandbox.type === \"image\" || sandbox.type === \"firecracker\"\n ? \" Binding store enabled. Platform user → vault routing active.\"\n : \" Binding store enabled. Host mode will not inject vault env.\",\n );\n}\n\nconst startupConfig = loadAgentConfig(workingDir);\nconst sandboxLimits =\n startupConfig.sandboxCpus || startupConfig.sandboxMemory\n ? { cpus: startupConfig.sandboxCpus, memory: startupConfig.sandboxMemory }\n : undefined;\n\nconst provisioner =\n sandbox.type === \"image\"\n ? new DockerContainerManager(sandbox.image, workingDir, { limits: sandboxLimits })\n : undefined;\n\nconst linkTokenStore = new InMemoryLinkTokenStore();\nconst sessionViewTokenStore = new InMemorySessionViewTokenStore();\nsetInterval(() => linkTokenStore.purge(), 5 * 60 * 1000).unref();\nsetInterval(() => sessionViewTokenStore.purge(), 5 * 60 * 1000).unref();\n\nfunction portalBaseUrl(): string | undefined {\n if (MOM_LINK_URL) return MOM_LINK_URL.replace(/\\/+$/, \"\");\n if (MOM_LINK_PORT) return `http://localhost:${MOM_LINK_PORT}`;\n return undefined;\n}\n\nfunction isPrivateConversation(event: BotEvent): boolean {\n return event.conversationKind === \"direct\" || event.type === \"dm\";\n}\n\nfunction ensureLoginVault(platform: string, platformUserId: string): string {\n const vaultId = resolveActorVaultKey(\n sandbox,\n vaultManager,\n bindingStore,\n platform,\n platformUserId,\n );\n\n ensureSandboxVaultEntry(sandbox, vaultManager, platform, platformUserId, vaultId);\n if (sandbox.type !== \"container\" && sandbox.type !== \"image\") {\n vaultManager.addEntry(vaultId, createManagedVaultEntry(platform, platformUserId, vaultId));\n }\n\n return vaultId;\n}\n\nasync function replyWithContext(\n responseCtx: BotAdapters[\"responseCtx\"],\n text: string,\n): Promise<void> {\n await responseCtx.setTyping(false);\n await responseCtx.setWorking(false);\n await responseCtx.respond(text);\n}\n\nasync function handleLoginCommand(\n platform: string,\n platformUserId: string,\n conversationId: string,\n responseCtx: BotAdapters[\"responseCtx\"],\n commandText: string,\n privateConversation: boolean,\n): Promise<boolean> {\n const parsed = parseLoginCommand(commandText);\n if (!parsed) return false;\n\n if (!privateConversation) {\n await replyWithContext(\n responseCtx,\n \"為了保護你的憑證,`/login` 只能在與機器人的私訊中使用。請先私訊機器人,再重新執行 `/login`。\",\n );\n return true;\n }\n\n const baseUrl = portalBaseUrl();\n if (!baseUrl) {\n await replyWithContext(\n responseCtx,\n \"Login is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.\",\n );\n return true;\n }\n\n let vaultId: string;\n try {\n vaultId = ensureLoginVault(platform, platformUserId);\n } catch (error) {\n log.logWarning(\n `[${conversationId}] Failed to prepare login vault for ${platform}/${platformUserId}`,\n error instanceof Error ? error.message : String(error),\n );\n await replyWithContext(\n responseCtx,\n \"Login setup failed on the server. 請稍後重試,或聯絡管理員檢查 vault 儲存權限。\",\n );\n return true;\n }\n\n const token = linkTokenStore.create(\n platform as \"slack\" | \"discord\" | \"telegram\",\n platformUserId,\n conversationId,\n vaultId,\n \"\",\n );\n const vaultLabel = sandbox.type === \"container\" ? `container vault (${vaultId})` : \"your vault\";\n await replyWithContext(\n responseCtx,\n `Open this link to store credentials in ${vaultLabel} (expires in 15 minutes):\\n${baseUrl}/link?token=${token.token}`,\n );\n return true;\n}\n\nasync function handleSessionViewCommand(\n platform: string,\n platformUserId: string,\n conversationId: string,\n sessionKey: string,\n bot: Bot,\n responseCtx: BotAdapters[\"responseCtx\"],\n commandText: string,\n privateConversation: boolean,\n): Promise<boolean> {\n if (!parseSessionViewCommand(commandText)) return false;\n\n const allowSharedPrivateDelivery = platform === \"slack\" || platform === \"discord\";\n const sendSessionViewReply = async (text: string): Promise<void> => {\n if (privateConversation) {\n await replyWithContext(responseCtx, text);\n return;\n }\n\n if (platform === \"slack\" && bot instanceof SlackBotClass) {\n await bot.postEphemeral(conversationId, platformUserId, text);\n return;\n }\n\n if (platform === \"discord\" && bot instanceof DiscordBot) {\n await bot.sendDirectMessage(platformUserId, text);\n return;\n }\n\n await replyWithContext(responseCtx, text);\n };\n\n if (!privateConversation && !allowSharedPrivateDelivery) {\n await sendSessionViewReply(\n \"為了保護對話內容,`/session` 目前只能在與機器人的私訊 / DM 中使用。\",\n );\n return true;\n }\n\n const baseUrl = portalBaseUrl();\n if (!baseUrl) {\n await sendSessionViewReply(\n \"Session viewer is not configured. Set `MOM_LINK_URL` or `MOM_LINK_PORT` on the server.\",\n );\n return true;\n }\n\n const sessionFile = resolveExistingSessionFile(workingDir, conversationId, sessionKey);\n if (!sessionFile) {\n await sendSessionViewReply(\n \"目前還沒有可查看的 session。先和機器人對話一次,建立 session 後再試。\",\n );\n return true;\n }\n\n const token = sessionViewTokenStore.create(\n platform as \"slack\" | \"discord\" | \"telegram\",\n platformUserId,\n conversationId,\n sessionKey,\n sessionFile,\n );\n\n const linkText = `Open this read-only session link (expires in 24 hours):\\n${baseUrl}/session?token=${token.token}`;\n await sendSessionViewReply(linkText);\n return true;\n}\n\n// ============================================================================\n// State (per conversation)\n// ============================================================================\n\ninterface ConversationState {\n running: boolean;\n runner: AgentRunner;\n stopRequested: boolean;\n stopMessageTs?: string;\n lastAccessedAt: number;\n startedAt?: number;\n lastActivityAt?: number;\n}\n\nconst conversationStates = new Map<string, ConversationState>();\n\n/** Track in-flight runs for graceful shutdown */\nconst inFlightRuns = new Set<Promise<void>>();\n\n/** Flag to stop accepting new events during shutdown */\nlet isShuttingDown = false;\n\n/** Maximum number of cached sessions */\nconst MAX_SESSIONS = 500;\n/** Idle timeout before a non-running session can be evicted (1 hour) */\nconst IDLE_TIMEOUT_MS = 3600000;\n/** Idle timeout for managed image containers (10 minutes) */\nconst IMAGE_IDLE_TIMEOUT_MS = 10 * 60 * 1000;\n\nif (provisioner) {\n await provisioner.reconcile();\n await provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS);\n setInterval(() => provisioner.stopIdle(IMAGE_IDLE_TIMEOUT_MS), IMAGE_IDLE_TIMEOUT_MS).unref();\n}\n\nasync function resolveSessionScope(\n platformName: string,\n conversationDir: string,\n sessionKey: string,\n): Promise<ResolvedSessionScope> {\n if (platformName === \"slack\") {\n return resolveSlackSessionScope({ conversationDir, sessionKey });\n }\n return resolveGenericSessionScope({ conversationDir, sessionKey });\n}\n\nasync function getState(\n conversationId: string,\n platformName: string,\n sessionKey?: string,\n): Promise<ConversationState> {\n const key = sessionKey ?? conversationId;\n let state = conversationStates.get(key);\n if (!state) {\n const conversationDir = join(workingDir, conversationId);\n const sessionScope = await resolveSessionScope(platformName, conversationDir, key);\n state = {\n running: false,\n runner: await createRunner(\n sandbox,\n key,\n conversationId,\n conversationDir,\n workingDir,\n sessionScope,\n vaultManager,\n bindingStore,\n provisioner,\n ),\n stopRequested: false,\n lastAccessedAt: Date.now(),\n };\n conversationStates.set(key, state);\n } else {\n state.lastAccessedAt = Date.now();\n }\n return state;\n}\n\n/**\n * Evict idle sessions from conversationStates to bound memory usage.\n * Called after each handleEvent completes.\n */\nfunction evictIdleSessions(): void {\n const now = Date.now();\n\n for (const [key, state] of conversationStates) {\n if (!state.running && now - state.lastAccessedAt > IDLE_TIMEOUT_MS) {\n conversationStates.delete(key);\n }\n }\n\n if (conversationStates.size > MAX_SESSIONS) {\n const idleSessions: Array<{ key: string; lastAccessedAt: number }> = [];\n for (const [key, state] of conversationStates) {\n if (!state.running) {\n idleSessions.push({ key, lastAccessedAt: state.lastAccessedAt });\n }\n }\n\n idleSessions.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);\n\n const toEvict = conversationStates.size - MAX_SESSIONS;\n for (let i = 0; i < toEvict && i < idleSessions.length; i++) {\n conversationStates.delete(idleSessions[i].key);\n }\n }\n}\n\n// ============================================================================\n// Handler\n// ============================================================================\n\nconst handler: BotHandler = {\n isRunning(sessionKey: string): boolean {\n const state = conversationStates.get(sessionKey);\n return !!state?.running;\n },\n\n getRunningSessions() {\n const sessions: import(\"./adapter.js\").RunningSession[] = [];\n for (const [sessionKey, state] of conversationStates) {\n if (state.running && state.startedAt) {\n // Get current step from runner\n const currentStep = state.runner.getCurrentStep();\n sessions.push({\n sessionKey,\n startedAt: state.startedAt,\n lastActivityAt: state.lastActivityAt,\n currentTool: currentStep?.label || currentStep?.toolName,\n });\n }\n }\n return sessions;\n },\n\n async handleStop(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n const ts = await bot.postMessage(conversationId, formatStopping(bot));\n state.stopMessageTs = ts;\n } else {\n await bot.postMessage(conversationId, formatNothingRunning(bot));\n }\n },\n\n forceStop(sessionKey: string): void {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n log.logInfo(`[Force Stop] Force stopping session: ${sessionKey}`);\n state.stopRequested = true;\n state.runner.abort();\n state.running = false;\n }\n },\n\n async handleNew(sessionKey: string, conversationId: string, bot: Bot): Promise<void> {\n const state = conversationStates.get(sessionKey);\n if (state?.running) {\n state.stopRequested = true;\n state.runner.abort();\n }\n\n // Conversation sessions rotate via current pointer. Thread sessions reset in place.\n const conversationDir = join(workingDir, conversationId);\n if (sessionKey.includes(\":\")) {\n createManagedSessionFileAtPath(\n getThreadSessionFile(conversationDir, sessionKey),\n conversationDir,\n );\n } else {\n createManagedSessionFile(getChannelSessionDir(conversationDir), conversationDir);\n }\n\n // Remove from in-memory cache\n conversationStates.delete(sessionKey);\n\n log.logInfo(`[${conversationId}] Session reset: ${sessionKey}`);\n await bot.postMessage(conversationId, \"Conversation reset. Send a new message to start fresh.\");\n },\n\n async handleEvent(\n event: BotEvent,\n bot: Bot,\n adapters: BotAdapters,\n _isEvent?: boolean,\n ): Promise<void> {\n const conversationId = event.conversationId;\n\n // Don't accept new events during shutdown\n if (isShuttingDown) {\n log.logInfo(\n `[${conversationId}] Rejected event during shutdown: ${event.text.substring(0, 50)}`,\n );\n return;\n }\n\n const sessionKey = event.sessionKey ?? `${conversationId}:${event.thread_ts ?? event.ts}`;\n const privateConversation = isPrivateConversation(event);\n const handledLogin = await handleLoginCommand(\n adapters.platform.name,\n event.user,\n conversationId,\n adapters.responseCtx,\n event.text,\n privateConversation,\n );\n if (handledLogin) return;\n\n const handledSessionView = await handleSessionViewCommand(\n adapters.platform.name,\n event.user,\n conversationId,\n sessionKey,\n bot,\n adapters.responseCtx,\n event.text,\n privateConversation,\n );\n if (handledSessionView) return;\n\n const conversationDir = join(workingDir, conversationId);\n const waitedForParent =\n adapters.platform.name === \"slack\"\n ? await waitForSlackBranchBootstrap({\n parentSessionKey: conversationId,\n sessionKey,\n hasThreadSession: () => hasMaterializedSlackBranchSession(conversationDir, sessionKey),\n isParentRunning: () => conversationStates.get(conversationId)?.running === true,\n })\n : false;\n if (waitedForParent) {\n log.logInfo(\n `[${conversationId}] Delayed thread bootstrap until parent session sealed: ${sessionKey}`,\n );\n }\n\n const state = await getState(conversationId, adapters.platform.name, sessionKey);\n\n // Start run\n state.running = true;\n state.stopRequested = false;\n state.startedAt = Date.now();\n state.lastActivityAt = Date.now();\n\n log.logInfo(`[${conversationId}] Starting run: ${event.text.substring(0, 50)}`);\n\n // Wrap in-flight run tracking\n Sentry.metrics.count(\"agent.run.started\", 1, {\n attributes: { channel: conversationId },\n });\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size + 1);\n\n const runPromise = Sentry.startSpan(\n { name: \"agent.run\", op: \"agent\", attributes: { conversationId, sessionKey } },\n async () => {\n return Sentry.withScope(async (scope) => {\n const { message, responseCtx, platform } = adapters;\n applyRunScope(scope, {\n conversationId,\n sessionKey,\n messageId: message.id,\n platform: platform.name,\n userId: message.userId,\n userName: message.userName,\n threadTs: message.threadTs,\n isEvent: _isEvent,\n });\n addLifecycleBreadcrumb(\"agent.run.started\", {\n channel_id: conversationId,\n platform: platform.name,\n has_attachments: (message.attachments?.length ?? 0) > 0,\n });\n\n try {\n await responseCtx.setTyping(true);\n await responseCtx.setWorking(true);\n const result = await state.runner.run(message, responseCtx, platform);\n await responseCtx.setWorking(false);\n\n const durationMs = Date.now() - state.startedAt!;\n Sentry.metrics.distribution(\"agent.run.duration\", durationMs, {\n unit: \"millisecond\",\n attributes: {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n Sentry.metrics.count(\"agent.run.completed\", 1, {\n attributes: {\n channel: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n },\n });\n addLifecycleBreadcrumb(\"agent.run.completed\", {\n channel_id: conversationId,\n platform: platform.name,\n stop_reason: result.stopReason,\n duration_ms: durationMs,\n });\n\n if (result.stopReason === \"aborted\" && state.stopRequested) {\n if (state.stopMessageTs) {\n await bot.updateMessage(conversationId, state.stopMessageTs, formatStopped(bot));\n state.stopMessageTs = undefined;\n } else {\n await bot.postMessage(conversationId, formatStopped(bot));\n }\n }\n } catch (err) {\n scope.setContext(\"agent_run_error\", {\n conversationId,\n sessionKey,\n platform: adapters.platform.name,\n messageId: adapters.message.id,\n threadTs: adapters.message.threadTs,\n });\n Sentry.captureException(err);\n Sentry.metrics.count(\"agent.run.errors\", 1, {\n attributes: { channel: conversationId, platform: adapters.platform.name },\n });\n log.logWarning(\n `[${conversationId}] Run error`,\n err instanceof Error ? err.message : String(err),\n );\n } finally {\n state.running = false;\n state.lastAccessedAt = Date.now();\n Sentry.metrics.gauge(\"agent.sessions.active\", inFlightRuns.size - 1);\n evictIdleSessions();\n }\n });\n },\n );\n\n inFlightRuns.add(runPromise);\n try {\n await runPromise;\n } finally {\n inFlightRuns.delete(runPromise);\n }\n },\n};\n\n// ============================================================================\n// Start\n// ============================================================================\n\nconst sandboxDesc =\n sandbox.type === \"host\"\n ? \"host\"\n : sandbox.type === \"container\"\n ? `container:${sandbox.container}`\n : sandbox.type === \"image\"\n ? `image:${sandbox.image}`\n : `firecracker:${sandbox.vmId}`;\nlog.logStartup(workingDir, sandboxDesc);\n\n// Create platform bots\nconst bots: Bot[] = [];\nconst botsByPlatform: Record<string, Bot> = {};\n\nif (hasSlack) {\n const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });\n const slackBot = new SlackBotClass(handler, {\n appToken: MOM_SLACK_APP_TOKEN!,\n botToken: MOM_SLACK_BOT_TOKEN!,\n workingDir,\n store: sharedStore,\n });\n bots.push(slackBot);\n botsByPlatform.slack = slackBot;\n log.logInfo(\"Platform: Slack\");\n}\nif (hasTelegram) {\n const telegramBot = new TelegramBot(handler, {\n token: MOM_TELEGRAM_BOT_TOKEN!,\n workingDir,\n });\n bots.push(telegramBot);\n botsByPlatform.telegram = telegramBot;\n log.logInfo(\"Platform: Telegram\");\n}\nif (hasDiscord) {\n const discordBot = new DiscordBot(handler, {\n token: MOM_DISCORD_BOT_TOKEN!,\n workingDir,\n });\n bots.push(discordBot);\n botsByPlatform.discord = discordBot;\n log.logInfo(\"Platform: Discord\");\n}\n\nif (MOM_LINK_PORT) {\n startLinkServer(\n MOM_LINK_PORT,\n linkTokenStore,\n vaultManager,\n async (platform, conversationId, message) => {\n const bot = botsByPlatform[platform];\n if (bot) await bot.postMessage(conversationId, message);\n },\n sessionViewTokenStore,\n );\n}\n\n// Start events watcher with explicit platform routing\nconst eventsWatcher = createEventsWatcher(workingDir, botsByPlatform);\nconst slackBot = botsByPlatform.slack as SlackBotClass | undefined;\nif (slackBot) {\n slackBot.setEventsWatcher(eventsWatcher);\n}\neventsWatcher.start();\n\n// Handle shutdown\nasync function shutdown(): Promise<void> {\n if (isShuttingDown) return;\n isShuttingDown = true;\n log.logInfo(\"Shutting down gracefully...\");\n\n const timeout = Date.now() + 30000;\n while (inFlightRuns.size > 0 && Date.now() < timeout) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n\n if (inFlightRuns.size > 0) {\n log.logWarning(`Forcing exit with ${inFlightRuns.size} runs still in progress`);\n }\n\n eventsWatcher.stop();\n await Sentry.close(5000);\n process.exit(0);\n}\n\nprocess.on(\"SIGINT\", shutdown);\nprocess.on(\"SIGTERM\", shutdown);\n\n// Start all bots\nawait Promise.all(\n bots.map((bot) =>\n bot.start().catch((err) => {\n log.logWarning(\"Failed to start bot\", err instanceof Error ? err.message : String(err));\n process.exit(1);\n }),\n ),\n);\n"]}
|
package/dist/session-store.d.ts
CHANGED
|
@@ -47,11 +47,15 @@ export declare function extractSessionSuffix(sessionKey: string): string;
|
|
|
47
47
|
/**
|
|
48
48
|
* Creates an empty timestamped file and updates the "current" pointer.
|
|
49
49
|
* Used only by tests for placeholder-file scenarios.
|
|
50
|
+
*
|
|
51
|
+
* Order matters: write the session file first, then atomic-rename the pointer
|
|
52
|
+
* last so a crash mid-create never leaves "current" pointing at a missing file.
|
|
50
53
|
*/
|
|
51
54
|
export declare function createNewSessionFile(sessionDir: string): string;
|
|
52
55
|
/**
|
|
53
56
|
* Creates a new persistent session file with a proper SessionManager header and cwd.
|
|
54
|
-
* Also updates the "current" pointer.
|
|
57
|
+
* Also updates the "current" pointer. Header is written before the pointer flips so a
|
|
58
|
+
* partial create cannot leave "current" pointing at a missing file.
|
|
55
59
|
*/
|
|
56
60
|
export declare function createManagedSessionFile(sessionDir: string, cwd: string): string;
|
|
57
61
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,YAAY,WAAW,EAAE,MAAM,EAG9B;CACF;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAcD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIjF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAS/D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQhF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,GACV,cAAc,CAShB;AAQD;;GAEG;AACH,wBAAgB,8BAA8B,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGvF;AAeD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iCAAiC,GACzC,oBAAoB,CAoBtB;AA4HD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI1E;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE1E;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE3E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,iBAAiB,EAAE,MAAM,EACzB,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM,GACV,MAAM,CAWR;AAED,wBAAgB,sCAAsC,CACpD,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,iBAAiB,EAC9B,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAiCR;AAED,wBAAgB,oCAAoC,CAClD,iBAAiB,EAAE,MAAM,EACzB,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,iBAAiB,GAC7B,MAAM,CAqBR","sourcesContent":["import { randomUUID } from \"crypto\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { SessionManager } from \"@mariozechner/pi-coding-agent\";\n\nexport class ThreadRootNotFoundError extends Error {\n constructor(sessionFile: string) {\n super(`Thread root message not found in source session: ${sessionFile}`);\n this.name = \"ThreadRootNotFoundError\";\n }\n}\n\nexport interface ThreadRootMessage {\n text?: string;\n userName?: string;\n user?: string;\n loggedAt?: number;\n}\n\nexport interface ResolvedSessionScope {\n sessionDir: string;\n contextFile: string;\n threadRootMessage: ThreadRootMessage | null;\n}\n\nexport interface ResolveGenericSessionScopeOptions {\n conversationDir: string;\n sessionKey: string;\n}\n\ninterface SessionMessageEntryLike {\n type: string;\n id: string;\n parentId: string | null;\n timestamp: string;\n message?: {\n role?: string;\n timestamp?: number;\n content?: Array<{ type?: string; text?: string }> | string;\n };\n}\n\n/**\n * Returns the shared session directory for a conversation.\n * Channel sessions use a current pointer within this directory.\n * Thread sessions are stored as fixed files within the same directory.\n */\nexport function getChannelSessionDir(channelDir: string): string {\n return join(channelDir, \"sessions\");\n}\n\n/**\n * Resolves the current active session file for a session directory.\n * Reads the \"current\" pointer file; creates a new session if none exists\n * or the pointed-to file is missing.\n */\nexport function resolveSessionFile(sessionDir: string): string {\n const existing = tryResolveCurrentSession(sessionDir);\n if (existing) return existing;\n return createNewSessionFile(sessionDir);\n}\n\n/**\n * Resolve the current active session file for a session directory.\n * Creates a fully initialized persistent session with the provided cwd when none exists.\n */\nexport function resolveManagedSessionFile(sessionDir: string, cwd: string): string {\n const existingPath = getCurrentSessionPath(sessionDir);\n if (existingPath) return existingPath;\n return createManagedSessionFile(sessionDir, cwd);\n}\n\n/**\n * Extracts the short UUID from a session file path.\n * e.g. \"2026-04-05T00-00_7b54cf90.jsonl\" → \"7b54cf90\"\n */\nexport function extractSessionUuid(sessionFile: string): string {\n const base = sessionFile.split(\"/\").pop() ?? sessionFile;\n return base.replace(\".jsonl\", \"\").split(\"_\").pop() ?? base;\n}\n\n/**\n * Extracts the thread/suffix part of a session key.\n * \"channelId:threadId\" → \"threadId\", \"channelId\" → \"channelId\"\n */\nexport function extractSessionSuffix(sessionKey: string): string {\n return sessionKey.includes(\":\") ? sessionKey.split(\":\").pop()! : sessionKey;\n}\n\n/**\n * Creates an empty timestamped file and updates the \"current\" pointer.\n * Used only by tests for placeholder-file scenarios.\n */\nexport function createNewSessionFile(sessionDir: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const uuid = randomUUID().slice(0, 8);\n const filename = `${timestamp}_${uuid}.jsonl`;\n const filePath = join(sessionDir, filename);\n writeFileSync(join(sessionDir, \"current\"), filename, \"utf-8\");\n writeFileSync(filePath, \"\", \"utf-8\");\n return filePath;\n}\n\n/**\n * Creates a new persistent session file with a proper SessionManager header and cwd.\n * Also updates the \"current\" pointer.\n */\nexport function createManagedSessionFile(sessionDir: string, cwd: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const sessionId = randomUUID();\n const sessionFile = join(sessionDir, `${timestamp}_${sessionId.slice(0, 8)}.jsonl`);\n writeSessionHeader(sessionFile, cwd, sessionId);\n setCurrentPointer(sessionDir, sessionFile);\n return sessionFile;\n}\n\n/**\n * Open a session file with an explicit cwd, even if the file does not exist yet.\n * This avoids SessionManager.open() falling back to process.cwd() for fresh sessions.\n */\nexport function openManagedSession(\n sessionFile: string,\n sessionDir: string,\n cwd: string,\n): SessionManager {\n if (shouldRecreatePreinitializedSession(sessionFile)) {\n rmSync(sessionFile, { force: true });\n }\n\n const SessionManagerCtor = SessionManager as unknown as {\n new (cwd: string, sessionDir: string, sessionFile: string, persist: boolean): SessionManager;\n };\n return new SessionManagerCtor(cwd, sessionDir, sessionFile, true);\n}\n\nfunction setCurrentPointer(sessionDir: string, sessionFilePath: string): void {\n const filename = sessionFilePath.split(\"/\").pop()!;\n mkdirSync(sessionDir, { recursive: true });\n writeFileSync(join(sessionDir, \"current\"), filename, \"utf-8\");\n}\n\n/**\n * Creates or overwrites a fixed-path session file with a valid session header.\n */\nexport function createManagedSessionFileAtPath(sessionFile: string, cwd: string): string {\n writeSessionHeader(sessionFile, cwd);\n return sessionFile;\n}\n\nfunction writeSessionHeader(sessionFile: string, cwd: string, sessionId = randomUUID()): void {\n const sessionDir = getFileDir(sessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const header = {\n type: \"session\",\n version: 3,\n id: sessionId,\n timestamp: new Date().toISOString(),\n cwd,\n };\n writeFileSync(sessionFile, `${JSON.stringify(header)}\\n`, \"utf-8\");\n}\n\n/**\n * Returns the fixed session file path for a Slack thread.\n */\nexport function getThreadSessionFile(channelDir: string, sessionKey: string): string {\n return join(getChannelSessionDir(channelDir), `${extractSessionSuffix(sessionKey)}.jsonl`);\n}\n\n/**\n * Resolve the default session scope for platforms without Slack-style branch forking.\n * Top-level/private sessions use the conversation's current pointer. Threaded or\n * per-message sessions use a fixed file derived from the session key suffix.\n */\nexport function resolveGenericSessionScope(\n options: ResolveGenericSessionScopeOptions,\n): ResolvedSessionScope {\n const { conversationDir, sessionKey } = options;\n const sessionDir = getChannelSessionDir(conversationDir);\n\n if (!sessionKey.includes(\":\")) {\n return {\n sessionDir,\n contextFile: resolveManagedSessionFile(sessionDir, conversationDir),\n threadRootMessage: null,\n };\n }\n\n const threadFile = getThreadSessionFile(conversationDir, sessionKey);\n return {\n sessionDir,\n contextFile:\n tryResolveThreadSession(threadFile) ??\n createManagedSessionFileAtPath(threadFile, conversationDir),\n threadRootMessage: null,\n };\n}\n\nfunction hasSessionHeader(sessionFile: string): boolean {\n try {\n const lines = readFileSync(sessionFile, \"utf-8\").split(\"\\n\");\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n const entry = JSON.parse(trimmed) as { type?: string };\n return entry.type === \"session\";\n }\n } catch {\n return false;\n }\n return false;\n}\n\nfunction shouldRecreatePreinitializedSession(sessionFile: string): boolean {\n if (!existsSync(sessionFile)) return false;\n\n try {\n const entries = readFileSync(sessionFile, \"utf-8\")\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => JSON.parse(line) as { type?: string });\n\n return entries.length === 1 && entries[0]?.type === \"session\";\n } catch {\n return false;\n }\n}\n\nfunction getFileDir(sessionFile: string): string {\n return sessionFile.substring(0, sessionFile.lastIndexOf(\"/\"));\n}\n\nfunction resolveThreadSnapshotEntries(\n sourceSessionFile: string,\n rootMessage: ThreadRootMessage,\n): SessionMessageEntryLike[] | null {\n const targetText = buildComparableRootMessageText(rootMessage);\n if (!targetText) return null;\n\n const entries = SessionManager.open(sourceSessionFile).getEntries() as SessionMessageEntryLike[];\n const matchIndex = findRootMessageIndex(entries, targetText, rootMessage.loggedAt);\n if (matchIndex === -1) return null;\n\n const nextTopLevelUserIndex = entries.findIndex(\n (entry, index) => index > matchIndex && isUserMessageEntry(entry),\n );\n const endIndex = nextTopLevelUserIndex === -1 ? entries.length : nextTopLevelUserIndex;\n return entries.slice(0, endIndex);\n}\n\nfunction findRootMessageIndex(\n entries: SessionMessageEntryLike[],\n targetText: string,\n loggedAt?: number,\n): number {\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n if (!isUserMessageEntry(entry)) continue;\n\n const comparableText = normalizeComparableUserText(getMessageText(entry));\n if (comparableText !== targetText) continue;\n\n const messageTimestamp = entry.message?.timestamp;\n if (\n loggedAt !== undefined &&\n typeof messageTimestamp === \"number\" &&\n messageTimestamp < loggedAt\n ) {\n continue;\n }\n\n return i;\n }\n\n return -1;\n}\n\nfunction isUserMessageEntry(entry: SessionMessageEntryLike): boolean {\n return entry.type === \"message\" && entry.message?.role === \"user\";\n}\n\nfunction getMessageText(entry: SessionMessageEntryLike): string {\n const content = entry.message?.content;\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n\n return content\n .filter((part): part is { type?: string; text?: string } => part.type === \"text\")\n .map((part) => part.text ?? \"\")\n .join(\"\\n\\n\");\n}\n\nfunction buildComparableRootMessageText(rootMessage: ThreadRootMessage): string | null {\n const userLabel = rootMessage.userName || rootMessage.user || \"unknown\";\n const text = rootMessage.text?.trim();\n if (!text) return null;\n return normalizeComparableUserText(`[${userLabel}]: ${text}`);\n}\n\nfunction stripSlackAttachmentBlock(text: string): string {\n return text.replace(/\\n*<slack_attachments>\\n[\\s\\S]*?\\n<\\/slack_attachments>\\s*$/g, \"\");\n}\n\nfunction normalizeComparableUserText(text: string): string {\n const withoutTimestamp = text.replace(\n /^\\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}\\]\\s+(?=\\[[^\\]]+\\](?:\\s+\\[in-thread:[^\\]]+\\])?:\\s)/,\n \"\",\n );\n return stripSlackAttachmentBlock(withoutTimestamp).trim();\n}\n\nfunction getCurrentSessionPath(sessionDir: string): string | null {\n const pointerFile = join(sessionDir, \"current\");\n if (!existsSync(pointerFile)) return null;\n const filename = readFileSync(pointerFile, \"utf-8\").trim();\n if (!filename) return null;\n return join(sessionDir, filename);\n}\n\n/**\n * Try to resolve an existing current session file.\n * Returns null if no current pointer exists or the pointed file has no valid session header.\n */\nexport function tryResolveCurrentSession(sessionDir: string): string | null {\n const fullPath = getCurrentSessionPath(sessionDir);\n if (fullPath && existsSync(fullPath) && hasSessionHeader(fullPath)) return fullPath;\n return null;\n}\n\n/**\n * Try to resolve an existing thread session file.\n * Returns the file path if found, or null if no valid thread session exists yet.\n */\nexport function tryResolveThreadSession(sessionFile: string): string | null {\n return existsSync(sessionFile) && hasSessionHeader(sessionFile) ? sessionFile : null;\n}\n\n/**\n * Resolve the channel's current session file path (for fork source).\n * Returns null if no channel session exists.\n */\nexport function resolveChannelSessionFile(channelDir: string): string | null {\n return tryResolveCurrentSession(getChannelSessionDir(channelDir));\n}\n\n/**\n * Fork a channel session into a fixed thread-session path.\n * The resulting file keeps forkFrom's distinct session/header metadata.\n */\nexport function forkThreadSessionFile(\n sourceSessionFile: string,\n targetSessionFile: string,\n cwd: string,\n): string {\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const forked = SessionManager.forkFrom(sourceSessionFile, cwd, sessionDir);\n const forkedFile = forked.getSessionFile();\n if (!forkedFile) {\n throw new Error(`Failed to fork session from ${sourceSessionFile}`);\n }\n rmSync(targetSessionFile, { force: true });\n renameSync(forkedFile, targetSessionFile);\n return targetSessionFile;\n}\n\nexport function createThreadSessionFileFromRootMessage(\n targetSessionFile: string,\n cwd: string,\n rootMessage: ThreadRootMessage,\n parentSession?: string,\n): string {\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n rmSync(targetSessionFile, { force: true });\n\n const header = {\n type: \"session\",\n version: 3,\n id: randomUUID(),\n timestamp: new Date().toISOString(),\n cwd,\n ...(parentSession ? { parentSession } : {}),\n };\n const rootText = buildComparableRootMessageText(rootMessage);\n if (!rootText) {\n writeFileSync(targetSessionFile, `${JSON.stringify(header)}\\n`, \"utf-8\");\n return targetSessionFile;\n }\n\n const rootEntry = {\n type: \"message\",\n id: randomUUID().slice(0, 8),\n parentId: null,\n timestamp: new Date().toISOString(),\n message: {\n role: \"user\",\n content: [{ type: \"text\", text: rootText }],\n ...(rootMessage.loggedAt !== undefined ? { timestamp: rootMessage.loggedAt } : {}),\n },\n };\n const content = [header, rootEntry].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n writeFileSync(targetSessionFile, `${content}\\n`, \"utf-8\");\n return targetSessionFile;\n}\n\nexport function forkThreadSessionFileFromRootMessage(\n sourceSessionFile: string,\n targetSessionFile: string,\n cwd: string,\n rootMessage: ThreadRootMessage,\n): string {\n const snapshotEntries = resolveThreadSnapshotEntries(sourceSessionFile, rootMessage);\n if (!snapshotEntries) {\n throw new ThreadRootNotFoundError(sourceSessionFile);\n }\n\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n rmSync(targetSessionFile, { force: true });\n\n const header = {\n type: \"session\",\n version: 3,\n id: randomUUID(),\n timestamp: new Date().toISOString(),\n cwd,\n parentSession: sourceSessionFile,\n };\n const content = [header, ...snapshotEntries].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n writeFileSync(targetSessionFile, `${content}\\n`, \"utf-8\");\n return targetSessionFile;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAG/D,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,YAAY,WAAW,EAAE,MAAM,EAG9B;CACF;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAcD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIjF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAS/D;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQhF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,GACV,cAAc,CAShB;AAQD;;GAEG;AACH,wBAAgB,8BAA8B,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAGvF;AAeD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,iCAAiC,GACzC,oBAAoB,CAoBtB;AA4HD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI1E;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE1E;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE3E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,iBAAiB,EAAE,MAAM,EACzB,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM,GACV,MAAM,CAWR;AAED,wBAAgB,sCAAsC,CACpD,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,iBAAiB,EAC9B,aAAa,CAAC,EAAE,MAAM,GACrB,MAAM,CAiCR;AAED,wBAAgB,oCAAoC,CAClD,iBAAiB,EAAE,MAAM,EACzB,iBAAiB,EAAE,MAAM,EACzB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,iBAAiB,GAC7B,MAAM,CAqBR","sourcesContent":["import { randomUUID } from \"crypto\";\nimport { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from \"fs\";\nimport { join } from \"path\";\nimport { SessionManager } from \"@mariozechner/pi-coding-agent\";\nimport { atomicWritePrivateFile } from \"./fs-atomic.js\";\n\nexport class ThreadRootNotFoundError extends Error {\n constructor(sessionFile: string) {\n super(`Thread root message not found in source session: ${sessionFile}`);\n this.name = \"ThreadRootNotFoundError\";\n }\n}\n\nexport interface ThreadRootMessage {\n text?: string;\n userName?: string;\n user?: string;\n loggedAt?: number;\n}\n\nexport interface ResolvedSessionScope {\n sessionDir: string;\n contextFile: string;\n threadRootMessage: ThreadRootMessage | null;\n}\n\nexport interface ResolveGenericSessionScopeOptions {\n conversationDir: string;\n sessionKey: string;\n}\n\ninterface SessionMessageEntryLike {\n type: string;\n id: string;\n parentId: string | null;\n timestamp: string;\n message?: {\n role?: string;\n timestamp?: number;\n content?: Array<{ type?: string; text?: string }> | string;\n };\n}\n\n/**\n * Returns the shared session directory for a conversation.\n * Channel sessions use a current pointer within this directory.\n * Thread sessions are stored as fixed files within the same directory.\n */\nexport function getChannelSessionDir(channelDir: string): string {\n return join(channelDir, \"sessions\");\n}\n\n/**\n * Resolves the current active session file for a session directory.\n * Reads the \"current\" pointer file; creates a new session if none exists\n * or the pointed-to file is missing.\n */\nexport function resolveSessionFile(sessionDir: string): string {\n const existing = tryResolveCurrentSession(sessionDir);\n if (existing) return existing;\n return createNewSessionFile(sessionDir);\n}\n\n/**\n * Resolve the current active session file for a session directory.\n * Creates a fully initialized persistent session with the provided cwd when none exists.\n */\nexport function resolveManagedSessionFile(sessionDir: string, cwd: string): string {\n const existingPath = getCurrentSessionPath(sessionDir);\n if (existingPath) return existingPath;\n return createManagedSessionFile(sessionDir, cwd);\n}\n\n/**\n * Extracts the short UUID from a session file path.\n * e.g. \"2026-04-05T00-00_7b54cf90.jsonl\" → \"7b54cf90\"\n */\nexport function extractSessionUuid(sessionFile: string): string {\n const base = sessionFile.split(\"/\").pop() ?? sessionFile;\n return base.replace(\".jsonl\", \"\").split(\"_\").pop() ?? base;\n}\n\n/**\n * Extracts the thread/suffix part of a session key.\n * \"channelId:threadId\" → \"threadId\", \"channelId\" → \"channelId\"\n */\nexport function extractSessionSuffix(sessionKey: string): string {\n return sessionKey.includes(\":\") ? sessionKey.split(\":\").pop()! : sessionKey;\n}\n\n/**\n * Creates an empty timestamped file and updates the \"current\" pointer.\n * Used only by tests for placeholder-file scenarios.\n *\n * Order matters: write the session file first, then atomic-rename the pointer\n * last so a crash mid-create never leaves \"current\" pointing at a missing file.\n */\nexport function createNewSessionFile(sessionDir: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const uuid = randomUUID().slice(0, 8);\n const filename = `${timestamp}_${uuid}.jsonl`;\n const filePath = join(sessionDir, filename);\n atomicWritePrivateFile(filePath, \"\");\n atomicWritePrivateFile(join(sessionDir, \"current\"), filename);\n return filePath;\n}\n\n/**\n * Creates a new persistent session file with a proper SessionManager header and cwd.\n * Also updates the \"current\" pointer. Header is written before the pointer flips so a\n * partial create cannot leave \"current\" pointing at a missing file.\n */\nexport function createManagedSessionFile(sessionDir: string, cwd: string): string {\n mkdirSync(sessionDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const sessionId = randomUUID();\n const sessionFile = join(sessionDir, `${timestamp}_${sessionId.slice(0, 8)}.jsonl`);\n writeSessionHeader(sessionFile, cwd, sessionId);\n setCurrentPointer(sessionDir, sessionFile);\n return sessionFile;\n}\n\n/**\n * Open a session file with an explicit cwd, even if the file does not exist yet.\n * This avoids SessionManager.open() falling back to process.cwd() for fresh sessions.\n */\nexport function openManagedSession(\n sessionFile: string,\n sessionDir: string,\n cwd: string,\n): SessionManager {\n if (shouldRecreatePreinitializedSession(sessionFile)) {\n rmSync(sessionFile, { force: true });\n }\n\n const SessionManagerCtor = SessionManager as unknown as {\n new (cwd: string, sessionDir: string, sessionFile: string, persist: boolean): SessionManager;\n };\n return new SessionManagerCtor(cwd, sessionDir, sessionFile, true);\n}\n\nfunction setCurrentPointer(sessionDir: string, sessionFilePath: string): void {\n const filename = sessionFilePath.split(\"/\").pop()!;\n mkdirSync(sessionDir, { recursive: true });\n atomicWritePrivateFile(join(sessionDir, \"current\"), filename);\n}\n\n/**\n * Creates or overwrites a fixed-path session file with a valid session header.\n */\nexport function createManagedSessionFileAtPath(sessionFile: string, cwd: string): string {\n writeSessionHeader(sessionFile, cwd);\n return sessionFile;\n}\n\nfunction writeSessionHeader(sessionFile: string, cwd: string, sessionId = randomUUID()): void {\n const sessionDir = getFileDir(sessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const header = {\n type: \"session\",\n version: 3,\n id: sessionId,\n timestamp: new Date().toISOString(),\n cwd,\n };\n atomicWritePrivateFile(sessionFile, `${JSON.stringify(header)}\\n`);\n}\n\n/**\n * Returns the fixed session file path for a Slack thread.\n */\nexport function getThreadSessionFile(channelDir: string, sessionKey: string): string {\n return join(getChannelSessionDir(channelDir), `${extractSessionSuffix(sessionKey)}.jsonl`);\n}\n\n/**\n * Resolve the default session scope for platforms without Slack-style branch forking.\n * Top-level/private sessions use the conversation's current pointer. Threaded or\n * per-message sessions use a fixed file derived from the session key suffix.\n */\nexport function resolveGenericSessionScope(\n options: ResolveGenericSessionScopeOptions,\n): ResolvedSessionScope {\n const { conversationDir, sessionKey } = options;\n const sessionDir = getChannelSessionDir(conversationDir);\n\n if (!sessionKey.includes(\":\")) {\n return {\n sessionDir,\n contextFile: resolveManagedSessionFile(sessionDir, conversationDir),\n threadRootMessage: null,\n };\n }\n\n const threadFile = getThreadSessionFile(conversationDir, sessionKey);\n return {\n sessionDir,\n contextFile:\n tryResolveThreadSession(threadFile) ??\n createManagedSessionFileAtPath(threadFile, conversationDir),\n threadRootMessage: null,\n };\n}\n\nfunction hasSessionHeader(sessionFile: string): boolean {\n try {\n const lines = readFileSync(sessionFile, \"utf-8\").split(\"\\n\");\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n const entry = JSON.parse(trimmed) as { type?: string };\n return entry.type === \"session\";\n }\n } catch {\n return false;\n }\n return false;\n}\n\nfunction shouldRecreatePreinitializedSession(sessionFile: string): boolean {\n if (!existsSync(sessionFile)) return false;\n\n try {\n const entries = readFileSync(sessionFile, \"utf-8\")\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => JSON.parse(line) as { type?: string });\n\n return entries.length === 1 && entries[0]?.type === \"session\";\n } catch {\n return false;\n }\n}\n\nfunction getFileDir(sessionFile: string): string {\n return sessionFile.substring(0, sessionFile.lastIndexOf(\"/\"));\n}\n\nfunction resolveThreadSnapshotEntries(\n sourceSessionFile: string,\n rootMessage: ThreadRootMessage,\n): SessionMessageEntryLike[] | null {\n const targetText = buildComparableRootMessageText(rootMessage);\n if (!targetText) return null;\n\n const entries = SessionManager.open(sourceSessionFile).getEntries() as SessionMessageEntryLike[];\n const matchIndex = findRootMessageIndex(entries, targetText, rootMessage.loggedAt);\n if (matchIndex === -1) return null;\n\n const nextTopLevelUserIndex = entries.findIndex(\n (entry, index) => index > matchIndex && isUserMessageEntry(entry),\n );\n const endIndex = nextTopLevelUserIndex === -1 ? entries.length : nextTopLevelUserIndex;\n return entries.slice(0, endIndex);\n}\n\nfunction findRootMessageIndex(\n entries: SessionMessageEntryLike[],\n targetText: string,\n loggedAt?: number,\n): number {\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n if (!isUserMessageEntry(entry)) continue;\n\n const comparableText = normalizeComparableUserText(getMessageText(entry));\n if (comparableText !== targetText) continue;\n\n const messageTimestamp = entry.message?.timestamp;\n if (\n loggedAt !== undefined &&\n typeof messageTimestamp === \"number\" &&\n messageTimestamp < loggedAt\n ) {\n continue;\n }\n\n return i;\n }\n\n return -1;\n}\n\nfunction isUserMessageEntry(entry: SessionMessageEntryLike): boolean {\n return entry.type === \"message\" && entry.message?.role === \"user\";\n}\n\nfunction getMessageText(entry: SessionMessageEntryLike): string {\n const content = entry.message?.content;\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return \"\";\n\n return content\n .filter((part): part is { type?: string; text?: string } => part.type === \"text\")\n .map((part) => part.text ?? \"\")\n .join(\"\\n\\n\");\n}\n\nfunction buildComparableRootMessageText(rootMessage: ThreadRootMessage): string | null {\n const userLabel = rootMessage.userName || rootMessage.user || \"unknown\";\n const text = rootMessage.text?.trim();\n if (!text) return null;\n return normalizeComparableUserText(`[${userLabel}]: ${text}`);\n}\n\nfunction stripSlackAttachmentBlock(text: string): string {\n return text.replace(/\\n*<slack_attachments>\\n[\\s\\S]*?\\n<\\/slack_attachments>\\s*$/g, \"\");\n}\n\nfunction normalizeComparableUserText(text: string): string {\n const withoutTimestamp = text.replace(\n /^\\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}\\]\\s+(?=\\[[^\\]]+\\](?:\\s+\\[in-thread:[^\\]]+\\])?:\\s)/,\n \"\",\n );\n return stripSlackAttachmentBlock(withoutTimestamp).trim();\n}\n\nfunction getCurrentSessionPath(sessionDir: string): string | null {\n const pointerFile = join(sessionDir, \"current\");\n if (!existsSync(pointerFile)) return null;\n const filename = readFileSync(pointerFile, \"utf-8\").trim();\n if (!filename) return null;\n return join(sessionDir, filename);\n}\n\n/**\n * Try to resolve an existing current session file.\n * Returns null if no current pointer exists or the pointed file has no valid session header.\n */\nexport function tryResolveCurrentSession(sessionDir: string): string | null {\n const fullPath = getCurrentSessionPath(sessionDir);\n if (fullPath && existsSync(fullPath) && hasSessionHeader(fullPath)) return fullPath;\n return null;\n}\n\n/**\n * Try to resolve an existing thread session file.\n * Returns the file path if found, or null if no valid thread session exists yet.\n */\nexport function tryResolveThreadSession(sessionFile: string): string | null {\n return existsSync(sessionFile) && hasSessionHeader(sessionFile) ? sessionFile : null;\n}\n\n/**\n * Resolve the channel's current session file path (for fork source).\n * Returns null if no channel session exists.\n */\nexport function resolveChannelSessionFile(channelDir: string): string | null {\n return tryResolveCurrentSession(getChannelSessionDir(channelDir));\n}\n\n/**\n * Fork a channel session into a fixed thread-session path.\n * The resulting file keeps forkFrom's distinct session/header metadata.\n */\nexport function forkThreadSessionFile(\n sourceSessionFile: string,\n targetSessionFile: string,\n cwd: string,\n): string {\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n const forked = SessionManager.forkFrom(sourceSessionFile, cwd, sessionDir);\n const forkedFile = forked.getSessionFile();\n if (!forkedFile) {\n throw new Error(`Failed to fork session from ${sourceSessionFile}`);\n }\n rmSync(targetSessionFile, { force: true });\n renameSync(forkedFile, targetSessionFile);\n return targetSessionFile;\n}\n\nexport function createThreadSessionFileFromRootMessage(\n targetSessionFile: string,\n cwd: string,\n rootMessage: ThreadRootMessage,\n parentSession?: string,\n): string {\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n rmSync(targetSessionFile, { force: true });\n\n const header = {\n type: \"session\",\n version: 3,\n id: randomUUID(),\n timestamp: new Date().toISOString(),\n cwd,\n ...(parentSession ? { parentSession } : {}),\n };\n const rootText = buildComparableRootMessageText(rootMessage);\n if (!rootText) {\n atomicWritePrivateFile(targetSessionFile, `${JSON.stringify(header)}\\n`);\n return targetSessionFile;\n }\n\n const rootEntry = {\n type: \"message\",\n id: randomUUID().slice(0, 8),\n parentId: null,\n timestamp: new Date().toISOString(),\n message: {\n role: \"user\",\n content: [{ type: \"text\", text: rootText }],\n ...(rootMessage.loggedAt !== undefined ? { timestamp: rootMessage.loggedAt } : {}),\n },\n };\n const content = [header, rootEntry].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n atomicWritePrivateFile(targetSessionFile, `${content}\\n`);\n return targetSessionFile;\n}\n\nexport function forkThreadSessionFileFromRootMessage(\n sourceSessionFile: string,\n targetSessionFile: string,\n cwd: string,\n rootMessage: ThreadRootMessage,\n): string {\n const snapshotEntries = resolveThreadSnapshotEntries(sourceSessionFile, rootMessage);\n if (!snapshotEntries) {\n throw new ThreadRootNotFoundError(sourceSessionFile);\n }\n\n const sessionDir = getFileDir(targetSessionFile);\n mkdirSync(sessionDir, { recursive: true });\n rmSync(targetSessionFile, { force: true });\n\n const header = {\n type: \"session\",\n version: 3,\n id: randomUUID(),\n timestamp: new Date().toISOString(),\n cwd,\n parentSession: sourceSessionFile,\n };\n const content = [header, ...snapshotEntries].map((entry) => JSON.stringify(entry)).join(\"\\n\");\n atomicWritePrivateFile(targetSessionFile, `${content}\\n`);\n return targetSessionFile;\n}\n"]}
|