Skip to main content
PathDocs

Scheduled Dispatch

One-liner: @deepseek-ai/dsh-schedule gives live agents 3 in-session tools (schedule_create / schedule_list / schedule_delete) to manage persistent reminders: the session event log owns reminder state, and the timer and model follow-up are only discardable projections of the log.

The ability to make an agent do something on a schedule. Core insight: reminder state lives not in "an in-memory timer" but in the session event log; so fold/recover reads only the log, and a crashed timer doesn't matter.

1. Three tools

ToolInputBehavior
schedule_createafter_seconds (positive integer) / at (absolute time) / every_seconds (≥5 minutes)create a reminder
schedule_listlist active reminders (incl. scheduled / overdue state)
schedule_deleteiddelete a reminder

schedule_dispatch is not a tool: dispatch is an internal operation in the schedule/change event. On-time delivery is done by the runtime (see below).

2. State model (a key design)

session event log (single source of truth)
├── schedule/change events (create/delete/dispatch union, version 1)
├── timer (discardable projection)
└── model follow-up (discardable projection)

Important properties:

  • session fold = the full log; a fork folds only past seedLength: a child session does not inherit the parent session's reminders
  • before every read, first ctx.sessions.flush(session); persistence unavailable → persistence_uncertain, and never treat un-confirmed state as a result
  • replay rejects: unknown versions, duplicate ids, shape-mismatched dispatches, and transitions on inactive records
  • after each successful preflight, and after a real barrier succeeds, callback onDurableChange drives the owner to recompute

3. Time semantics (strict)

at supports two forms:

FormExample
RFC 3339 string2026-08-12T10:00:00Z or with an offset
local form{ date, time, time_zone }: must be explicit UTC or an IANA time zone

Rejections: missing time zone, offset-less strings, times inside a DST gap (overlap takes the earlier one), and targets that aren't in the future. After creation only the normalized UTC is kept: a Schedule never reads the browser/session/model-context time zone.

Minimum interval: every_seconds can't be smaller than MIN_EVERY_INTERVAL_SECONDS = 300 (5 minutes).

4. Runtime delivery: how a reminder becomes a real turn

An on-time reminder isn't "just a state change" — it's delivered to the agent as one real user message:

one ScheduleRuntime per root agent maintains the loop
→ a timer uses whenIdle() + MAX_TIMER_DELAY_MS(2147483647) to wait for an idle phase
→ on time, uses agent.runMaintenance(...) to claim an idle phase
→ builds a user message; agent.followup(message) injects one "follow-up turn"
→ this is precisely the next-turn input the model actually receives and processes

Tool calls also carry a cancellable transaction (runCancellableScheduleTransaction / exec.signal) to prevent duplicate delivery.

5. Composition requirements

  • load order: after ctx.sessions, ctx.agents, ctx.tools, ctx.sessionPersistence + the flush-persistence listener; a failed static injection = the composition fails outright
  • only listens to future agent/created: agents and subagents that already exist at load time will not get a Schedule
  • schedule/change's durably-commit delivery barrier and the onDurableChange observer drive recomputation after create/delete

6. Verification

# find schedule/change events in the session log (default zstd, two-level directories)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | jq -r 'select(.type == "schedule/change")'

# inspect what turn it gets delivered into
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | grep -E "user/message|schedule/" | tail

Next steps