mom: add events system for scheduled wake-ups

- Three event types: immediate, one-shot, periodic (cron)
- Events are JSON files in workspace/events/
- EventsWatcher with fs.watch, 100ms debounce
- Queue integration via SlackBot.enqueueEvent() (max 5)
- Fix setTyping race condition causing duplicate messages
- System prompt documents events for mom
- Design doc in docs/events.md
- Add croner dependency for cron scheduling
This commit is contained in:
Mario Zechner 2025-12-12 22:45:34 +01:00
parent 03c404c15f
commit d6809328da
9 changed files with 847 additions and 7 deletions

View file

@ -4,6 +4,7 @@ import { join, resolve } from "path";
import { type AgentRunner, getOrCreateRunner } from "./agent.js";
import { syncLogToContext } from "./context.js";
import { downloadChannel } from "./download.js";
import { createEventsWatcher } from "./events.js";
import * as log from "./log.js";
import { parseSandboxArg, type SandboxConfig, validateSandbox } from "./sandbox.js";
import { type MomHandler, type SlackBot, SlackBot as SlackBotClass, type SlackEvent } from "./slack.js";
@ -113,7 +114,7 @@ function getState(channelId: string): ChannelState {
// Create SlackContext adapter
// ============================================================================
function createSlackContext(event: SlackEvent, slack: SlackBot, state: ChannelState) {
function createSlackContext(event: SlackEvent, slack: SlackBot, state: ChannelState, isEvent?: boolean) {
let messageTs: string | null = null;
let accumulatedText = "";
let isWorking = true;
@ -122,6 +123,9 @@ function createSlackContext(event: SlackEvent, slack: SlackBot, state: ChannelSt
const user = slack.getUser(event.user);
// Extract event filename for status message
const eventFilename = isEvent ? event.text.match(/^\[EVENT:([^:]+):/)?.[1] : undefined;
return {
message: {
text: event.text,
@ -179,8 +183,13 @@ function createSlackContext(event: SlackEvent, slack: SlackBot, state: ChannelSt
setTyping: async (isTyping: boolean) => {
if (isTyping && !messageTs) {
accumulatedText = "_Thinking_";
messageTs = await slack.postMessage(event.channel, accumulatedText + workingIndicator);
updatePromise = updatePromise.then(async () => {
if (!messageTs) {
accumulatedText = eventFilename ? `_Starting event: ${eventFilename}_` : "_Thinking_";
messageTs = await slack.postMessage(event.channel, accumulatedText + workingIndicator);
}
});
await updatePromise;
}
},
@ -223,7 +232,7 @@ const handler: MomHandler = {
}
},
async handleEvent(event: SlackEvent, slack: SlackBot): Promise<void> {
async handleEvent(event: SlackEvent, slack: SlackBot, isEvent?: boolean): Promise<void> {
const state = getState(event.channel);
const channelDir = join(workingDir, event.channel);
@ -243,7 +252,7 @@ const handler: MomHandler = {
}
// Create context adapter
const ctx = createSlackContext(event, slack, state);
const ctx = createSlackContext(event, slack, state, isEvent);
// Run the agent
await ctx.setTyping(true);
@ -283,4 +292,21 @@ const bot = new SlackBotClass(handler, {
store: sharedStore,
});
// Start events watcher
const eventsWatcher = createEventsWatcher(workingDir, bot);
eventsWatcher.start();
// Handle shutdown
process.on("SIGINT", () => {
log.logInfo("Shutting down...");
eventsWatcher.stop();
process.exit(0);
});
process.on("SIGTERM", () => {
log.logInfo("Shutting down...");
eventsWatcher.stop();
process.exit(0);
});
bot.start();