DEV Community

Eric Wong
Eric Wong

Posted on

Giving Your Neovim AI Assistant a Sense of Time: Scheduled Tasks in chat.nvim

AI Is Passive

You tell AI "fix this code," it responds instantly. You say "remind me about the meeting in an hour," it says "sure" — and then nothing happens.

This isn't a bug in one product. It's a common limitation across most mainstream AI assistants: they are passive. They answer when you ask. They stay silent when you don't. They have no concept of "time," no concept of "the future," and no ability to initiate anything on their own. While some open-source or closed-source Agent frameworks already support scheduled tasks, mainstream assistants like Doubao and DeepSeek still lack this capability.

chat.nvim now includes a scheduled task feature, designed to give AI a sense of time — letting it proactively reach out to you at a future moment.

The Core Idea

To break AI's passivity, the key question is: how do we make AI start working at a future moment, as if it just received a user message?

The solution is straightforward, three steps:

  1. Remember a task (when, what)
  2. Trigger at the right time
  3. Inject the message into the session — AI processes it like any user message

Simple in concept, but the devil is in the details. Let's walk through the design decisions for each part.

Time Model: Unified as Absolute Timestamps

Users can describe time in three ways:

  • "In 1 hour" — relative delay
  • "Tomorrow at 9 AM" — absolute point in time
  • "Every day at 10 PM" — recurring cycle

Pick one, they're mutually exclusive.

Regardless of how the user describes it, internally everything converts to a Unix timestamp. "In 1 hour" becomes current_time + 3600. "Every day" becomes every 86400 seconds starting from creation time.

Why? Because the scheduling engine only needs to handle one time model. No need to distinguish between "delay" and "schedule," no need for different logic branches. Unified abstraction eliminates branching.

Trigger Mechanism: No Polling, Use Timers

The most intuitive approach is polling: scan the task list every few seconds to check if anything is due.

That's wasteful. 99 out of 100 tasks are still waiting, yet you scan them every second.

A better approach: one independent timer per task. It wakes up only when due, with zero CPU overhead while waiting. This is event-driven, not poll-driven.

Neovim has libuv built in. uv.new_timer() does exactly this — millisecond precision, non-blocking on the main thread.

Anti-Drift for Recurring Tasks

Recurring tasks have a pitfall: if Neovim restarts midway, how do you calculate the next trigger time after recovery?

The instinctive approach is current_time + interval. But this causes rhythm drift — you set "every day at 10 AM," after a restart it becomes 10:03, after another restart 10:07...

The correct approach: calculate the next trigger based on creation time and execution count.

next_trigger = creation_time + (executed_count + 1) × interval
Enter fullscreen mode Exit fullscreen mode

This way, no matter how many restarts happen, the 7th execution always lands at creation_time + 7 × interval. The rhythm is locked to the moment of creation.

The 24.8-Day Limit

libuv timers have a hard limit: maximum timeout of 2³¹-1 milliseconds, approximately 24.8 days.

What if a user sets a task 30 days out?

The solution is sharding. First set a 24.8-day timer. When it fires, check — the real trigger time hasn't arrived yet, so recalculate the remaining delay and set a new timer. Two relays cover any duration.

The 30-day cap covers reasonable use cases while preventing a buggy task from hanging around forever.

Decoupling Scheduling from Execution

This is the most critical decision in the entire design.

The scheduling engine does exactly one thing: when the time comes, push a message into the queue. It doesn't call the LLM API, doesn't care if AI is online, doesn't care about network status.

Why not just call the API directly when the timer fires? Because once responsibilities are mixed, complexity explodes:

  • What if AI is already processing another request?
  • What if the network is down?
  • What if the session was deleted?
  • What if the user is mid-keystroke?

After decoupling, the scheduling engine's logic is dead simple: fire → push → done. Everything else is handled by the message queue.

Message Queue: Waiting for a Window

After a task fires, the message enters a queue. The queue is responsible for delivering the message to AI at "the right moment."

What's the right moment? When the session is idle.

  • If AI isn't busy, send immediately
  • If AI is processing another request, queue up and check every few seconds — inject once it's free

It's like a colleague messaging you while you're in a meeting — the message doesn't disappear, you just read it after the meeting.

Only after 3 consecutive send failures does the message get discarded, preventing transient glitches from swallowing messages.

Persistence and Recovery

Neovim isn't a persistent process — users can close it at any time. Scheduled tasks must survive restarts.

The approach is straightforward: every time a task is created, cancelled, or fired, write all tasks to a JSON file on disk. On next launch, load it back.

One detail: libuv timers are in-memory objects that can't be serialized. When persisting, the timer handle must be excluded and recreated on recovery.

Recovery has two different strategies:

  • Expired one-time tasks: skip them. The trigger time has passed, they're meaningless now
  • Expired recurring tasks: must be restored. Recurring tasks need to continue executing — they shouldn't break just because of a restart

The Complete Flow

You say: "Remind me about the meeting in 1 hour"
    │
    ▼
AI calls the tool, calculates trigger_at = current_time + 3600
    │
    ▼
Scheduling engine creates the task, writes to disk, arms a 3600s timer
    │
    ··· (1 hour later) ···
    │
    ▼
Timer fires → pushes "Remind me about the meeting" into the session's message queue
    │
    ▼
Queue detects session is idle → sends to AI
    │
    ▼
AI receives the message, responds with a reminder — just as if you said it yourself
Enter fullscreen mode Exit fullscreen mode

How to Use It

Just tell AI in natural language:

Remind me to follow up on the Subei People's Hospital project in 1 hour

AI will automatically calculate the time and create a scheduled task.

Three modes:

  • One-time: delay_seconds=3600 or trigger_at=1717200000
  • Recurring: interval=86400 (daily), optionally repeat_count=7 to limit repeats
  • Management: action="list" to view, action="cancel" to cancel
Feature Details
Persistence Survives Neovim restarts
Max delay 30 days
Min interval 10 seconds
Precision Second-level (Unix timestamp based)
Dependencies Zero external deps — pure Neovim + libuv

Summary

The scheduled task system in chat.nvim proves that giving AI a sense of time doesn't require a complex architecture. The key design principles:

  1. Unify the time model — one abstraction, no branching
  2. Event-driven, not poll-driven — zero overhead when idle
  3. Lock the rhythm — anti-drift for recurring tasks
  4. Decouple scheduling from execution — keep the engine simple
  5. Persist everything — survive restarts gracefully

The result: AI that can proactively reach out when you need it, not just respond when you ask.

Top comments (0)