fix(mom): private channel messages not being logged

- Add message.groups to required bot events in README
- Add groups:history and groups:read to required scopes in README
- app_mention handler now logs messages directly instead of relying on message event
- Add deduplication in ChannelStore.logMessage() to prevent double-logging
- Remove redundant current message append in agent.ts (already in log)
This commit is contained in:
Mario Zechner 2025-12-04 12:24:29 +01:00
parent db6d655ee9
commit 706554a5d3
6 changed files with 67 additions and 11 deletions

View file

@ -2,6 +2,14 @@
## [Unreleased] ## [Unreleased]
### Fixed
- Private channel messages not being logged
- Added `message.groups` to required bot events in README
- Added `groups:history` and `groups:read` to required scopes in README
- `app_mention` handler now logs messages directly instead of relying on `message` event
- Added deduplication in `ChannelStore.logMessage()` to prevent double-logging
### Added ### Added
- Message backfill on startup (#103) - Message backfill on startup (#103)

View file

@ -31,6 +31,8 @@ npm install @mariozechner/pi-mom
- `chat:write` - `chat:write`
- `files:read` - `files:read`
- `files:write` - `files:write`
- `groups:history`
- `groups:read`
- `im:history` - `im:history`
- `im:read` - `im:read`
- `im:write` - `im:write`
@ -38,6 +40,7 @@ npm install @mariozechner/pi-mom
5. **Subscribe to Bot Events** (Event Subscriptions): 5. **Subscribe to Bot Events** (Event Subscriptions):
- `app_mention` - `app_mention`
- `message.channels` - `message.channels`
- `message.groups`
- `message.im` - `message.im`
6. Install the app to your workspace. Get the **Bot User OAuth Token**. This is `MOM_SLACK_BOT_TOKEN` 6. Install the app to your workspace. Get the **Bot User OAuth Token**. This is `MOM_SLACK_BOT_TOKEN`
7. Add mom to any channels where you want her to operate (she'll only see messages in channels she's added to) 7. Add mom to any channels where you want her to operate (she'll only see messages in channels she's added to)

30
packages/mom/dev.sh Executable file
View file

@ -0,0 +1,30 @@
#!/bin/bash
set -e
CONTAINER_NAME="mom-sandbox"
DATA_DIR="$(pwd)/data"
# Create data directory if it doesn't exist
mkdir -p "$DATA_DIR"
# Check if container exists
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
# Check if it's running
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo "Starting existing container: $CONTAINER_NAME"
docker start "$CONTAINER_NAME"
else
echo "Container $CONTAINER_NAME already running"
fi
else
echo "Creating container: $CONTAINER_NAME"
docker run -d \
--name "$CONTAINER_NAME" \
-v "$DATA_DIR:/workspace" \
alpine:latest \
tail -f /dev/null
fi
# Run mom with tsx watch mode
echo "Starting mom in dev mode..."
npx tsx --watch-path src --watch src/main.ts --sandbox=docker:$CONTAINER_NAME ./data

View file

@ -364,15 +364,7 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
const channelId = ctx.message.channel; const channelId = ctx.message.channel;
const workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, "")); const workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, ""));
const recentMessagesFromLog = getRecentMessages(channelDir, 50); const recentMessages = getRecentMessages(channelDir, 50);
// Append the current message (may not be in log yet due to race condition
// between app_mention and message events)
const currentMsgDate = new Date(parseFloat(ctx.message.ts) * 1000).toISOString().substring(0, 19);
const currentMsgUser = ctx.message.userName || ctx.message.user;
const currentMsgAttachments = ctx.message.attachments.map((a) => a.local).join(",");
const currentMsgLine = `${currentMsgDate}\t${currentMsgUser}\t${ctx.message.rawText}\t${currentMsgAttachments}`;
const recentMessages = recentMessagesFromLog + "\n" + currentMsgLine;
const memory = getMemory(channelDir); const memory = getMemory(channelDir);
const systemPrompt = buildSystemPrompt( const systemPrompt = buildSystemPrompt(

View file

@ -208,7 +208,6 @@ export class MomBot {
private setupEventHandlers(): void { private setupEventHandlers(): void {
// Handle @mentions in channels // Handle @mentions in channels
// Note: We don't log here - the message event handler logs all messages
this.socketClient.on("app_mention", async ({ event, ack }) => { this.socketClient.on("app_mention", async ({ event, ack }) => {
await ack(); await ack();
@ -220,6 +219,15 @@ export class MomBot {
files?: Array<{ name: string; url_private_download?: string; url_private?: string }>; files?: Array<{ name: string; url_private_download?: string; url_private?: string }>;
}; };
// Log the mention message (message event may not fire for all channel types)
await this.logMessage({
text: slackEvent.text,
channel: slackEvent.channel,
user: slackEvent.user,
ts: slackEvent.ts,
files: slackEvent.files,
});
const ctx = await this.createContext(slackEvent); const ctx = await this.createContext(slackEvent);
await this.handler.onChannelMention(ctx); await this.handler.onChannelMention(ctx);
}); });

View file

@ -35,6 +35,9 @@ export class ChannelStore {
private botToken: string; private botToken: string;
private pendingDownloads: PendingDownload[] = []; private pendingDownloads: PendingDownload[] = [];
private isDownloading = false; private isDownloading = false;
// Track recently logged message timestamps to prevent duplicates
// Key: "channelId:ts", automatically cleaned up after 60 seconds
private recentlyLogged = new Map<string, number>();
constructor(config: ChannelStoreConfig) { constructor(config: ChannelStoreConfig) {
this.workingDir = config.workingDir; this.workingDir = config.workingDir;
@ -107,8 +110,19 @@ export class ChannelStore {
/** /**
* Log a message to the channel's log.jsonl * Log a message to the channel's log.jsonl
* Returns false if message was already logged (duplicate)
*/ */
async logMessage(channelId: string, message: LoggedMessage): Promise<void> { async logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {
// Check for duplicate (same channel + timestamp)
const dedupeKey = `${channelId}:${message.ts}`;
if (this.recentlyLogged.has(dedupeKey)) {
return false; // Already logged
}
// Mark as logged and schedule cleanup after 60 seconds
this.recentlyLogged.set(dedupeKey, Date.now());
setTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);
const logPath = join(this.getChannelDir(channelId), "log.jsonl"); const logPath = join(this.getChannelDir(channelId), "log.jsonl");
// Ensure message has a date field // Ensure message has a date field
@ -127,6 +141,7 @@ export class ChannelStore {
const line = JSON.stringify(message) + "\n"; const line = JSON.stringify(message) + "\n";
await appendFile(logPath, line, "utf-8"); await appendFile(logPath, line, "utf-8");
return true;
} }
/** /**