@oh-my-pi/pi-mom 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/events.md ADDED
@@ -0,0 +1,307 @@
1
+ # Events System
2
+
3
+ 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.
4
+
5
+ ## Event Types
6
+
7
+ ### Immediate
8
+
9
+ Executes as soon as the harness discovers the file. Used by programs mom writes to signal external events (webhooks, file changes, API callbacks, etc.).
10
+
11
+ ```json
12
+ {
13
+ "type": "immediate",
14
+ "channelId": "C123ABC",
15
+ "text": "New support ticket received: #12345"
16
+ }
17
+ ```
18
+
19
+ After execution, the file is deleted. Staleness is determined by file mtime (see Startup Behavior).
20
+
21
+ ### One-Shot
22
+
23
+ Executes once at a specific date/time. Used for reminders, scheduled tasks, or deferred actions.
24
+
25
+ ```json
26
+ {
27
+ "type": "one-shot",
28
+ "channelId": "C123ABC",
29
+ "text": "Remind Mario about the dentist appointment",
30
+ "at": "2025-12-15T09:00:00+01:00"
31
+ }
32
+ ```
33
+
34
+ The `at` timestamp must include a timezone offset. After execution, the file is deleted.
35
+
36
+ ### Periodic
37
+
38
+ Executes repeatedly on a cron schedule. Used for recurring tasks like daily summaries, weekly reports, or regular checks.
39
+
40
+ ```json
41
+ {
42
+ "type": "periodic",
43
+ "channelId": "C123ABC",
44
+ "text": "Check inbox and post summary",
45
+ "schedule": "0 9 * * 1-5",
46
+ "timezone": "Europe/Vienna"
47
+ }
48
+ ```
49
+
50
+ 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.
51
+
52
+ #### Cron Format
53
+
54
+ `minute hour day-of-month month day-of-week`
55
+
56
+ Examples:
57
+ - `0 9 * * *` — daily at 9:00
58
+ - `0 9 * * 1-5` — weekdays at 9:00
59
+ - `30 14 * * 1` — Mondays at 14:30
60
+ - `0 0 1 * *` — first of each month at midnight
61
+ - `*/15 * * * *` — every 15 minutes
62
+
63
+ ## Timezone Handling
64
+
65
+ All timestamps must include timezone information:
66
+ - For `one-shot`: Use ISO 8601 format with offset (e.g., `2025-12-15T09:00:00+01:00`)
67
+ - For `periodic`: Use the `timezone` field with an IANA timezone name (e.g., `Europe/Vienna`, `America/New_York`)
68
+
69
+ The harness runs in the host process timezone. When users mention times without specifying timezone, assume the harness timezone.
70
+
71
+ ## Harness Behavior
72
+
73
+ ### Startup
74
+
75
+ 1. Scan `workspace/events/` for all `.json` files
76
+ 2. Parse each event file
77
+ 3. For each event:
78
+ - **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.
79
+ - **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.
80
+ - **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.
81
+
82
+ ### File System Watching
83
+
84
+ The harness watches `workspace/events/` using `fs.watch()` with 100ms debounce.
85
+
86
+ **New file added:**
87
+ - Parse the event
88
+ - Based on type: execute immediately, set `setTimeout`, or set up cron job
89
+
90
+ **Existing file modified:**
91
+ - Cancel any existing timer/cron for this file
92
+ - Re-parse and set up again (allows rescheduling)
93
+
94
+ **File deleted:**
95
+ - Cancel any existing timer/cron for this file
96
+
97
+ ### Parse Errors
98
+
99
+ If a JSON file fails to parse:
100
+ 1. Retry with exponential backoff (100ms, 200ms, 400ms)
101
+ 2. If still failing after retries, delete the file and log error to console
102
+
103
+ ### Execution Errors
104
+
105
+ If the agent errors while processing an event:
106
+ 1. Post error message to the channel
107
+ 2. Delete the event file (for immediate/one-shot)
108
+ 3. No retries
109
+
110
+ ## Queue Integration
111
+
112
+ Events integrate with the existing `ChannelQueue` in `SlackBot`:
113
+
114
+ - New method: `SlackBot.enqueueEvent(event: SlackEvent)` — always queues, no "already working" rejection
115
+ - Maximum 5 events can be queued per channel. If queue is full, discard and log to console.
116
+ - User @mom mentions retain current behavior: rejected with "Already working" message if agent is busy
117
+
118
+ When an event triggers:
119
+ 1. Create a synthetic `SlackEvent` with formatted message
120
+ 2. Call `slack.enqueueEvent(event)`
121
+ 3. Event waits in queue if agent is busy, processed when idle
122
+
123
+ ## Event Execution
124
+
125
+ When an event is dequeued and executes:
126
+
127
+ 1. Post status message: "_Starting event: {filename}_"
128
+ 2. Invoke the agent with message: `[EVENT:{filename}:{type}:{schedule}] {text}`
129
+ - For immediate: `[EVENT:webhook-123.json:immediate] New support ticket`
130
+ - For one-shot: `[EVENT:dentist.json:one-shot:2025-12-15T09:00:00+01:00] Remind Mario`
131
+ - For periodic: `[EVENT:daily-inbox.json:periodic:0 9 * * 1-5] Check inbox`
132
+ 3. After execution:
133
+ - If response is `[SILENT]`: delete status message, post nothing to Slack
134
+ - Immediate and one-shot: delete the event file
135
+ - Periodic: keep the file, event will trigger again on schedule
136
+
137
+ ## Silent Completion
138
+
139
+ 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.
140
+
141
+ 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.
142
+
143
+ ## File Naming
144
+
145
+ Event files should have descriptive names ending in `.json`:
146
+ - `webhook-12345.json` (immediate)
147
+ - `dentist-reminder-2025-12-15.json` (one-shot)
148
+ - `daily-inbox-summary.json` (periodic)
149
+
150
+ The filename is used as an identifier for tracking timers and in the event message. Avoid special characters.
151
+
152
+ ## Implementation
153
+
154
+ ### Files
155
+
156
+ - `src/events.ts` — Event parsing, timer management, fs watching
157
+ - `src/slack.ts` — Add `enqueueEvent()` method and `size()` to `ChannelQueue`
158
+ - `src/main.ts` — Initialize events watcher on startup
159
+ - `src/agent.ts` — Update system prompt with events documentation
160
+
161
+ ### Key Components
162
+
163
+ ```typescript
164
+ // events.ts
165
+
166
+ interface ImmediateEvent {
167
+ type: "immediate";
168
+ channelId: string;
169
+ text: string;
170
+ }
171
+
172
+ interface OneShotEvent {
173
+ type: "one-shot";
174
+ channelId: string;
175
+ text: string;
176
+ at: string; // ISO 8601 with timezone offset
177
+ }
178
+
179
+ interface PeriodicEvent {
180
+ type: "periodic";
181
+ channelId: string;
182
+ text: string;
183
+ schedule: string; // cron syntax
184
+ timezone: string; // IANA timezone
185
+ }
186
+
187
+ type MomEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;
188
+
189
+ class EventsWatcher {
190
+ private timers: Map<string, NodeJS.Timeout> = new Map();
191
+ private crons: Map<string, Cron> = new Map();
192
+ private startTime: number;
193
+
194
+ constructor(
195
+ private eventsDir: string,
196
+ private slack: SlackBot,
197
+ private onError: (filename: string, error: Error) => void
198
+ ) {
199
+ this.startTime = Date.now();
200
+ }
201
+
202
+ start(): void { /* scan existing, setup fs.watch */ }
203
+ stop(): void { /* cancel all timers/crons, stop watching */ }
204
+
205
+ private handleFile(filename: string): void { /* parse, schedule */ }
206
+ private handleDelete(filename: string): void { /* cancel timer/cron */ }
207
+ private execute(filename: string, event: MomEvent): void { /* enqueue */ }
208
+ }
209
+ ```
210
+
211
+ ### Dependencies
212
+
213
+ - `croner` — Cron scheduling with timezone support
214
+
215
+ ## System Prompt Section
216
+
217
+ The following should be added to mom's system prompt:
218
+
219
+ ```markdown
220
+ ## Events
221
+
222
+ You can schedule events that wake you up at specific times or when external things happen. Events are JSON files in `/workspace/events/`.
223
+
224
+ ### Event Types
225
+
226
+ **Immediate** — Triggers as soon as harness sees the file. Use in scripts/webhooks to signal external events.
227
+ ```json
228
+ {"type": "immediate", "channelId": "C123", "text": "New GitHub issue opened"}
229
+ ```
230
+
231
+ **One-shot** — Triggers once at a specific time. Use for reminders.
232
+ ```json
233
+ {"type": "one-shot", "channelId": "C123", "text": "Remind Mario about dentist", "at": "2025-12-15T09:00:00+01:00"}
234
+ ```
235
+
236
+ **Periodic** — Triggers on a cron schedule. Use for recurring tasks.
237
+ ```json
238
+ {"type": "periodic", "channelId": "C123", "text": "Check inbox and summarize", "schedule": "0 9 * * 1-5", "timezone": "Europe/Vienna"}
239
+ ```
240
+
241
+ ### Cron Format
242
+
243
+ `minute hour day-of-month month day-of-week`
244
+
245
+ - `0 9 * * *` = daily at 9:00
246
+ - `0 9 * * 1-5` = weekdays at 9:00
247
+ - `30 14 * * 1` = Mondays at 14:30
248
+ - `0 0 1 * *` = first of each month at midnight
249
+
250
+ ### Timezones
251
+
252
+ 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}.
253
+
254
+ ### Creating Events
255
+
256
+ ```bash
257
+ cat > /workspace/events/dentist-reminder.json << 'EOF'
258
+ {"type": "one-shot", "channelId": "${CHANNEL}", "text": "Dentist tomorrow", "at": "2025-12-14T09:00:00+01:00"}
259
+ EOF
260
+ ```
261
+
262
+ ### Managing Events
263
+
264
+ - List: `ls /workspace/events/`
265
+ - View: `cat /workspace/events/foo.json`
266
+ - Delete/cancel: `rm /workspace/events/foo.json`
267
+
268
+ ### When Events Trigger
269
+
270
+ You receive a message like:
271
+ ```
272
+ [EVENT:dentist-reminder.json:one-shot:2025-12-14T09:00:00+01:00] Dentist tomorrow
273
+ ```
274
+
275
+ Immediate and one-shot events auto-delete after triggering. Periodic events persist until you delete them.
276
+
277
+ ### Debouncing
278
+
279
+ 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:
280
+
281
+ - Collect events over a window (e.g., 30 seconds)
282
+ - Create ONE immediate event summarizing what happened
283
+ - Or just signal "new activity, check inbox" rather than per-item events
284
+
285
+ Bad:
286
+ ```bash
287
+ # Creates event per email — will flood the queue
288
+ on_email() { echo '{"type":"immediate"...}' > /workspace/events/email-$ID.json; }
289
+ ```
290
+
291
+ Good:
292
+ ```bash
293
+ # Debounce: flag file + single delayed event
294
+ on_email() {
295
+ echo "$SUBJECT" >> /tmp/pending-emails.txt
296
+ if [ ! -f /workspace/events/email-batch.json ]; then
297
+ (sleep 30 && mv /tmp/pending-emails.txt /workspace/events/email-batch.json) &
298
+ fi
299
+ }
300
+ ```
301
+
302
+ Or simpler: use a periodic event to check for new emails every 15 minutes instead of immediate events.
303
+
304
+ ### Limits
305
+
306
+ Maximum 5 events can be queued. Don't create excessive immediate or periodic events.
307
+ ```