co-mono/packages/mom/docs/events.md
Mario Zechner 8ba6aa6627 mom: add [SILENT] response marker for periodic events
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.
2025-12-12 22:50:12 +01:00

307 lines
9.8 KiB
Markdown

# 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.).
```json
{
"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.
```json
{
"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.
```json
{
"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:00
- `0 9 * * 1-5` — weekdays at 9:00
- `30 14 * * 1` — Mondays at 14:30
- `0 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 the `timezone` field 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
1. Scan `workspace/events/` for all `.json` files
2. Parse each event file
3. 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 `at` is in the past, delete the file. If `at` is in the future, set a `setTimeout` to execute at the specified time.
- **Periodic**: Set up a cron job (using `croner` library) 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:
1. Retry with exponential backoff (100ms, 200ms, 400ms)
2. If still failing after retries, delete the file and log error to console
### Execution Errors
If the agent errors while processing an event:
1. Post error message to the channel
2. Delete the event file (for immediate/one-shot)
3. 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:
1. Create a synthetic `SlackEvent` with formatted message
2. Call `slack.enqueueEvent(event)`
3. Event waits in queue if agent is busy, processed when idle
## Event Execution
When an event is dequeued and executes:
1. Post status message: "_Starting event: {filename}_"
2. 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`
3. 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
## 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 watching
- `src/slack.ts` Add `enqueueEvent()` method and `size()` to `ChannelQueue`
- `src/main.ts` Initialize events watcher on startup
- `src/agent.ts` Update system prompt with events documentation
### Key Components
```typescript
// 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:
```markdown
## 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.
```json
{"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.
```json
{"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:00
- `0 9 * * 1-5` = weekdays at 9:00
- `30 14 * * 1` = Mondays at 14:30
- `0 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
```bash
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:
```bash
# Creates event per email — will flood the queue
on_email() { echo '{"type":"immediate"...}' > /workspace/events/email-$ID.json; }
```
Good:
```bash
# 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.
```