Skip to content

Latest commit

 

History

History
307 lines (219 loc) · 9.82 KB

File metadata and controls

307 lines (219 loc) · 9.82 KB

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: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

// 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: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

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.