mom: fix attachment downloads - pass store to SlackBot, process files, use absolute paths

This commit is contained in:
Mario Zechner 2025-12-11 23:55:49 +01:00
parent d277d739b3
commit e3576fe016
4 changed files with 246 additions and 107 deletions

View file

@ -842,6 +842,23 @@ export const MODELS = {
contextWindow: 200000,
maxTokens: 100000,
} satisfies Model<"openai-responses">,
"gpt-5.2-pro": {
id: "gpt-5.2-pro",
name: "GPT-5.2 Pro",
api: "openai-responses",
provider: "openai",
baseUrl: "https://api.openai.com/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 21,
output: 168,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 400000,
maxTokens: 128000,
} satisfies Model<"openai-responses">,
"gpt-4-turbo": {
id: "gpt-4-turbo",
name: "GPT-4 Turbo",
@ -893,6 +910,23 @@ export const MODELS = {
contextWindow: 200000,
maxTokens: 100000,
} satisfies Model<"openai-responses">,
"gpt-5.2-chat-latest": {
id: "gpt-5.2-chat-latest",
name: "GPT-5.2 Chat",
api: "openai-responses",
provider: "openai",
baseUrl: "https://api.openai.com/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1.75,
output: 14,
cacheRead: 0.175,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 16384,
} satisfies Model<"openai-responses">,
"gpt-5.1": {
id: "gpt-5.1",
name: "GPT-5.1",
@ -1182,6 +1216,23 @@ export const MODELS = {
contextWindow: 400000,
maxTokens: 272000,
} satisfies Model<"openai-responses">,
"gpt-5.2": {
id: "gpt-5.2",
name: "GPT-5.2",
api: "openai-responses",
provider: "openai",
baseUrl: "https://api.openai.com/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1.75,
output: 14,
cacheRead: 0.175,
cacheWrite: 0,
},
contextWindow: 400000,
maxTokens: 128000,
} satisfies Model<"openai-responses">,
"gpt-5.1-chat-latest": {
id: "gpt-5.1-chat-latest",
name: "GPT-5.1 Chat",
@ -2093,6 +2144,23 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"mistral-small-2506": {
id: "mistral-small-2506",
name: "Mistral Small 3.2",
api: "openai-completions",
provider: "mistral",
baseUrl: "https://api.mistral.ai/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 0.1,
output: 0.3,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"ministral-3b-latest": {
id: "ministral-3b-latest",
name: "Ministral 3B",
@ -2401,6 +2469,57 @@ export const MODELS = {
} satisfies Model<"openai-completions">,
},
openrouter: {
"openai/gpt-5.2-chat": {
id: "openai/gpt-5.2-chat",
name: "OpenAI: GPT-5.2 Chat",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 1.75,
output: 14,
cacheRead: 0.175,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"openai/gpt-5.2-pro": {
id: "openai/gpt-5.2-pro",
name: "OpenAI: GPT-5.2 Pro",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 21,
output: 168,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 400000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"openai/gpt-5.2": {
id: "openai/gpt-5.2",
name: "OpenAI: GPT-5.2",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
input: ["text", "image"],
cost: {
input: 1.75,
output: 14,
cacheRead: 0.175,
cacheWrite: 0,
},
contextWindow: 400000,
maxTokens: 128000,
} satisfies Model<"openai-completions">,
"mistralai/devstral-2512:free": {
id: "mistralai/devstral-2512:free",
name: "Mistral: Devstral 2 2512 (free)",
@ -2475,7 +2594,7 @@ export const MODELS = {
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: true,
reasoning: false,
input: ["text"],
cost: {
input: 0,
@ -2648,9 +2767,9 @@ export const MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.26,
output: 0.39,
cacheRead: 0.19999999999999998,
input: 0.25,
output: 0.38,
cacheRead: 0.19,
cacheWrite: 0,
},
contextWindow: 163840,
@ -3192,13 +3311,13 @@ export const MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.43,
output: 1.75,
cacheRead: 0.0799999993,
input: 0.44,
output: 1.76,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 202752,
maxTokens: 4096,
contextWindow: 204800,
maxTokens: 131072,
} satisfies Model<"openai-completions">,
"anthropic/claude-sonnet-4.5": {
id: "anthropic/claude-sonnet-4.5",
@ -5512,23 +5631,6 @@ export const MODELS = {
contextWindow: 200000,
maxTokens: 8192,
} satisfies Model<"openai-completions">,
"mistralai/ministral-3b": {
id: "mistralai/ministral-3b",
name: "Mistral: Ministral 3B",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.04,
output: 0.04,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/ministral-8b": {
id: "mistralai/ministral-8b",
name: "Mistral: Ministral 8B",
@ -5546,6 +5648,23 @@ export const MODELS = {
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/ministral-3b": {
id: "mistralai/ministral-3b",
name: "Mistral: Ministral 3B",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.04,
output: 0.04,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"nvidia/llama-3.1-nemotron-70b-instruct": {
id: "nvidia/llama-3.1-nemotron-70b-instruct",
name: "NVIDIA: Llama 3.1 Nemotron 70B Instruct",
@ -5716,22 +5835,22 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-70b-instruct": {
id: "meta-llama/llama-3.1-70b-instruct",
name: "Meta: Llama 3.1 70B Instruct",
"meta-llama/llama-3.1-8b-instruct": {
id: "meta-llama/llama-3.1-8b-instruct",
name: "Meta: Llama 3.1 8B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.39999999999999997,
output: 0.39999999999999997,
input: 0.02,
output: 0.03,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 4096,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-405b-instruct": {
id: "meta-llama/llama-3.1-405b-instruct",
@ -5750,22 +5869,22 @@ export const MODELS = {
contextWindow: 130815,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3.1-8b-instruct": {
id: "meta-llama/llama-3.1-8b-instruct",
name: "Meta: Llama 3.1 8B Instruct",
"meta-llama/llama-3.1-70b-instruct": {
id: "meta-llama/llama-3.1-70b-instruct",
name: "Meta: Llama 3.1 70B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.02,
output: 0.03,
input: 0.39999999999999997,
output: 0.39999999999999997,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 131072,
maxTokens: 16384,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mistral-nemo": {
id: "mistralai/mistral-nemo",
@ -5903,6 +6022,23 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4o-2024-05-13": {
id: "openai/gpt-4o-2024-05-13",
name: "OpenAI: GPT-4o (2024-05-13)",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
cost: {
input: 5,
output: 15,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4o": {
id: "openai/gpt-4o",
name: "OpenAI: GPT-4o",
@ -5937,22 +6073,22 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 64000,
} satisfies Model<"openai-completions">,
"openai/gpt-4o-2024-05-13": {
id: "openai/gpt-4o-2024-05-13",
name: "OpenAI: GPT-4o (2024-05-13)",
"meta-llama/llama-3-70b-instruct": {
id: "meta-llama/llama-3-70b-instruct",
name: "Meta: Llama 3 70B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text", "image"],
input: ["text"],
cost: {
input: 5,
output: 15,
input: 0.3,
output: 0.39999999999999997,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-8b-instruct": {
id: "meta-llama/llama-3-8b-instruct",
@ -5971,23 +6107,6 @@ export const MODELS = {
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"meta-llama/llama-3-70b-instruct": {
id: "meta-llama/llama-3-70b-instruct",
name: "Meta: Llama 3 70B Instruct",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.3,
output: 0.39999999999999997,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 8192,
maxTokens: 16384,
} satisfies Model<"openai-completions">,
"mistralai/mixtral-8x22b-instruct": {
id: "mistralai/mixtral-8x22b-instruct",
name: "Mistral: Mixtral 8x22B Instruct",
@ -6073,23 +6192,6 @@ export const MODELS = {
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4-turbo-preview": {
id: "openai/gpt-4-turbo-preview",
name: "OpenAI: GPT-4 Turbo Preview",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 10,
output: 30,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-3.5-turbo-0613": {
id: "openai/gpt-3.5-turbo-0613",
name: "OpenAI: GPT-3.5 Turbo (older v0613)",
@ -6107,6 +6209,23 @@ export const MODELS = {
contextWindow: 4095,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4-turbo-preview": {
id: "openai/gpt-4-turbo-preview",
name: "OpenAI: GPT-4 Turbo Preview",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 10,
output: 30,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"mistralai/mistral-tiny": {
id: "mistralai/mistral-tiny",
name: "Mistral Tiny",
@ -6175,23 +6294,6 @@ export const MODELS = {
contextWindow: 16385,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-3.5-turbo": {
id: "openai/gpt-3.5-turbo",
name: "OpenAI: GPT-3.5 Turbo",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.5,
output: 1.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 16385,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-4-0314": {
id: "openai/gpt-4-0314",
name: "OpenAI: GPT-4 (older v0314)",
@ -6226,6 +6328,23 @@ export const MODELS = {
contextWindow: 8191,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openai/gpt-3.5-turbo": {
id: "openai/gpt-3.5-turbo",
name: "OpenAI: GPT-3.5 Turbo",
api: "openai-completions",
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
reasoning: false,
input: ["text"],
cost: {
input: 0.5,
output: 1.5,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 16385,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"openrouter/auto": {
id: "openrouter/auto",
name: "OpenRouter: Auto Router",

View file

@ -576,9 +576,9 @@ function createRunner(sandboxConfig: SandboxConfig, channelId: string, channelDi
// Format: "[username]: message" so LLM knows who's talking
let userMessage = `[${ctx.message.userName || "unknown"}]: ${ctx.message.text}`;
// Add attachment paths if any
// Add attachment paths if any (convert to absolute paths in execution environment)
if (ctx.message.attachments && ctx.message.attachments.length > 0) {
const attachmentPaths = ctx.message.attachments.map((a) => a.local).join("\n");
const attachmentPaths = ctx.message.attachments.map((a) => `${workspacePath}/${a.local}`).join("\n");
userMessage += `\n\nAttachments:\n${attachmentPaths}`;
}

View file

@ -100,7 +100,7 @@ function createSlackContext(event: SlackEvent, slack: SlackBot, state: ChannelSt
userName: user?.userName,
channel: event.channel,
ts: event.ts,
attachments: [],
attachments: (event.attachments || []).map((a) => ({ local: a.local })),
},
channelName: slack.getChannel(event.channel)?.name,
store: state.store,
@ -243,10 +243,14 @@ const handler: MomHandler = {
log.logStartup(workingDir, sandbox.type === "host" ? "host" : `docker:${sandbox.container}`);
// Shared store for attachment downloads (also used per-channel in getState)
const sharedStore = new ChannelStore({ workingDir, botToken: MOM_SLACK_BOT_TOKEN! });
const bot = new SlackBotClass(handler, {
appToken: MOM_SLACK_APP_TOKEN,
botToken: MOM_SLACK_BOT_TOKEN,
workingDir,
store: sharedStore,
});
bot.start();

View file

@ -3,6 +3,7 @@ import { WebClient } from "@slack/web-api";
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
import { basename, join } from "path";
import * as log from "./log.js";
import type { Attachment, ChannelStore } from "./store.js";
// ============================================================================
// Types
@ -14,7 +15,9 @@ export interface SlackEvent {
ts: string;
user: string;
text: string;
files?: Array<{ name: string; url_private_download?: string; url_private?: string }>;
files?: Array<{ name?: string; url_private_download?: string; url_private?: string }>;
/** Processed attachments with local paths (populated after logUserMessage) */
attachments?: Attachment[];
}
export interface SlackUser {
@ -118,6 +121,7 @@ export class SlackBot {
private webClient: WebClient;
private handler: MomHandler;
private workingDir: string;
private store: ChannelStore;
private botUserId: string | null = null;
private startupTs: string | null = null; // Messages older than this are just logged, not processed
@ -125,9 +129,13 @@ export class SlackBot {
private channels = new Map<string, SlackChannel>();
private queues = new Map<string, ChannelQueue>();
constructor(handler: MomHandler, config: { appToken: string; botToken: string; workingDir: string }) {
constructor(
handler: MomHandler,
config: { appToken: string; botToken: string; workingDir: string; store: ChannelStore },
) {
this.handler = handler;
this.workingDir = config.workingDir;
this.store = config.store;
this.socketClient = new SocketModeClient({ appToken: config.appToken });
this.webClient = new WebClient(config.botToken);
}
@ -258,7 +266,8 @@ export class SlackBot {
};
// SYNC: Log to log.jsonl (ALWAYS, even for old messages)
this.logUserMessage(slackEvent);
// Also downloads attachments in background and stores local paths
slackEvent.attachments = this.logUserMessage(slackEvent);
// Only trigger processing for messages AFTER startup (not replayed old messages)
if (this.startupTs && e.ts < this.startupTs) {
@ -336,7 +345,8 @@ export class SlackBot {
};
// SYNC: Log to log.jsonl (ALL messages - channel chatter and DMs)
this.logUserMessage(slackEvent);
// Also downloads attachments in background and stores local paths
slackEvent.attachments = this.logUserMessage(slackEvent);
// Only trigger processing for messages AFTER startup (not replayed old messages)
if (this.startupTs && e.ts < this.startupTs) {
@ -371,9 +381,12 @@ export class SlackBot {
/**
* Log a user message to log.jsonl (SYNC)
* Downloads attachments in background via store
*/
private logUserMessage(event: SlackEvent): void {
private logUserMessage(event: SlackEvent): Attachment[] {
const user = this.users.get(event.user);
// Process attachments - queues downloads in background
const attachments = event.files ? this.store.processAttachments(event.channel, event.files, event.ts) : [];
this.logToFile(event.channel, {
date: new Date(parseFloat(event.ts) * 1000).toISOString(),
ts: event.ts,
@ -381,9 +394,10 @@ export class SlackBot {
userName: user?.userName,
displayName: user?.displayName,
text: event.text,
attachments: event.files?.map((f) => f.name) || [],
attachments,
isBot: false,
});
return attachments;
}
// ==========================================================================
@ -464,6 +478,8 @@ export class SlackBot {
const user = this.users.get(msg.user!);
// Strip @mentions from text (same as live messages)
const text = (msg.text || "").replace(/<@[A-Z0-9]+>/gi, "").trim();
// Process attachments - queues downloads in background
const attachments = msg.files ? this.store.processAttachments(channelId, msg.files, msg.ts!) : [];
this.logToFile(channelId, {
date: new Date(parseFloat(msg.ts!) * 1000).toISOString(),
@ -472,7 +488,7 @@ export class SlackBot {
userName: isMomMessage ? undefined : user?.userName,
displayName: isMomMessage ? undefined : user?.displayName,
text,
attachments: msg.files?.map((f) => f.name) || [],
attachments,
isBot: isMomMessage,
});
}