Skip to content

[Feat] Let Fast Sessions schedule reminders and recurring checks - #2239

Merged
daniel-lxs merged 11 commits into
developfrom
feat/session-wakeups
Sep 5, 2026
Merged

[Feat] Let Fast Sessions schedule reminders and recurring checks#2239
daniel-lxs merged 11 commits into
developfrom
feat/session-wakeups

Conversation

@daniel-lxs

@daniel-lxs daniel-lxs commented Sep 4, 2026

Copy link
Copy Markdown
Member

​Opened on behalf of @daniel-lxs. Follow up by mentioning @roomote-roomote, in the web UI, or in Slack.

Related issue

No linked issue; maintainer-authored feature work.

Why this PR exists

  • A maintainer explicitly invited this PR in the linked issue or discussion
  • I am a maintainer / this is internal Roomote work

Fast Sessions need reminders and recurring checks that resume the same conversation instead of detached automations or waiting inference turns.

What changed

  • Add conversation-scoped manage_wakeups create/list/get/cancel with relative, absolute, interval, and timezone-aware cron schedules. Tight cadences require bounds.
  • Persist wakeups in additive migration 0077. BullMQ hints feed the durable parent-event inbox before schedule advancement, with idempotent occurrence admission and lost-hint recovery.
  • Serialize create admission to enforce the ten-active cap. Dedupe handles JSONB key ordering and relative-delay identity across retries.
  • Deliver as the creating user in the existing Session, checking cancellation/archive before running and replying. Include tool guidance and documentation.
  • Archive now acquires the same Fast conversation turn lock held during reply delivery before committing archivedAt. After a two-second contention retry budget, a busy Session returns retryable CONFLICT and stays unarchived. Errors fail closed and acquired locks release in finally; 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

  • Archive follow-up: 31 web tests pass, including eight archive regressions using real PostgreSQL, the actual turn-lock implementation and mocked Redis ownership. Covers in-flight reply serialization, timeout without archival, acquisition/update failures, cleanup and permissions. 71 SDK lifecycle tests and web check-types pass.
  • Earlier dedupe validation: 12 DB tests, 27 schedule/parser tests, 5 contract tests, and 101 SDK queue/delivery tests passed. Development migrations applied successfully.
  • Full pre-push oxlint, residual ESLint, check-types:fast, and Knip pass. No live-provider/model smoke test was rerun in this fixer cycle.
  • Visual proof remains blocked (proof capture timed out); no captures are claimed or retried. Remote checks and automatic review must pass at the final head.

Checklist

  • The PR title follows the repo convention: [Fix], [Feat], [Improve], [Refactor], [Docs], or [Chore] followed by a user-facing description
  • This PR is small and scoped to one change
  • pnpm lint and pnpm check-types pass locally
  • I added tests or included a clear manual validation note above
  • I removed secrets, tokens, private keys, and customer data from code, logs, and screenshots
  • If this change should appear in the changelog, I ran pnpm changeset

The static commands were the pre-push equivalents above, not the two literal full commands. The existing .changeset/session-wakeups.md is retained; this fixer did not run the interactive changeset command.

@roomote-community

roomote-community Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

No code issues found. See task

  • A cancelled or archived wakeup can still be delivered after its parent event has already been admitted (packages/sdk/src/server/lib/fast-agent-parent-event.ts:2198).
  • A cron schedule can bypass the tight-interval safety cap and run indefinitely every minute (packages/cloud-agents/src/server/session-wakeups/parse.ts:161).
  • An archive can land after the reply-time wakeup check but before dispatch, allowing an archived Session to reply (apps/web/src/trpc/commands/sessions/index.ts:156).
  • Retrying a relative one-shot reminder creates duplicate wakeups because its resolved absolute timestamp differs on each call (packages/db/src/lib/session-wakeups.ts:129).

Reviewed e92ccc1

Comment thread packages/sdk/src/server/lib/session-wakeups.ts
@daniel-lxs

Copy link
Copy Markdown
Member Author

Local smoke test (2026-09-04)

Ran against a local stack (web, api, bullmq via PM2; Postgres and Redis in Docker; OpenRouter openai/gpt-5.4 through OpenCode).

Step Result
"Remind me in 2 minutes to check the deploy" in a web Session One manage_wakeups call, once schedule; Fast confirmed the time in one sentence. Fired 100 ms after its due time; the wake turn replied "Time to check the deploy." in the same Session 19 s later. Row moved to completed.
"Every 1 minute for 3 runs, tell me the current UTC time" interval with maxRuns: 3; run 1 fired 74 ms after due and replied with the time.
"Cancel that timer" mid-way Row moved to cancelled; the already-queued run-2 job was skipped by the worker ("Wakeup is cancelled"), no further jobs.
bullmq restart with a pending wakeup The startup recovery sweep re-added the hint and the wakeup fired once. Also observed: a stale conversation turn lock from a process killed mid-turn delayed one delivery until the lock's TTL expired (~10 min); that is existing Fast lock behaviour, not wakeup-specific.
Archive cancels wakeups Covered by the db helper test; not exercised through the UI in this run.

Fixes that came out of the run (second commit)

  • BullMQ rejects custom job ids containing :; the delayed fire job never enqueued. Job ids now use -.
  • The model fills every optional argument (at: "", wakeupId: "", until: "none", maxRuns: 0) and each strict rejection cost a retry (12 to 17 tool calls in the first two sessions). Arguments are now normalized before validation, once ignores stray maxRuns/until, and inMinutes wins when a computed at is sent alongside it. After the fix the same request took one call.

Local environment notes

  • Fast turns were failing locally with OpenCode's generic "Unexpected server error" before any of this: the installed opencode was 1.17.8, which returns 500 when the prompt carries a variant (the session's reasoning effort). The repo pins the 1.18.10 SDK; upgrading the global binary to 1.18.10 fixed it. Worth adding to LOCAL_DEVELOPMENT.md.
  • pm2 restart of api/bullmq can leave the previous node --watch dist/index.js child alive and holding the port, so the new process dies with EADDRINUSE. Killing the orphan and restarting again resolves it.

@daniel-lxs

Copy link
Copy Markdown
Member Author

Schedule is now one string (third commit)

The structured schedule union was the source of the placeholder thrash, so it is gone. create now takes name, prompt, schedule, and an optional reportPolicy; the schedule is a single required string parsed server-side into the same stored shape:

in 2m
at 2026-09-04T15:00:00-04:00
every 10m
every 1m x3
every 10m until 2026-09-04T18:00:00Z
cron 0 9 * * 1-5 America/New_York

maxRuns and until are no longer tool fields; the tight-interval cap is enforced through x<count> / until. Unreadable input gets the grammar back in the error. Parser tests cover spellings like "in 2 minutes", "every minute for 3 runs", and the ambiguous bare "2m".

Local smoke test, repeated on the new contract

Step Result
"Remind me in 2 minutes to check the deploy" One manage_wakeups call, once schedule from in 2m; fired 14 ms after due; "Time to check the deploy." posted in the Session 15 s later; row completed.
"Every 1 minute for 3 runs, tell me the current UTC time" One call, every 1m x3; run 1 fired 49 ms after due and replied with the time.
"Cancel that timer" One call; row cancelled, the queued run-2 job was skipped by the worker, no delayed jobs left.

Every create and cancel in this run took exactly one tool call, versus 12 to 17 with the union before the argument normalization.

Comment thread packages/cloud-agents/src/server/session-wakeups/parse.ts
Comment thread packages/cloud-agents/src/server/session-wakeups/parse.ts Fixed
@daniel-lxs
daniel-lxs force-pushed the feat/session-wakeups branch 2 times, most recently from d905925 to 0f11cd7 Compare September 5, 2026 00:23
Comment thread apps/web/src/trpc/commands/sessions/index.ts Outdated
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.
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.
@roomote-roomote

roomote-roomote Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

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:

  • pnpm exec dotenvx run -f .env.test -- pnpm --filter @roomote/web exec vitest run src/trpc/commands/sessions/archive.test.ts src/trpc/commands/sessions/index.test.ts src/lib/server/sessions.test.ts (31 tests, including 8 archive regressions).
  • pnpm exec dotenvx run -f .env.test -- pnpm --filter @roomote/sdk exec vitest run src/server/lib/fast-agent-parent-event.test.ts src/server/lib/session-wakeups.test.ts (71 tests).
  • pnpm --filter @roomote/web check-types, changed-file formatting and git diff --check.
  • Full pre-push oxlint, residual ESLint, check-types:fast, and Knip.

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 REVIEW_REQUIRED and BLOCKED, despite conflict-free mergeability and green checks. An eligible reviewer approval is still required. This run did not approve itself, merge, or bypass protection. Branch-protection details could not be inspected with the integration (403), so no specific approval count is claimed.

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 (proof capture timed out); no captures were claimed or retried.

Comment thread packages/db/src/lib/session-wakeups.ts Outdated
@roomote-roomote roomote-roomote Bot changed the title [Feat] Session wakeups: let a Fast Session schedule a message to itself [Feat] Let Fast Sessions schedule reminders and recurring checks Sep 5, 2026
@daniel-lxs
daniel-lxs merged commit 4738f29 into develop Sep 5, 2026
18 checks passed
@daniel-lxs
daniel-lxs deleted the feat/session-wakeups branch September 5, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants