[Feat] Let Fast Sessions schedule reminders and recurring checks - #2239
Conversation
|
No code issues found. See task
Reviewed e92ccc1 |
Local smoke test (2026-09-04)Ran against a local stack (web, api, bullmq via PM2; Postgres and Redis in Docker; OpenRouter
Fixes that came out of the run (second commit)
Local environment notes
|
Schedule is now one string (third commit)The structured schedule union was the source of the placeholder thrash, so it is gone.
Local smoke test, repeated on the new contract
Every create and cancel in this run took exactly one tool call, versus 12 to 17 with the union before the argument normalization. |
49d16fd to
074f738
Compare
d905925 to
0f11cd7
Compare
Add the manage_wakeups Fast native tool (create, list, get, cancel) backed by a session_wakeups table. A wakeup is a durable row plus one delayed BullMQ job; when it fires, a scheduled_wakeup platform event is admitted into the conversation's existing parent-event inbox and runs as a normal turn with the full history in context. Occurrences are claimed with a compare-and-set on next_run_at, so duplicate jobs cannot double-fire, and a 60s recovery sweep re-adds hints for due rows. One-shot wakeups always reply; recurring ones stay quiet unless notable and retire after five consecutive failed turns. Archiving a Session cancels its wakeups.
… BullMQ job ids - BullMQ rejects custom job ids containing ':'; use '-' between the wakeup id and occurrence time so delayed fire jobs actually enqueue. - Strip empty strings, null, 'none'-style placeholders, and non-positive caps from manage_wakeups arguments before validation. Models fill every optional field, and each strict rejection cost a retry. - A once schedule ignores stray maxRuns/until and prefers inMinutes when a computed 'at' is sent alongside it, instead of failing.
…red union
Models fill every optional structured field with placeholders, and each
rejected placeholder cost a retry. The tool now takes a single required
schedule string ("in 20m", "at <iso>", "every 10m x3", "every 10m until
<iso>", "cron 0 9 * * 1-5 America/New_York") parsed server-side into the
same stored schedule. This removes the discriminated union and the
maxRuns/until fields from the tool surface; the create action is now name,
prompt, schedule, and an optional reportPolicy.
…equency cron - Delivery of a scheduled_wakeup event now re-reads the row and skips when the wakeup was cancelled or failed after its occurrence was admitted, so cancel and archive keep their guarantee even against an in-flight event. A row that completed at claim time (one-shot or final run) still runs. - Cron schedules are held to the same tight-interval cap as intervals by sampling the gap between upcoming occurrences; "cron * * * * *" now needs "x<count>" or "until <iso>", which the cron grammar accepts alongside an optional timezone.
…rivate The parser already collapses whitespace and the contract caps the string length, so the patterns use literal single spaces instead of \s+ runs that CodeQL flagged as polynomial. estimateCronMinGapMinutes is only used inside the schedule module.
Archiving cancels a Session's wakeups, but that cancellation is best-effort after the archive itself. Delivery now also checks the Session and skips a scheduled_wakeup whose Session is archived, so a failed cancellation cannot make an archived Session speak.
20f4de3 to
9557854
Compare
A cancel or archive that lands while the wake turn is generating must still win. The wakeup turn's postReply is now guarded: it re-checks the wakeup row and the Session immediately before posting, drops the reply if either was superseded, and aborts the turn's signal so no further tool calls run. The next drain of the event settles it as skipped.
|
Following explicit author approval, pushed e92ccc18 to close the archive-versus-reply race. Archive takes the existing Fast conversation turn lock before committing archivedAt; contention beyond its two-second retry budget returns CONFLICT without archival. Acquisition errors fail closed and acquired locks release in finally. No DB transaction spans external reply calls. Validation passed:
Final-head CI and Roomote code review now pass. Automatic review reports no code issues and all four findings addressed; all five review threads are resolved. The earlier dedupe fixes remain included. PR metadata and the archive discussion are updated with evidence; worktree is clean. Remaining merge gate: GitHub reports Test limits: real PostgreSQL with mocked Redis ownership/controlled reply completion, not live provider delivery. The two-second budget bounds contention retries rather than Redis network latency. Visual proof remains blocked ( |
Related issue
No linked issue; maintainer-authored feature work.
Why this PR exists
Fast Sessions need reminders and recurring checks that resume the same conversation instead of detached automations or waiting inference turns.
What changed
manage_wakeupscreate/list/get/cancel with relative, absolute, interval, and timezone-aware cron schedules. Tight cadences require bounds.archivedAt. After a two-second contention retry budget, a busy Session returns retryableCONFLICTand stays unarchived. Errors fail closed and acquired locks release infinally; no database transaction spans provider I/O.Users receive reminders and monitor updates in their existing Session. Archive synchronization is now included with explicit author approval, superseding its earlier deferral. The budget bounds contention retries, not underlying Redis request latency. No new wakeup-management UI or sandbox-task resume is included.
How it was tested
check-typespass.check-types:fast, and Knip pass. No live-provider/model smoke test was rerun in this fixer cycle.proof capture timed out); no captures are claimed or retried. Remote checks and automatic review must pass at the final head.Checklist
[Fix],[Feat],[Improve],[Refactor],[Docs], or[Chore]followed by a user-facing descriptionpnpm lintandpnpm check-typespass locallypnpm changesetThe static commands were the pre-push equivalents above, not the two literal full commands. The existing
.changeset/session-wakeups.mdis retained; this fixer did not run the interactive changeset command.