feat(api): scheduled-task tools via vercel queues with self-host fall…#2
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
bugbot run |
|
Skipping Bugbot: Bugbot is disabled for this repository. Visit the Bugbot dashboard to update your settings. |
|
bugbot run |
|
bugbot run |
There was a problem hiding this comment.
✅ 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.
…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.
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 }; | ||
| } |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 15dec05. Configure here.
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


…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, andcancel_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-taskconsumer, 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.jsontrigger config,@vercel/queuedependency, 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.