When mom responds with just [SILENT], the status message is deleted and nothing is posted to Slack. Useful for periodic events that check for activity but find nothing to report.
9.8 KiB
Events System
The events system allows mom to be triggered by scheduled or immediate events. Events are JSON files in the workspace/events/ directory. The harness watches this directory and executes events when they become due.
Event Types
Immediate
Executes as soon as the harness discovers the file. Used by programs mom writes to signal external events (webhooks, file changes, API callbacks, etc.).
{
"type": "immediate",
"channelId": "C123ABC",
"text": "New support ticket received: #12345"
}
After execution, the file is deleted. Staleness is determined by file mtime (see Startup Behavior).
One-Shot
Executes once at a specific date/time. Used for reminders, scheduled tasks, or deferred actions.
{
"type": "one-shot",
"channelId": "C123ABC",
"text": "Remind Mario about the dentist appointment",
"at": "2025-12-15T09:00:00+01:00"
}
The at timestamp must include a timezone offset. After execution, the file is deleted.
Periodic
Executes repeatedly on a cron schedule. Used for recurring tasks like daily summaries, weekly reports, or regular checks.
{
"type": "periodic",
"channelId": "C123ABC",
"text": "Check inbox and post summary",
"schedule": "0 9 * * 1-5",
"timezone": "Europe/Vienna"
}
The schedule field uses standard cron syntax. The timezone field uses IANA timezone names. The file persists until explicitly deleted by mom or the program that created it.
Cron Format
minute hour day-of-month month day-of-week
Examples:
0 9 * * *— daily at 9:000 9 * * 1-5— weekdays at 9:0030 14 * * 1— Mondays at 14:300 0 1 * *— first of each month at midnight*/15 * * * *— every 15 minutes
Timezone Handling
All timestamps must include timezone information:
- For
one-shot: Use ISO 8601 format with offset (e.g.,2025-12-15T09:00:00+01:00) - For
periodic: Use thetimezonefield with an IANA timezone name (e.g.,Europe/Vienna,America/New_York)
The harness runs in the host process timezone. When users mention times without specifying timezone, assume the harness timezone.
Harness Behavior
Startup
- Scan
workspace/events/for all.jsonfiles - Parse each event file
- For each event:
- Immediate: Check file mtime. If the file was created while the harness was NOT running (mtime < harness start time), it's stale. Delete without executing. Otherwise, execute immediately and delete.
- One-shot: If
atis in the past, delete the file. Ifatis in the future, set asetTimeoutto execute at the specified time. - Periodic: Set up a cron job (using
cronerlibrary) to execute on the specified schedule. If a scheduled time was missed while harness was down, do NOT catch up. Wait for the next scheduled occurrence.
File System Watching
The harness watches workspace/events/ using fs.watch() with 100ms debounce.
New file added:
- Parse the event
- Based on type: execute immediately, set
setTimeout, or set up cron job
Existing file modified:
- Cancel any existing timer/cron for this file
- Re-parse and set up again (allows rescheduling)
File deleted:
- Cancel any existing timer/cron for this file
Parse Errors
If a JSON file fails to parse:
- Retry with exponential backoff (100ms, 200ms, 400ms)
- If still failing after retries, delete the file and log error to console
Execution Errors
If the agent errors while processing an event:
- Post error message to the channel
- Delete the event file (for immediate/one-shot)
- No retries
Queue Integration
Events integrate with the existing ChannelQueue in SlackBot:
- New method:
SlackBot.enqueueEvent(event: SlackEvent)— always queues, no "already working" rejection - Maximum 5 events can be queued per channel. If queue is full, discard and log to console.
- User @mom mentions retain current behavior: rejected with "Already working" message if agent is busy
When an event triggers:
- Create a synthetic
SlackEventwith formatted message - Call
slack.enqueueEvent(event) - Event waits in queue if agent is busy, processed when idle
Event Execution
When an event is dequeued and executes:
- Post status message: "Starting event: {filename}"
- Invoke the agent with message:
[EVENT:{filename}:{type}:{schedule}] {text}- For immediate:
[EVENT:webhook-123.json:immediate] New support ticket - For one-shot:
[EVENT:dentist.json:one-shot:2025-12-15T09:00:00+01:00] Remind Mario - For periodic:
[EVENT:daily-inbox.json:periodic:0 9 * * 1-5] Check inbox
- For immediate:
- After execution:
- If response is
[SILENT]: delete status message, post nothing to Slack - Immediate and one-shot: delete the event file
- Periodic: keep the file, event will trigger again on schedule
- If response is
Silent Completion
For periodic events that check for activity (inbox, notifications, etc.), mom may find nothing to report. To avoid spamming the channel, mom can respond with just [SILENT]. This deletes the "Starting event..." status message and posts nothing to Slack.
Example: A periodic event checks for new emails every 15 minutes. If there are no new emails, mom responds [SILENT]. If there are new emails, mom posts a summary.
File Naming
Event files should have descriptive names ending in .json:
webhook-12345.json(immediate)dentist-reminder-2025-12-15.json(one-shot)daily-inbox-summary.json(periodic)
The filename is used as an identifier for tracking timers and in the event message. Avoid special characters.
Implementation
Files
src/events.ts— Event parsing, timer management, fs watchingsrc/slack.ts— AddenqueueEvent()method andsize()toChannelQueuesrc/main.ts— Initialize events watcher on startupsrc/agent.ts— Update system prompt with events documentation
Key Components
// events.ts
interface ImmediateEvent {
type: "immediate";
channelId: string;
text: string;
}
interface OneShotEvent {
type: "one-shot";
channelId: string;
text: string;
at: string; // ISO 8601 with timezone offset
}
interface PeriodicEvent {
type: "periodic";
channelId: string;
text: string;
schedule: string; // cron syntax
timezone: string; // IANA timezone
}
type MomEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;
class EventsWatcher {
private timers: Map<string, NodeJS.Timeout> = new Map();
private crons: Map<string, Cron> = new Map();
private startTime: number;
constructor(
private eventsDir: string,
private slack: SlackBot,
private onError: (filename: string, error: Error) => void
) {
this.startTime = Date.now();
}
start(): void { /* scan existing, setup fs.watch */ }
stop(): void { /* cancel all timers/crons, stop watching */ }
private handleFile(filename: string): void { /* parse, schedule */ }
private handleDelete(filename: string): void { /* cancel timer/cron */ }
private execute(filename: string, event: MomEvent): void { /* enqueue */ }
}
Dependencies
croner— Cron scheduling with timezone support
System Prompt Section
The following should be added to mom's system prompt:
## Events
You can schedule events that wake you up at specific times or when external things happen. Events are JSON files in `/workspace/events/`.
### Event Types
**Immediate** — Triggers as soon as harness sees the file. Use in scripts/webhooks to signal external events.
```json
{"type": "immediate", "channelId": "C123", "text": "New GitHub issue opened"}
One-shot — Triggers once at a specific time. Use for reminders.
{"type": "one-shot", "channelId": "C123", "text": "Remind Mario about dentist", "at": "2025-12-15T09:00:00+01:00"}
Periodic — Triggers on a cron schedule. Use for recurring tasks.
{"type": "periodic", "channelId": "C123", "text": "Check inbox and summarize", "schedule": "0 9 * * 1-5", "timezone": "Europe/Vienna"}
Cron Format
minute hour day-of-month month day-of-week
0 9 * * *= daily at 9:000 9 * * 1-5= weekdays at 9:0030 14 * * 1= Mondays at 14:300 0 1 * *= first of each month at midnight
Timezones
All at timestamps must include offset (e.g., +01:00). Periodic events use IANA timezone names. The harness runs in ${TIMEZONE}. When users mention times without timezone, assume ${TIMEZONE}.
Creating Events
cat > /workspace/events/dentist-reminder.json << 'EOF'
{"type": "one-shot", "channelId": "${CHANNEL}", "text": "Dentist tomorrow", "at": "2025-12-14T09:00:00+01:00"}
EOF
Managing Events
- List:
ls /workspace/events/ - View:
cat /workspace/events/foo.json - Delete/cancel:
rm /workspace/events/foo.json
When Events Trigger
You receive a message like:
[EVENT:dentist-reminder.json:one-shot:2025-12-14T09:00:00+01:00] Dentist tomorrow
Immediate and one-shot events auto-delete after triggering. Periodic events persist until you delete them.
Debouncing
When writing programs that create immediate events (email watchers, webhook handlers, etc.), always debounce. If 50 emails arrive in a minute, don't create 50 immediate events. Instead:
- Collect events over a window (e.g., 30 seconds)
- Create ONE immediate event summarizing what happened
- Or just signal "new activity, check inbox" rather than per-item events
Bad:
# Creates event per email — will flood the queue
on_email() { echo '{"type":"immediate"...}' > /workspace/events/email-$ID.json; }
Good:
# Debounce: flag file + single delayed event
on_email() {
echo "$SUBJECT" >> /tmp/pending-emails.txt
if [ ! -f /workspace/events/email-batch.json ]; then
(sleep 30 && mv /tmp/pending-emails.txt /workspace/events/email-batch.json) &
fi
}
Or simpler: use a periodic event to check for new emails every 15 minutes instead of immediate events.
Limits
Maximum 5 events can be queued. Don't create excessive immediate or periodic events.