Skip to content

feat(api): scheduled-task tools via vercel queues with self-host fall…#2

Merged
aidenybai merged 5 commits into
mainfrom
scheduled-tasks
May 7, 2026
Merged

feat(api): scheduled-task tools via vercel queues with self-host fall…#2
aidenybai merged 5 commits into
mainfrom
scheduled-tasks

Conversation

@aidenybai

@aidenybai aidenybai commented May 7, 2026

Copy link
Copy Markdown
Member

…back

Adds three agent tools (schedule_task / list_scheduled_tasks / cancel_scheduled_task) that let pookie schedule one-shot or recurring work in a thread. at fire time the consumer posts <@pookie> {prompt} into the original thread and runs handleSlackMessage, so the agent responds in-thread off its own self-prompt.

built on @vercel/queue with at-least-once redelivery dedup, encrypted records at rest, daisy-chain split for intervals > 7 days, and graceful decline (validation error with cron guidance) on self-hosted pookie.


Note

Medium Risk
Introduces a new queue consumer endpoint plus Redis-persisted scheduled task execution, which impacts background job execution and Slack posting behavior; guardrails are included but misconfiguration could expose task triggering or cause unexpected runs.

Overview
Adds scheduled task support to the agent via new tools schedule_task, list_scheduled_tasks, and cancel_scheduled_task, enabling one-shot and recurring runs that re-invoke the agent in the original Slack thread.

Implements Vercel-Queues-backed delivery with a new POST /api/queues/scheduled-task consumer, including platform-availability checks, basic request header gating, retry/backoff behavior, and at-least-once deduplication.

Persists scheduled task records in Redis with encryption at rest, per-team/per-user limits, cancellation authorization, failure tracking/retirement, and sanitization to prevent Slack broadcast/secret leakage; adds vercel.json trigger config, @vercel/queue dependency, and comprehensive tests around validation, auth, dedup, TTL, and route guards.

Reviewed by Cursor Bugbot for commit 15dec05. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented May 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pookie Ready Ready Preview, Comment May 7, 2026 4:07am

Comment thread apps/api/server/scheduling/run-task.ts Outdated
@NisargIO

NisargIO commented May 7, 2026

Copy link
Copy Markdown
Member

bugbot run

@cursor

cursor Bot commented May 7, 2026

Copy link
Copy Markdown

Skipping Bugbot: Bugbot is disabled for this repository. Visit the Bugbot dashboard to update your settings.

@NisargIO

NisargIO commented May 7, 2026

Copy link
Copy Markdown
Member

bugbot run

Comment thread apps/api/server/scheduling/index.ts Outdated
Comment thread apps/api/server/scheduling/run-task.ts Outdated
@NisargIO

NisargIO commented May 7, 2026

Copy link
Copy Markdown
Member

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 5ca9b01. Configure here.

aidenybai added 3 commits May 6, 2026 20:43
…back

Adds three agent tools (schedule_task / list_scheduled_tasks /
cancel_scheduled_task) that let pookie schedule one-shot or recurring
work in a thread. at fire time the consumer posts `<@Pookie> {prompt}`
into the original thread and runs handleSlackMessage, so the agent
responds in-thread off its own self-prompt.

built on @vercel/queue with at-least-once redelivery dedup, encrypted
records at rest, daisy-chain split for intervals > 7 days, and graceful
decline (validation error with cron guidance) on self-hosted pookie.
prevents permission/identity laundering and tightens authorization
across the scheduling subsystem. five user-visible behavior changes:

1. broadcasts (<!channel>, <!here>, <!everyone>, <!subteam^...>) in a
   scheduled prompt are stripped from the visible bot post — agent still
   sees the original directive in the synthetic message text.
2. visible post now starts with `_(scheduled by <@user>)_` so the
   originator is always traceable, even though the bot is the poster.
3. recurring tasks now require a 10-minute minimum interval (was 1m).
   one-shot reminders keep the 1m floor.
4. per-user cap of 10 active scheduled tasks added on top of the
   per-team cap of 50 — prevents one member from monopolizing a team's
   scheduling budget or compute.
5. cancel_scheduled_task and list_scheduled_tasks now scope to the
   calling user. cancelling another user's task returns wrong_user;
   listing returns only your own tasks.

defense-in-depth on the consumer route: in addition to the vercel.json
queue trigger air-gap, the route now requires both the cloudevent v2beta
type header and a vercel platform header (x-vercel-id) to be present.
add escape hatch via ALLOW_QUEUE_LOCAL_FORGERY for local testing.

errors persisted to redis (lastError) and emitted to logs are now
redacted: secret-shaped substrings (long opaque tokens, jwts, base64
ids) replaced with [redacted] before storage / log emit.

vercel.json test now asserts the trigger `type` (queue/v2beta) in
addition to topic match — a missing type field would silently disable
the air-gap.

22 new tests covering: broadcast stripping, error redaction, cross-team
+ cross-user cancel, per-user list scoping, per-user limit, recurring
minimum, and consumer route auth.
oxlint's typescript/consistent-type-imports rule rejects `typeof import("...")`
in type position. switch to `import type * as Module` + `typeof Module` to
unblock the lint job on CI without changing runtime behavior.
…republish failure

addresses two bugs flagged on the open PR review:

1. cursor[bot] HIGH (idempotency-key collision): the publish-time
   idempotency key was `${taskId}:${chunkSeconds}:${remainingSeconds}`,
   fully deterministic across recurring fires of the same task. vercel
   queues silently drops duplicates within the message TTL (24h
   default), so an hourly recurring task would fire once and then every
   subsequent re-publish would dedup-collide and be discarded — recurring
   tasks silently stop after the first run. fix: include the occurrence's
   target fire time (`nextRunAt`) in the key, so consumer-retry republishes
   for the SAME occurrence still collapse but the NEXT occurrence's
   publish gets a fresh key.

2. cursor[bot] MEDIUM / vercel[bot]: after a successful recurring run,
   updateScheduledTaskAfterRun resets failureCount to 0 and bumps
   nextRunAt + lastRunAt in redis. but if the next-occurrence publish
   then failed, trackFailure was called with the STALE pre-update record,
   reverting nextRunAt and re-incrementing the prior failureCount —
   meaning a single publish blip on a previously-flaky task could
   immediately retire it. fix: updateScheduledTaskAfterRun now returns
   the updated record; run-task uses that for the publish + trackFailure
   path.

regression tests added for both: one verifying the publish call uses a
fresh occurrence-time key per recurring fire, one verifying the failure
recorded after a successful run sees failureCount=0 (not the prior count).

also adds a unit test on publishScheduledTaskMessage's idempotency key
to lock in the consumer-retry-collapse semantics: same occurrence + same
chunk shape produces the same key, different occurrences produce
different keys.
Comment thread apps/api/server/scheduling/store.ts
cursor[bot] flagged a team-set-orphaning bug:
updateScheduledTaskAfterRun (and recordScheduledTaskFailure /
markScheduledTaskCancelled) reset the individual record's TTL to 90 days
on every write, but never refreshed the team-set key's TTL — only set
once at creation in saveScheduledTask. for recurring tasks running
longer than 90 days without any new task being scheduled in the same
team, the team-set key would expire while the record kept refreshing
itself, making the task invisible to listScheduledTasksForTeam (and
therefore list_scheduled_tasks / cancel_scheduled_task) yet still firing
via queue messages indefinitely.

fix: writeRecord now atomically does SET + SADD + EXPIRE on the team
set in a single multi(), so EVERY update path keeps both keys in sync.
saveScheduledTask becomes a thin delegate (it was already doing the same
multi shape inline).

regression tests in scheduling-store-ttl.test.ts verify the multi shape
for all four write paths: save, updateAfterRun, recordFailure, and
markCancelled. each asserts SET + SADD + EXPIRE were all called against
the right keys.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 15dec05. Configure here.

messageId,
});
return { status: "redelivered", taskId: message.taskId };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early dedup claim permanently blocks queue retries

Medium Severity

claimDedupSlot is called at the very start of processScheduledTaskMessage, before any processing occurs. If a subsequent operation throws (e.g., loadScheduledTask, deleteScheduledTask, updateScheduledTaskAfterRun, or recordScheduledTaskFailure hitting a transient Redis error), the exception propagates to handleCallback, which triggers a retry via RETRY_HANDLER_OPTIONS. However, retries reuse the same messageId, and since the dedup slot (24h TTL) was already claimed, every retry is silently suppressed as a "redelivery." This effectively makes the RETRY_HANDLER_OPTIONS retry handler and vercel.json's retryAfterSeconds: 60 dead code for all transient failures, and permanently drops the task.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 15dec05. Configure here.

@aidenybai aidenybai merged commit 22ef093 into main May 7, 2026
7 checks passed
aidenybai added a commit that referenced this pull request May 7, 2026
Closes the four actionable findings from the review pass:

#1 (medium) -- detection-source mismatch between auto-react and system
reminder. The auto-react in handleSlackMessage previously only inspected
[currentMessage, ...skippedMessages], so a uwu/owo/meow that arrived as
a Redis-drained follow-up mid-turn (round 2+) flipped pet-mode on in
the system reminder but never got its cat reaction. Auto-react
bookkeeping now lives at the turn scope in handleSlackMessage with a
single \`petAutoReacted\` flag and a \`tryPetAutoReact(candidates)\`
helper called both on round 0 input AND after each Redis drain. To
keep the messageId attached, drainFollowUps now returns the full
QueuedFollowUp[] (text + messageId) instead of \`string[]\`; callers
that only need text \`.map((f) => f.text)\`. The downstream
followUpMessages -> system reminder pipeline is unchanged.

#2 (low) -- belt-and-suspenders: findUwuTriggerMessage now filters out
candidates with empty \`id\` so a malformed Slack message with text but
no ts can't trigger a 400 from reactions.add.

#5 (low) -- test coverage: resolveSlackEmojiShortcode now has cases
for the bare cat emojis (🐱 → cat, 🐈 → cat2), skin-tone modifiers
(👍🏻 / 👍🏿 → +1), and the bare-codepoint heart without its variation
selector (\\u2764 → heart).

#6 (low) -- tracing observability: stamp \`pookiebot.uwu_mode: true\`
when the per-round detection lights up, plus
\`pookiebot.uwu_trigger_message_id\` when the auto-react fires.
Consistent with existing \`pookiebot.dropped_card\` etc. attributes.

#7 (doc nit) -- the \`already_reacted\` comment now reflects Slack's
exact-emoji semantics: that branch only fires when the SAME shortcode
is already on the message, so reporting success there is genuinely
correct (the user-facing reaction state matches what the model wanted).

Tests:
- thread-lock tests rewritten for the QueuedFollowUp[] return shape
- agent-follow-ups mocks updated to mock objects with messageId
- four new resolveSlackEmojiShortcode cases for #5
- 281 total tests passing
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