Skip to content

feat(calendar): scheduled LLM agent triggers - #862

Merged
2witstudios merged 14 commits into
masterfrom
pu/triggers
Apr 9, 2026
Merged

2witstudios merged 14 commits into
masterfrom
pu/triggers

Conversation

@2witstudios

@2witstudios 2witstudios commented Apr 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Agents can self-schedule future work by creating calendar events that fire LLM executions at the specified time
  • New agentTrigger parameter on create_calendar_event — any agent can schedule work for itself or another agent (respecting RBAC)
  • Cancellation via delete_calendar_event — trashing a trigger-linked event automatically cancels pending/claimed/running triggers
  • New calendarTriggers table tracks execution state (pending → running → completed/failed/cancelled)
  • Cron endpoint polls every 2 minutes with atomic claim pattern and batch execution (max 5 concurrent)
  • Executor adapter reuses existing executeWorkflow() — no duplicated infrastructure
  • Calendar read tools surface trigger status on events so agents can see their scheduled work
  • Cost control: triggers consume from existing daily AI call rate limits (Free: 50/day, Pro: 200/day, Founder: 500/day, Business: 1000/day)
  • Shared parseDateTime() extracted to timestamp-utils.ts (was duplicated in write-tools)

Security hardening (5 review rounds)

  • Agent, instruction, and context pages validated against caller's accessible drives — personal pages (null driveId) rejected
  • Context pages restricted to same drive as event (cross-drive pages silently dropped by executeWorkflow)
  • Scheduling user's drive membership re-verified at execution time
  • Instruction page access re-verified at execution time — revoked access silently excludes content
  • Agent page existence verified before consuming usage credit (cheap preflight)
  • Trigger cancellation on event delete covers pending + claimed + running states (race-safe)
  • Recurrence + agentTrigger combination rejected (single trigger row can't serve recurring events)
  • Cron skips triggers whose calendar events are trashed

Changed files

File Purpose
packages/db/src/schema/calendar-triggers.ts Schema, enum, relations, types
apps/web/src/lib/workflows/calendar-trigger-executor.ts Thin adapter over executeWorkflow() with preflight checks
apps/web/src/app/api/cron/calendar-triggers/route.ts Cron polling endpoint
apps/web/src/lib/ai/tools/calendar-write-tools.ts agentTrigger param on create_calendar_event, trigger sync/cancel on update/delete
apps/web/src/lib/ai/tools/calendar-read-tools.ts Trigger status surfacing on events
apps/web/src/lib/ai/core/timestamp-utils.ts Shared parseDateTime + timezone utilities

Test plan

  • pnpm db:generate succeeds (migration: 0095_blushing_firelord.sql)
  • pnpm --filter web build / typecheck passes with zero errors
  • 120 tests pass across 5 test suites (write-tools: 55, executor: 13, cron route: 7, timestamp-utils: 42, ai-tools: 3)
  • agentTrigger validation: driveId required, prompt/instructionPageId required, agent page type/trash/drive checks
  • Trigger sync on event update (startAt → triggerAt)
  • Trigger cancel on event delete (pending + claimed + running)
  • Execution-time drive access re-check
  • Agent preflight before usage accounting
  • Recurrence + trigger rejection
  • Cross-drive context page rejection
  • pnpm db:migrate applies cleanly against dev database
  • Manual end-to-end: create event with agentTrigger → cron fires → agent executes

🤖 Generated with Claude Code

@vercel

vercel Bot commented Apr 9, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
pagespace-master-plan Ready Ready Preview, Comment Apr 9, 2026 5:06pm

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Apr 9, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds calendar-event-driven scheduled agent work: DB schema for triggers, AI tools to schedule/cancel triggers, parsing utilities, a cron POST endpoint that claims and executes due triggers in batches, an execution engine that builds prompts and runs workflows, and tests/docs for the end-to-end flow.

Changes

Cohort / File(s) Summary
Database Schema
packages/db/src/schema/calendar-triggers.ts, packages/db/src/schema.ts, packages/db/drizzle/0095_blushing_firelord.sql, packages/db/drizzle/meta/_journal.json
New calendar_triggers table and CalendarTriggerStatus enum; relations to events/pages/drives/users, unique (calendarEventId, occurrenceDate), indexes for polling; migration and journal entry added.
Cron Polling Endpoint
apps/web/src/app/api/cron/calendar-triggers/route.ts, docker/cron/crontab, apps/web/src/app/api/cron/calendar-triggers/__tests__/route.test.ts
New signed POST cron endpoint invoked every 2 minutes that resets stale running triggers, claims due pending triggers (atomic UPDATE...RETURNING in batches), executes them concurrently via the executor, handles missing/trashed events, records errors, and returns an execution summary; contract tests added.
AI Tools: scheduling & cancellation
apps/web/src/lib/ai/tools/calendar-trigger-tools.ts, apps/web/src/lib/ai/tools/__tests__/calendar-trigger-tools.test.ts
New calendarTriggerTools export with schedule_agent_work and cancel_scheduled_work; input validation, access checks, transactional creation of calendar events+triggers, backfill metadata, broadcasting; cancellation is creator-only and atomically enforced; comprehensive tests.
Tool registry & filtering
apps/web/src/lib/ai/core/ai-tools.ts, apps/web/src/lib/ai/core/tool-filtering.ts, apps/web/src/lib/ai/core/__tests__/ai-tools.test.ts
Added calendarTriggerTools into pageSpaceTools; added schedule_agent_work and cancel_scheduled_work to WRITE_TOOLS so they are excluded in read-only modes; tests updated to include mocked tools.
Calendar read/write integration
apps/web/src/lib/ai/tools/calendar-read-tools.ts, apps/web/src/lib/ai/tools/calendar-write-tools.ts
Read tools now batch-load trigger metadata and include scheduledWork in formatted event responses; write tools now use centralized parseDateTime, update pending trigger triggerAt on event edits, and cancel pending triggers on event deletion.
Trigger execution engine
apps/web/src/lib/workflows/calendar-trigger-executor.ts, apps/web/src/lib/workflows/workflow-executor.ts, apps/web/src/lib/workflows/__tests__/calendar-trigger-executor.test.ts
New executeCalendarTrigger that consumes subscription usage, builds event+instruction prompts, delegates to executeWorkflow, persists status/duration/error/conversationId, and logs outcomes; WorkflowExecutionResult extended with conversationId; tests added.
Timestamp parsing utilities & tests
apps/web/src/lib/ai/core/timestamp-utils.ts, apps/web/src/lib/ai/core/__tests__/timestamp-utils.test.ts
Introduced parseDateTime using ISO / naive-ISO+IANA timezone interpretation and chrono-node fallback with DST-aware corrections; tests added for ISO, naive timezone, natural language, relative parsing, and error cases.
Calendar write-tool cleanup
apps/web/src/lib/ai/tools/calendar-write-tools.ts
Removed local chrono-node parsing, switched to shared parseDateTime, added trigger synchronization/cancellation logic on event changes.
Documentation
docs/1.0-overview/changelog.md
Changelog entry documenting scheduled agent work, new tools, trigger lifecycle, cron poller behavior, and scheduling constraints/cost semantics.

Sequence Diagram(s)

sequenceDiagram
    participant Cron as Cron Job
    participant API as /api/cron/calendar-triggers
    participant DB as Database
    participant Exec as Executor
    participant LLM as Workflow/LLM

    Cron->>API: POST /api/cron/calendar-triggers
    API->>DB: UPDATE running triggers older than 10m -> failed
    API->>DB: SELECT pending triggers WHERE triggerAt <= now LIMIT 50
    alt No due triggers
        API-->>Cron: { executed: 0 }
    else Triggers due
        loop For each batch (<=5)
            API->>DB: UPDATE pending→running (atomic RETURNING)
            DB-->>API: claimed triggers
            API->>DB: SELECT calendarEvents for claimed triggers
            par For each claimed trigger
                API->>Exec: executeCalendarTrigger(trigger, event)
                Exec->>DB: incrementUsage(scheduledById)
                alt Usage denied
                    Exec->>DB: UPDATE trigger -> failed
                    Exec-->>API: result failure
                else Usage allowed
                    Exec->>DB: SELECT attendees, instruction page
                    Exec->>LLM: executeWorkflow(synthetic workflow)
                    LLM-->>Exec: { success, conversationId?, error? }
                    Exec->>DB: UPDATE trigger (completed/failed, conversationId, duration)
                    Exec-->>API: result
                end
            end
            API->>DB: aggregate results / persist failures for exceptions
        end
        API-->>Cron: { executed: N, total: M, errors?: [...] }
    end
Loading
sequenceDiagram
    participant User as Caller
    participant Tool as schedule_agent_work
    participant Validate as Validators
    participant DB as Database
    participant Broadcast as Broadcaster

    User->>Tool: schedule_agent_work(input)
    Tool->>Tool: require authenticated user
    Tool->>Tool: normalize timezone
    Tool->>Validate: require prompt OR instructionPageId
    Tool->>Validate: verify drive membership for driveId
    Tool->>DB: load agent page, verify type/drive/access
    alt instructionPageId
        Tool->>DB: load instruction page, verify access/not trashed
    end
    Tool->>Tool: parseDateTime(triggerAt) → must be future
    Tool->>DB: BEGIN transaction
    Tool->>DB: INSERT calendar_event
    Tool->>DB: INSERT calendar_trigger
    Tool->>DB: UPDATE calendar_event.metadata with triggerId
    Tool->>DB: COMMIT
    Tool->>Broadcast: broadcastCalendarEvent(created event)
    Tool-->>User: { triggerId, eventId, scheduledFor, agentName }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐰 I dug a burrow in calendar ground,
Planted triggers that hum and sound.
At scheduled dawn they wake and run,
Prompts unfurl beneath the sun.
Hop! — the agents chitter, work is done.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(calendar): scheduled LLM agent triggers' clearly and concisely summarizes the main feature addition: enabling agents to schedule LLM executions via calendar events. It is specific, directly related to the primary change, and follows conventional commit format.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/triggers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/lib/ai/tools/calendar-read-tools.ts`:
- Around line 128-134: The response currently includes triggerInfo.agentPageId
inside the scheduledWork object (see scheduledWork, triggerInfo,
fetchTriggerInfoForEvents, formatEventForResponse), which can leak cross-drive
page IDs; remove agentPageId from the returned scheduledWork or conditionally
include it only after calling the centralized permission check (use the
permission helpers from packages/lib/src/permissions/) to verify the caller can
view that page before adding agentPageId; update both occurrences (lines around
the scheduledWork block and the other instance noted) to either omit agentPageId
entirely or wrap its inclusion in the centralized permission function, ensuring
no custom permission logic is introduced.

In `@apps/web/src/lib/ai/tools/calendar-trigger-tools.ts`:
- Around line 237-273: The cancel flow has a race: the select-then-update can be
lost to the cron flipping status to "running"; change cancel routine to perform
an atomic transactional update guarded by status='pending' (and ensure
scheduledById===userId) against calendarTriggers and, in the same transaction,
update calendarEvents (isTrashed/trashedAt/updatedAt); check the affected-rows
count from the guarded update—if zero, do a fresh SELECT of calendarTriggers by
triggerId to read and return the current status/error (e.g., already
running/completed/failed/cancelled or not found); use the existing db,
calendarTriggers, calendarEvents, triggerId and userId symbols and ensure
completedAt is set when marking cancelled.

In `@apps/web/src/lib/workflows/calendar-trigger-executor.ts`:
- Around line 121-131: The current code reads instructionPage by
trigger.instructionPageId without verifying that trigger.scheduledById still has
access; update the logic in calendar-trigger-executor.ts to either (A) snapshot
the instruction page content at schedule time and store it on the trigger so
execution uses the snapshot, or (B) before pushing instructionPage into parts at
runtime, call the centralized permission check from packages/lib/src/permissions
(do not implement custom checks) to confirm trigger.scheduledById can read the
page (use pages and instructionPage identifiers) and only include
instructionPage.content if that permission check passes; make sure to reference
trigger.scheduledById, instructionPage, pages and avoid adding ad-hoc access
checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b7d835d4-413f-4503-b968-02bf9172c0c7

📥 Commits

Reviewing files that changed from the base of the PR and between 28b4659 and 6e5d918.

📒 Files selected for processing (17)
  • apps/web/src/app/api/cron/calendar-triggers/route.ts
  • apps/web/src/lib/ai/core/__tests__/timestamp-utils.test.ts
  • apps/web/src/lib/ai/core/ai-tools.ts
  • apps/web/src/lib/ai/core/timestamp-utils.ts
  • apps/web/src/lib/ai/core/tool-filtering.ts
  • apps/web/src/lib/ai/tools/calendar-read-tools.ts
  • apps/web/src/lib/ai/tools/calendar-trigger-tools.ts
  • apps/web/src/lib/ai/tools/calendar-write-tools.ts
  • apps/web/src/lib/workflows/calendar-trigger-executor.ts
  • apps/web/src/lib/workflows/workflow-executor.ts
  • docker/cron/crontab
  • docs/1.0-overview/changelog.md
  • packages/db/drizzle/0094_mysterious_vision.sql
  • packages/db/drizzle/meta/0094_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema.ts
  • packages/db/src/schema/calendar-triggers.ts

Comment thread apps/web/src/lib/ai/tools/calendar-read-tools.ts
Comment thread apps/web/src/lib/ai/tools/calendar-trigger-tools.ts Outdated
Comment thread apps/web/src/lib/ai/tools/calendar-trigger-tools.ts Outdated
Comment thread apps/web/src/lib/workflows/calendar-trigger-executor.ts
2witstudios and others added 3 commits April 9, 2026 00:48
Agents can now self-schedule future work by creating calendar events that
fire LLM executions at the specified time. Any agent can schedule work for
itself or another agent (respecting RBAC). Triggers consume from the
existing daily AI call rate limits for cost control.

- New `calendarTriggers` table + CalendarTriggerStatus enum
- `schedule_agent_work` + `cancel_scheduled_work` AI tools
- Cron endpoint polling every 2 min with atomic claim pattern
- Executor adapter reuses existing workflow-executor
- Calendar read tools surface trigger status on events

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…arseDateTime

P1 Security:
- Enforce drive access on agent and instruction pages in schedule_agent_work
- Restrict cancel_scheduled_work to scheduledById (not any drive member)
- Sync trigger rows with calendar event mutations (update/delete)
- Cron skips triggers whose events are trashed

P2 Reliability:
- Eliminate stale 'claimed' state — claim directly to 'running'
- Add LIMIT 50 to cron discovery query

P3 Correctness:
- Return conversationId from executeWorkflow so trigger stores actual value
- Wrap event+trigger+metadata creation in db.transaction()
- Extract parseDateTime to shared timestamp-utils (eliminates duplication)

Tests & Docs:
- Add parseDateTime unit tests to timestamp-utils.test.ts
- Update changelog with feature entry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ixes

1. Remove agentPageId from scheduledWork response to prevent cross-drive
   page ID leaks (calendar-read-tools.ts)

2. Reject personal pages (null driveId) for agent, instruction, and context
   pages. Validate all contextPageIds against accessible drives
   (calendar-trigger-tools.ts)

3. Make cancel_scheduled_work atomic: single UPDATE with WHERE
   status='pending' AND scheduledById=userId guard in a transaction,
   eliminating the read-then-write race with cron (calendar-trigger-tools.ts)

4. Re-check instruction page drive access at execution time in
   buildTriggerPrompt — if access was revoked after scheduling, the page
   content is silently excluded (calendar-trigger-executor.ts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/lib/workflows/calendar-trigger-executor.ts (1)

109-118: Consider privacy implications of including attendee emails in prompts.

The attendee query selects both name and email, and Line 117 falls back to email when name is absent. This could leak user email addresses into AI prompts and potentially stored conversation logs.

♻️ Suggested change to avoid email fallback
   if (attendees.length > 0) {
-    parts.push(`Attendees: ${attendees.map(a => a.name || a.email).join(', ')}`);
+    parts.push(`Attendees: ${attendees.filter(a => a.name).map(a => a.name).join(', ')}`);
   }

Alternatively, if some attendee identification is necessary when names are absent, consider using a generic placeholder like "unnamed attendee".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/workflows/calendar-trigger-executor.ts` around lines 109 -
118, The attendees query currently selects name and email and the parts.push
uses attendees.map(a => a.name || a.email), which can leak emails into prompts;
modify the query and formatting to avoid returning or using emails: select only
users.name (remove users.email from the .select on eventAttendees/users),
filter/skip records with no name or map missing names to a generic placeholder
like "unnamed attendee" instead of falling back to a.email, and update the
parts.push to join only the safe name/placeholder values (references: attendees
variable, eventAttendees, users, and the parts.push mapping expression).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/lib/workflows/calendar-trigger-executor.ts`:
- Around line 109-118: The attendees query currently selects name and email and
the parts.push uses attendees.map(a => a.name || a.email), which can leak emails
into prompts; modify the query and formatting to avoid returning or using
emails: select only users.name (remove users.email from the .select on
eventAttendees/users), filter/skip records with no name or map missing names to
a generic placeholder like "unnamed attendee" instead of falling back to
a.email, and update the parts.push to join only the safe name/placeholder values
(references: attendees variable, eventAttendees, users, and the parts.push
mapping expression).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f04e540f-00a9-43d7-958a-0ff6af9b32f4

📥 Commits

Reviewing files that changed from the base of the PR and between 6e5d918 and a8e0050.

📒 Files selected for processing (3)
  • apps/web/src/lib/ai/tools/calendar-read-tools.ts
  • apps/web/src/lib/ai/tools/calendar-trigger-tools.ts
  • apps/web/src/lib/workflows/calendar-trigger-executor.ts

Comment thread apps/web/src/lib/ai/tools/calendar-trigger-tools.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (4)
apps/web/src/lib/ai/tools/calendar-read-tools.ts (1)

162-166: Multiple triggers per recurring event will show only one status.

The map overwrites entries by calendarEventId, so recurring events with multiple trigger rows (different occurrenceDate values) will only display the last-processed trigger's status. This may be acceptable for the current UI, but consider returning the "most relevant" trigger (e.g., the next pending one) if users need to see upcoming occurrence statuses.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/ai/tools/calendar-read-tools.ts` around lines 162 - 166, The
current loop in calendar-read-tools.ts builds a Map keyed by calendarEventId and
blindly overwrites entries, losing other triggers for recurring events; update
the logic in the loop that populates map (the map variable, the triggers array,
and usage of calendarEventId) to select and keep the most relevant trigger per
calendarEventId (e.g., prefer the next pending occurrence: compare
occurrenceDate and status of the existing entry vs the incoming trigger and only
replace when the incoming trigger is a closer upcoming pending occurrence or
otherwise more relevant). Ensure you compare the trigger.occurrenceDate and
trigger.status when deciding to call map.set so the Map retains the chosen
triggerId/status rather than the last-seen row.
apps/web/src/lib/ai/tools/calendar-write-tools.ts (1)

509-519: Trigger cancellation on event delete is correct but not transactional with the soft-delete.

The cancellation correctly uses atomic WHERE status='pending' to avoid racing with cron. However, the trigger cancellation (lines 509-519) and event soft-delete (lines 521-529) are separate operations without a transaction wrapper.

If the soft-delete fails after trigger cancellation succeeds, the trigger would be cancelled while the event remains active. This is low-risk since:

  1. The cron already skips trashed events
  2. The cancelled trigger won't execute regardless

Consider wrapping in a transaction if stricter consistency is needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/ai/tools/calendar-write-tools.ts` around lines 509 - 519,
Wrap the trigger-cancellation and the event soft-delete in a single database
transaction so both succeed or both roll back together: when detecting
event.metadata as CalendarTriggerMetadata (eventMeta) and cancelling pending
triggers via db.update(calendarTriggers).set(...).where(...), perform that
update and the subsequent soft-delete update on the calendar event inside a
single db transaction (use the existing db.transaction/transactional API) using
the same eventId/calendarEventId and status='pending' predicates so either both
operations commit or both are rolled back on error.
apps/web/src/app/api/cron/calendar-triggers/route.ts (1)

7-8: Stuck trigger timeout could mark legitimately running triggers as failed.

The 10-minute timeout resets triggers that are still in running state. Per context snippet 1, executeCalendarTrigger has no internal timeout and relies on AI provider response times. If an AI call legitimately takes >10 minutes:

  1. The next cron run marks it failed
  2. The original execution completes and updates to completed/failed, overwriting the stuck-timeout status

This creates a race where the final status depends on timing. Consider either:

  • Increasing the timeout for AI workloads
  • Adding a check in executeCalendarTrigger to skip the final update if status is no longer running

Also applies to: 19-33

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/api/cron/calendar-triggers/route.ts` around lines 7 - 8, The
current STUCK_TRIGGER_TIMEOUT_MS (10 minutes) can mark legitimately long-running
executeCalendarTrigger executions as failed; either raise
STUCK_TRIGGER_TIMEOUT_MS to a much larger value for AI workloads, or (preferred)
make executeCalendarTrigger resilient by reading the trigger's latest status
before writing its final state and only perform the completion update if the
stored status is still "running" (i.e., add a pre-update check inside
executeCalendarTrigger to skip updating when status !== "running"); apply the
same guard where the cron resets stuck runs (lines referenced around 19-33) so
you don't overwrite a true final result.
apps/web/src/lib/ai/tools/calendar-trigger-tools.ts (1)

139-147: Consider clock skew tolerance for "must be in the future" check.

The check parsedTriggerAt <= new Date() may reject valid requests if there's minor clock drift between client and server, or if processing delays push new Date() past a very-near-future triggerAt. Consider a small tolerance (e.g., 30 seconds) for borderline cases:

-if (parsedTriggerAt <= new Date()) {
+const MIN_FUTURE_BUFFER_MS = 30_000; // 30 seconds
+if (parsedTriggerAt.getTime() <= Date.now() + MIN_FUTURE_BUFFER_MS) {

This also prevents scheduling work that would immediately execute before the cron even picks it up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/ai/tools/calendar-trigger-tools.ts` around lines 139 - 147,
The current future-check using parsedTriggerAt <= new Date() is too strict;
introduce a small clock-skew/processing tolerance (e.g., const
FUTURE_TOLERANCE_MS = 30_000) and change the condition to reject only when
parsedTriggerAt.getTime() <= Date.now() + FUTURE_TOLERANCE_MS so near-term times
within the tolerance are treated as valid; update the check around
parseDateTime/parsedTriggerAt/triggerAt accordingly and keep the same error
response when the adjusted check fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/lib/ai/core/timestamp-utils.ts`:
- Around line 226-233: The chrono parsing uses a single fixed offset from
ref.instant which breaks across DST; update the logic to follow the two-pass
pattern used in parseNaiveDatetimeInTimezone: (1) build ref with initial
timezone = getTimezoneOffsetMinutes(timezone, ref.instant) and call
chrono.parseDate(input, ref, { forwardDate: true }); (2) if a parsed Date is
returned, recompute newOffset = getTimezoneOffsetMinutes(timezone, parsed) and
if newOffset !== ref.timezone set ref.timezone = newOffset and call
chrono.parseDate(input, ref, { forwardDate: true }) again so the final result
uses the correct offset. Ensure you reference ref, timezone,
getTimezoneOffsetMinutes, chrono.parseDate and parsed in the change.
- Around line 216-222: Handle naive ISO datetimes before falling back to native
Date and only accept strict ISO/date-only/explicit-offset formats for native
parsing: when timezone is provided, check isNaiveISODatetime(input) first and
call parseNaiveDatetimeInTimezone(input, timezone) if true; otherwise only allow
new Date(input) to be returned when input matches a safe ISO date-only or
ISO-with-offset pattern (reject loose formats like "2024-06-15 14:00" so
timezone is not silently ignored). Update the logic around the isoDate/new
Date(input) branch in timestamp-utils.ts to enforce this ordering and pattern
check.

In `@packages/db/drizzle/0095_mysterious_vision.sql`:
- Around line 24-27: The migration's UNIQUE constraint
"calendar_triggers_event_occurrence_key" on columns calendarEventId and
occurrenceDate allows multiple NULL occurrenceDate values and thus duplicate
one-shot triggers; fix this in the calendar-triggers schema source
(calendar-triggers.ts) by changing the constraint to enforce uniqueness only
when occurrenceDate IS NOT NULL (e.g., use a partial/conditional unique index or
Drizzle's conditional constraint on the calendar_triggers table so uniqueness
applies for non-NULL occurrenceDate), or alternatively add application-level
validation to prevent NULL duplicates, then run pnpm db:generate to regenerate
the SQL migration.

In `@packages/db/drizzle/meta/0094_snapshot.json`:
- Around line 888-893: The snapshot incorrectly includes a resurrected nullable
column "password" for table public.users; remove the "password" entry from the
snapshot JSON (the object where "name": "password") so the snapshot matches the
intended schema, then regenerate the snapshot from your current intended DB
schema and commit the regenerated snapshot (ensuring public.users no longer
contains a password key) to avoid a bogus add-column migration in the next
generated migration.
- Around line 11908-11913: The unique constraint on (calendarEventId,
occurrenceDate) fails to dedupe one-shot triggers because occurrenceDate is
nullable and inserts leave it NULL; update the schema to ensure NULLs are
treated consistently by either: 1) make occurrenceDate NOT NULL and require
callers to supply a value (change occurrenceDate definition and callers that
insert into that table), or 2) set a deterministic sentinel timestamp for
one-shot triggers at insert time (modify the insert path to populate
occurrenceDate for one-shot events), or 3) alter the unique constraint to use
NULLS NOT DISTINCT (Postgres 15+) so NULL occurrenceDate values are considered
equal; pick one approach and update the table definition and any insert/update
logic that references occurrenceDate and calendarEventId accordingly.

---

Nitpick comments:
In `@apps/web/src/app/api/cron/calendar-triggers/route.ts`:
- Around line 7-8: The current STUCK_TRIGGER_TIMEOUT_MS (10 minutes) can mark
legitimately long-running executeCalendarTrigger executions as failed; either
raise STUCK_TRIGGER_TIMEOUT_MS to a much larger value for AI workloads, or
(preferred) make executeCalendarTrigger resilient by reading the trigger's
latest status before writing its final state and only perform the completion
update if the stored status is still "running" (i.e., add a pre-update check
inside executeCalendarTrigger to skip updating when status !== "running"); apply
the same guard where the cron resets stuck runs (lines referenced around 19-33)
so you don't overwrite a true final result.

In `@apps/web/src/lib/ai/tools/calendar-read-tools.ts`:
- Around line 162-166: The current loop in calendar-read-tools.ts builds a Map
keyed by calendarEventId and blindly overwrites entries, losing other triggers
for recurring events; update the logic in the loop that populates map (the map
variable, the triggers array, and usage of calendarEventId) to select and keep
the most relevant trigger per calendarEventId (e.g., prefer the next pending
occurrence: compare occurrenceDate and status of the existing entry vs the
incoming trigger and only replace when the incoming trigger is a closer upcoming
pending occurrence or otherwise more relevant). Ensure you compare the
trigger.occurrenceDate and trigger.status when deciding to call map.set so the
Map retains the chosen triggerId/status rather than the last-seen row.

In `@apps/web/src/lib/ai/tools/calendar-trigger-tools.ts`:
- Around line 139-147: The current future-check using parsedTriggerAt <= new
Date() is too strict; introduce a small clock-skew/processing tolerance (e.g.,
const FUTURE_TOLERANCE_MS = 30_000) and change the condition to reject only when
parsedTriggerAt.getTime() <= Date.now() + FUTURE_TOLERANCE_MS so near-term times
within the tolerance are treated as valid; update the check around
parseDateTime/parsedTriggerAt/triggerAt accordingly and keep the same error
response when the adjusted check fails.

In `@apps/web/src/lib/ai/tools/calendar-write-tools.ts`:
- Around line 509-519: Wrap the trigger-cancellation and the event soft-delete
in a single database transaction so both succeed or both roll back together:
when detecting event.metadata as CalendarTriggerMetadata (eventMeta) and
cancelling pending triggers via db.update(calendarTriggers).set(...).where(...),
perform that update and the subsequent soft-delete update on the calendar event
inside a single db transaction (use the existing db.transaction/transactional
API) using the same eventId/calendarEventId and status='pending' predicates so
either both operations commit or both are rolled back on error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 13f48142-a161-44b7-a9cf-4b5869ec8f1c

📥 Commits

Reviewing files that changed from the base of the PR and between a8e0050 and f713685.

📒 Files selected for processing (18)
  • apps/web/src/app/api/cron/calendar-triggers/route.ts
  • apps/web/src/lib/ai/core/__tests__/timestamp-utils.test.ts
  • apps/web/src/lib/ai/core/ai-tools.ts
  • apps/web/src/lib/ai/core/timestamp-utils.ts
  • apps/web/src/lib/ai/core/tool-filtering.ts
  • apps/web/src/lib/ai/tools/calendar-read-tools.ts
  • apps/web/src/lib/ai/tools/calendar-trigger-tools.ts
  • apps/web/src/lib/ai/tools/calendar-write-tools.ts
  • apps/web/src/lib/workflows/calendar-trigger-executor.ts
  • apps/web/src/lib/workflows/workflow-executor.ts
  • docker/cron/crontab
  • docs/1.0-overview/changelog.md
  • packages/db/drizzle/0095_mysterious_vision.sql
  • packages/db/drizzle/meta/0094_snapshot.json
  • packages/db/drizzle/meta/0095_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema.ts
  • packages/db/src/schema/calendar-triggers.ts
✅ Files skipped from review due to trivial changes (5)
  • docker/cron/crontab
  • packages/db/src/schema.ts
  • packages/db/drizzle/meta/_journal.json
  • apps/web/src/lib/ai/core/tests/timestamp-utils.test.ts
  • docs/1.0-overview/changelog.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/src/lib/ai/core/ai-tools.ts
  • apps/web/src/lib/workflows/workflow-executor.ts
  • apps/web/src/lib/ai/core/tool-filtering.ts
  • apps/web/src/lib/workflows/calendar-trigger-executor.ts

Comment thread apps/web/src/lib/ai/core/timestamp-utils.ts
Comment thread apps/web/src/lib/ai/core/timestamp-utils.ts
Comment thread packages/db/drizzle/0095_mysterious_vision.sql Outdated
Comment thread packages/db/drizzle/meta/0094_snapshot.json Outdated
Comment thread packages/db/drizzle/meta/0094_snapshot.json Outdated
The CI test checks that pageSpaceTools equals the merged object of all
tool modules. Add the new calendarTriggerTools mock and import so the
assertion matches the updated ai-tools.ts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. parseDateTime: check naive ISO before native Date fallback, add
   two-pass DST resolution for chrono natural language parsing
2. occurrenceDate: make NOT NULL with epoch sentinel for one-shot events
   so the unique constraint actually deduplicates (NULL != NULL in PG)
3. Regenerate migration as 0095_tiresome_iron_man.sql with correct
   snapshot (no stale users.password column)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…apshot

The previous migration only had ALTER TABLE statements because the
snapshot already contained the table from a prior generation cycle.
After rebase, the CREATE TABLE was lost. Regenerated from master's
0094 snapshot so Drizzle produces the full CREATE TABLE + indexes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 18 unit tests for calendar-trigger-tools (schedule validation,
  cancel race conditions, cross-drive access denial, error leakage)
- Add 11 unit tests for calendar-trigger-executor (rate limits, access
  rechecking, prompt building, workflow delegation)
- Add 7 unit tests for cron calendar-triggers route (auth, claiming,
  trashed events, error handling)
- Wrap tool throw statements with generic messages to prevent internal
  DB error details from leaking to AI/user (details still logged server-side)
- Document claimed enum status as reserved for future recurring triggers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s test

Removes mockSelectFrom and mockSelectWhere that were declared but never
referenced, fixing ESLint no-unused-vars errors in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/lib/ai/tools/calendar-trigger-tools.ts`:
- Around line 207-214: The post-commit broadcast via broadcastCalendarEvent is
currently awaited and will throw if it fails, causing callers to potentially
retry non-idempotent scheduling; make this broadcast best-effort by wrapping the
broadcastCalendarEvent call in a try/catch (referencing broadcastCalendarEvent
and the payload using event.id, driveId, userId, operation:'created') and on
error log the failure (with context) but do not rethrow; alternatively you may
dispatch it fire-and-forget (no await) after logging, but ensure errors are not
allowed to bubble up to callers.
- Around line 311-318: The broadcastCalendarEvent call after the cancellation
commit should not turn a successful cancel into a reported failure; change the
code around broadcastCalendarEvent (the call that uses
cancelled.calendarEventId, cancelled.driveId, operation 'deleted') to run
asynchronously without bubbling errors — e.g. await it inside a try/catch and
log any websocket/broadcast errors (do not rethrow), or fire-and-forget the
broadcast with .catch(...) to swallow/log errors; ensure the cancellation flow
(the transaction that produced cancelled) still returns success even if
broadcast fails.
- Around line 117-135: The code currently queries pages with
inArray(contextPageIds) but never rejects IDs that don't exist, allowing
nonexistent contextPageIds to be stored; after fetching ctxPages (the result of
db.select(...).where(inArray(pages.id, contextPageIds))), compare the set of
returned ids (ctxPages.map(p => p.id)) to the original contextPageIds and if any
ids are missing return { success: false, error: `Context page(s) not found:
${missingIds.join(', ')}` }; keep the existing per-page checks (cp.isTrashed,
cp.driveId, isUserDriveMember) but perform this missing-ID validation
immediately after obtaining ctxPages to reject unknown IDs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 67b7d0a4-08f3-40cc-9bbd-b1bb152bbfec

📥 Commits

Reviewing files that changed from the base of the PR and between f713685 and 483211d.

📒 Files selected for processing (10)
  • apps/web/src/app/api/cron/calendar-triggers/__tests__/route.test.ts
  • apps/web/src/lib/ai/core/__tests__/ai-tools.test.ts
  • apps/web/src/lib/ai/core/timestamp-utils.ts
  • apps/web/src/lib/ai/tools/__tests__/calendar-trigger-tools.test.ts
  • apps/web/src/lib/ai/tools/calendar-trigger-tools.ts
  • apps/web/src/lib/workflows/__tests__/calendar-trigger-executor.test.ts
  • packages/db/drizzle/0095_blushing_firelord.sql
  • packages/db/drizzle/meta/0095_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema/calendar-triggers.ts
✅ Files skipped from review due to trivial changes (2)
  • apps/web/src/lib/ai/tools/tests/calendar-trigger-tools.test.ts
  • packages/db/drizzle/meta/_journal.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/lib/ai/core/timestamp-utils.ts

Comment thread apps/web/src/lib/ai/tools/calendar-trigger-tools.ts Outdated
Comment thread apps/web/src/lib/ai/tools/calendar-trigger-tools.ts Outdated
Comment thread apps/web/src/lib/ai/tools/calendar-trigger-tools.ts Outdated
CodeRabbit round-3 fixes:
- Reject unknown contextPageIds (IDs not found in DB)
- Make schedule and cancel broadcasts best-effort (try/catch, log errors)

Test TypeScript fixes:
- Replace banned `as Function`/`as any` casts with @ts-expect-error and
  try/catch patterns that satisfy strict ESLint rules
- Regenerated migration 0095 as full CREATE TABLE from master's snapshot

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI's full typecheck generates proper types, making the directive
unnecessary and causing TS2578.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scheduling is a property of a calendar event, not a separate action.
Remove standalone schedule_agent_work and cancel_scheduled_work tools
(38 tools now, down from 40). Add optional `agentTrigger` param to
create_calendar_event instead.

Cancel = delete_calendar_event (already wired to cancel triggers).
Reschedule = update_calendar_event (already wired to sync triggerAt).

All backend infrastructure unchanged (calendarTriggers table, cron
poller, executor, migration).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2witstudios and others added 2 commits April 9, 2026 11:38
…x changelog

Address review findings: add 11 tests for agentTrigger creation validation,
trigger sync on update, trigger cancel on delete, and execution-time drive
access. Fix changelog to reference actual shipped API (agentTrigger param,
not removed schedule_agent_work tool).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ss fixes

- Reject recurrence + agentTrigger combination (one trigger row can't
  serve multiple occurrences; recurring support is a future feature)
- Restrict context pages to same drive (cross-drive pages are silently
  dropped by executeWorkflow, so reject at schedule time)
- Cancel running/claimed triggers on event delete (not just pending),
  closing the race between cron claim and user deletion
- Add agent page preflight before consuming usage credit, so deleted
  agents don't waste the scheduler's daily AI call budget

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merge origin/master — both the calendar-trigger cron (this branch)
and the orphaned-file-cleanup cron (#863) are kept.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
Preview — fe0ee63c Deployed Apr 9, 2026 by vercel[bot]
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.

1 participant