description, kind
| description | kind |
|---|---|
| Host-wide durable reminders and shared Session-bound task management. | package-reference |
@deepseek-ai/dsh-schedule
English | 中文
Summary
Schedule delivers one-shot, fixed-rate, daily, weekly, and cron wall-clock reminders as follow-up messages in their original Session. Tasks remain available after Host restart, and each recurring task contributes only its latest missed occurrence. The Host restores a cold Session when delivery is due. Active and inactive tasks remain inspectable until explicit deletion, and deletion removes the task row together with its saved delivery records.
Table of Contents
- Use this package
- Understand the implementation
- Further Exploration
- Model Experience
- Known Limitations and Deferred Work
- Dev Note
Use this package
The shipped Web composition mounts the service alongside storage-domain and the Session controller. Its Config states deliveryHistoryDays (default 30) and deliveryHistoryRecords (default 200). Storage backend routing belongs to storage-domain; Session model and preset restoration belong to the Session controller. Delivery asks schedule/resolve-agent to activate the original Session through its owning controller; without a handler it uses the Web Session controller. The SDK server supplies its own handler and does not mount the Web controller. Web restoration is registered inside ctx.inject(['sessionController']); storage and management do not wait for the Web controller. When the controller becomes ready or remounts, overdue tasks are reconsidered. The SDK composition waits for its own activation handler before mounting Schedule. A Session persistence backend is required because a delivery commits only after the Session acknowledges session/flush.
The reminder tools schedule_create, schedule_list, schedule_delete, and schedule_update come from @deepseek-ai/dsh-tool-schedule, which Web presets and the SDK bundle mount; this package contributes the Host service those tools call. Update replaces the name, instruction, or timing of one reminder in place and keeps its id and saved records; it is not offered for the relative after delay. Creation requires a non-empty prompt, a title, and exactly one of six selectors:
| Selector | Example | Timing |
|---|---|---|
after_seconds |
{"prompt":"Check the build","title":"Build check","after_seconds":600} |
Positive safe-integer delay. |
at |
{"prompt":"Review the release","title":"Release review","at":"2099-01-01T09:00:00+08:00"} |
Strictly future absolute instant; a local date/time object with an explicit zone is also accepted. |
every_seconds |
{"prompt":"Check the queue","title":"Queue check","every_seconds":300} |
Fixed safe-integer interval of at least 60 seconds, initially aligned to creation. |
daily |
{"prompt":"Review today's tasks","title":"Daily review","daily":{"time":"23:00:00","time_zone":"Asia/Shanghai"}} |
Local wall-clock time in an explicit IANA zone. |
weekly |
{"prompt":"Review the week","title":"Weekly review","weekly":{"time":"09:00:00","time_zone":"Asia/Shanghai","weekdays":[1,3]}} |
Local wall-clock time on an explicit ISO weekday set in an explicit IANA zone. |
cron |
{"prompt":"Check the deploy","title":"Deploy check","cron":{"expression":"*/15 9-17 * * 1-5","time_zone":"Asia/Shanghai"}} |
Five-field Vixie cron expression evaluated in an explicit IANA zone. |
Every creation must supply title, which names the task in the model views, the task list, the detail heading, and the reminder catalog. The title is trimmed and must remain non-empty and at most 120 characters; a missing, blank, or over-long title returns invalid_prompt. Creation never derives a title from the instruction. Decoding requires the stored title too: a record whose title is missing, blank after trimming, untrimmed, or over-long is rejected, so records written before titles existed are not read.
Daily input accepts HH:mm:ss with optional one-to-three fractional digits. It stores normalized time and timeZone alongside the next UTC scheduledAt. The first target is strictly future; missing local times or dates are skipped, and overlaps use only the earlier instant once per date. every_seconds: 86400 is a fixed interval, not a substitute for daily wall-clock timing. See daily timing for catch-up and time-zone-data limits.
Weekly input adds weekdays, a non-empty set of ISO weekday numbers from Monday 1 through Sunday 7. The stored record normalizes the set to unique ascending numbers, so duplicate, out-of-range, and non-integer entries are rejected. The first target is the first strictly future instant whose local date in that zone carries one of the selected weekdays; the same gap skip and earlier-overlap rules as Daily apply per date.
Cron input carries expression and time_zone. The expression is the standard five-field Vixie form minute hour day-of-month month day-of-week: minute 0-59, hour 0-23, day-of-month 1-31, month 1-12, and day-of-week 0-7 where both 0 and 7 mean Sunday. Every field accepts *, a single value, an a-b range, a */n or a-b/n step with n >= 1, and a comma-separated list of those forms. L, W, #, JAN/MON names, @daily-style macros, six-field expressions, out-of-range values, inverted ranges, zero steps, and empty fields are rejected with invalid_rule and a message naming the offending field. Because the dialect has five fields, the smallest interval is one minute. Creation stores a canonical expression: repeated values collapse, adjacent values and ranges merge, a uniform step is written as a-b/n or, for a field that started with *, as a star-step (* for every value, otherwise the widest star-step walk plus any remaining values), a step of 1 is dropped, and Sunday is written as 0. A field that did not start with * never becomes a star-step, so the day rule below survives storage. The durable decoder rejects a non-canonical stored expression, so the record always holds the canonical text. When either day-of-month or day-of-week is a star, a local date matches only when both fields match; when neither is a star, either field matching is enough. A field is a star when its text starts with *, independently of the values it matches, so a stepped star constrains alongside the other field instead of substituting for it. The first target is the first strictly future instant whose local date and time in that zone match, using the same gap skip and earlier-overlap rules as Daily. See cron timing for the full dialect and canonicalization rules.
A reminder is bound to the calling Agent's Session. The shared schedule Remote namespace exposes catalog for active and inactive Host tasks with their original Session ids, and list, history, update, and delete with an explicit Session id. It also exposes deleteInactive, which removes every inactive task across Sessions and answers how many rows it removed; an inactive task never arms a timer, so that removal recomputes no target, and the batch is one durable write, so a normal failure removes nothing and rejects instead of reporting a count; a backend unknown-state failure may have removed rows and quarantines the Host schedule domain, so later schedule operations reject until it is reopened, and any rejection announces schedule/changed so clients re-read. Remote list and model schedule_list return only active tasks. Catalog entries retain only the latest receipt in lastDelivery; catalog and list responses omit saved delivery history. None of these operations activates an Agent or reads Session logs. Explicit deletion stops future delivery, leaves the original Session and already queued messages intact, and removes the stored task row together with its saved delivery records: the task leaves list and catalog, never schedules again, and history answers schedule_not_found for the same (sessionId, id).
Archiving a Session with active reminders is refused until they stop, and choosing to stop them deletes every active reminder; unarchiving does not bring them back.
history({sessionId, id, limit, before?}) reads saved deliveries for one stored task. Callers must provide an integer limit from 1 through 100; an invalid limit rejects with invalid_rule. Records return newest-first in append order, even when wall time moves backward. The optional before message-id cursor is exclusive; nextBefore is the oldest returned message id and appears only when more saved records remain. A missing task or wrong Session binding returns schedule_not_found; an unknown cursor returns delivery_cursor_not_found. A successful empty page is distinct from either failure.
update(ScheduleUpdateRequest) edits an active task's name, instruction, and timing using its original sessionId and id, the complete expected: ScheduleRecord captured before editing, and any combination of optional title, optional prompt, and an optional discriminated change (at, every, daily, weekly, or cron). A supplied title must be non-empty after trimming and at most 120 characters; a supplied prompt must be non-empty after trimming. An omitted field keeps its stored value: a supplied name or instruction, or no change, keeps the stored rule kind and committed target, while a timing change re-anchors. The change kind may differ from the stored record's kind; every combination is accepted, and the new rule computes its target exactly as creation would from the accepted-save time. Daily, weekly, and cron time or zone changes select the first future target under the same DST skip/earlier-overlap rules; a weekly change also carries the complete weekday set, and a cron change carries its complete expression. A changed Every interval must be a safe integer of at least 60 seconds; its new first target is the Host's accepted-save time plus that interval. An absolute at target must be strictly future and stores kind: "at" with the same id. One-shot records store only the UTC instant, not the input zone. An equivalent normalized rule within the same kind is a no-op: it performs no write or target reset, an unchanged one-shot keeps its stored after/at spelling, a cron change compares canonical expressions and zones, and the same Every interval does not reanchor.
Every update is a complete-record compare-and-set inside the same FIFO as creation and deletion, and it preserves the task id, original Session binding, status, latest receipt, and complete saved history. Missing tasks or wrong bindings return schedule_not_found; inactive tasks return schedule_ended. If delivery, target advancement, or another edit changed the expected record, the update returns schedule_conflict without overwriting it. Name, instruction, and timing validation errors retain their specific codes; storage failures reject rather than report persisted success. Refresh the catalog and capture a new expected record before retrying a conflict. See the task page for the staged form's Save and Cancel behavior.
Understand the implementation
Storage, dispatch, and ownership
ScheduleService owns one version-1 schedule domain with globally unique task ids and one Host timer. Every task stores its Session binding, record, and active or inactive status together; only active tasks drive the timer. Stored records without a status normalize to active, without scanning or recreating historical Sessions. Management and dispatch writes share one FIFO. Updates compare the complete expected record and sample Date.now() inside that queue; one task put changes the rule, name, or instruction without replacing the binding or delivery history. Creation, deletion, and update recheck supplied cancellation after queueing, before persistence; once a write begins, cancellation does not roll it back. The timer rechecks the wall clock, including due members after Session restoration if the clock rolls backward, and segments delays beyond the platform timer limit. Due Every, Daily, Weekly, and Cron tasks for the same Session share one message; each task advances independently after the delivery decision. Successfully advanced tasks remain eligible for the next Host timer even if another batch member fails to persist.
Delivery resolves the original Session through schedule/resolve-agent; the Web fallback calls sessionController.resolveAgent only while that controller is injected. A plugin-sourced followup() synchronously appends the message to the Session inbox; successful Session flush acknowledges durable delivery. One task-row put then updates lastDelivery, appends the actual receipt and sent prompt snapshot to deliveryHistory.records, and stores the one-shot's inactive status or the recurring task's next target. Receipts contain the occurrence's scheduledAt, acknowledgment time deliveredAt, and messageId; they acknowledge inbox delivery, not model execution. Failed flush or task put publishes no new saved record. Session persistence and the task put are separate durable writes; a crash or task-write failure after Session flush can leave a delivered message unrecorded and deliver the same reminder again.
The optional version-1 deliveryHistory stays in the task row so its oldest-first records and earlierRecordsUnavailable flag share the status and target commit. New tasks start with empty records and a false flag. Reading a task without history exposes only its existing lastDelivery, if any, with no prompt snapshot and a true flag; it neither rewrites the task nor reconstructs missing deliveries or prompts from Session logs or the current prompt. Future appends preserve that true flag and retain the existing receipt. Stored history rejects duplicate message ids or a latest receipt that differs from lastDelivery.
The optional earlierRecordsPruned flag records confirmed removal by an append, remains true after later deliveries and restarts, and is never inferred from earlierRecordsUnavailable. Missing flags in existing rows mean pruning is unconfirmed. History responses expose this distinction and the current Host retention limits; reads do not prune records.
schedule/changed notifies clients after committed task changes. Timing updates request timer recomputation only after the task put commits. Tool and browser consumers use the same service; list and delete read storage directly. Recomputations keep at most one pending timer, including changes during delivery. Shutdown cancels it and drains accepted work before closing the domain. Dispatch admission failures are logged without automatic retry and do not invalidate an already-persisted management result. Storage validation and cleanup registration failures still reject initialization. The activation provider owns Agents it restores.
The Schedule domain declares the whole-unit layout because tasks are authoritative. When routed to the JSON backend, an unreadable file, malformed document, unsupported version, or invalid task rejects startup instead of publishing a partial task catalog. Failed recovery leaves schedule.json unchanged, while an initially absent file opens as an empty domain. Repairing the file permits reopening the same task identities. Registered startup awaits storage validation and runtime setup through Service.init; unloading during open releases the acquired domain without starting dispatch.
Historical schedule/change events retain LegacyScheduleRecord (after, at, and every) in their decoder and fold. The Host record decoder separately accepts daily, weekly, and cron; it preserves committed UTC targets and valid stored zone aliases across canonical-name changes. The Host record decoder requires a stored title: a task record whose title is missing, blank after trimming, untrimmed, or longer than 120 characters fails to decode with ScheduleLogError. The historical change decoder tolerates an absent title so an already-written Session log stays readable, and it applies the same validation when the member is present. The task schema declares no backup-and-skip policy, so one such stored task rejects the whole domain open instead of being dropped. Historical events do not populate the Host task table. Loading a Session with active historical reminders logs a warning to recreate them with schedule_create; the Host does not scan historical Sessions, migrate tasks implicitly, or convert existing at tasks into daily, weekly, or cron rules.
The schedule.archiveAdmission() effect answers the Workspace registry's archive admission (seam) for every Session. Host tasks outlive their Session's Agent, so admission reads the stored rows rather than a live runtime or a Session-log fold: workspace/session-activity reports that Session's active Host tasks as the schedule family, one item per task with its stored id and title as label, and prepends that family to next() so other families keep their entries; workspace/session-stop removes those rows in one slot of the same serialized queue the tools use, so the stop is ordered behind a create whose write is still in flight; it deletes the rows directly, because re-entering the public delete from inside that queue would deadlock. A Session with no active Host task reports nothing and has nothing to stop.
Further Exploration
- Schedule domain helpers define selectors, recurrence arithmetic, and reminder framing.
- Timing updates define expected-record comparison and no-op normalization.
- Storage declaration defines durable task validation.
- Host runtime owns timer and enqueue ordering.
- Schedule subsystem describes composition and consumers.
Model Experience
Tool schemas in mounted presets
What the model sees
The generated tool catalog contains the descriptions and schemas for schedule_create, schedule_list, schedule_delete, and schedule_update, which @deepseek-ai/dsh-tool-schedule contributes to the presets that mount it.
Token effect
The four schemas contribute fixed request-context tokens while available. Stored tasks and browser catalog queries add no schema tokens.
KV Cache effect
Unchanged schemas preserve their repeated prefix. Loading, unloading, or changing the tool definitions can change request-prefix tokens; provider cache availability remains outside this package.
Tool results after management calls
What the model sees
Tools render their values as JSON text. Create and update return one reminder view; list returns an array of active reminder views. Update answers with the committed view, or a non-mutating schedule_not_found, schedule_ended, or schedule_conflict miss. Each view contains id, kind, title, prompt, scheduledAt, state, and deliveryMode: "host", plus afterSeconds, everySeconds, or the wall-clock rule's normalized time, stored timeZone, and — for weekly — ascending weekdays or — for cron — the canonical expression, for the corresponding kind. Delete returns id and deleted, with code: "schedule_not_found" when absent. Failures return code and message; internal failures use "The schedule operation failed.".
Token effect
Result tokens depend on reminder content, list length, or the returned deletion and error fields. Browser-only management does not append tool results.
KV Cache effect
Tool results append to conversation history. Creating, listing, or deleting tasks does not rewrite earlier model-visible messages.
Due reminders in the original Session
What the model sees
Due reminders enter as user-role messages with producer kind schedule. One-shot messages append schedule_id_json, occurrence_at, and reminder_prompt_json after the fixed text below; the id and prompt are JSON-encoded. Recurring batches append reminders_json, an array containing schedule_id, occurrence_at, and reminder_prompt for each latest due occurrence. The fixed text names the origin of the message; schedule_create still describes prompt to the model as content to present when the target becomes due, and schedule_update as new reminder content.
One-shot framing
[SCHEDULE REMINDER]
This is a scheduled message from the user
Recurring batch framing
[SCHEDULE REMINDER BATCH]
This is a scheduled message from the user
Token effect
Each delivery adds fixed framing and content-dependent payload tokens. A recurring batch includes only the latest missed occurrence per task. Storage records and timer checks do not issue model requests.
KV Cache effect
Reminder messages append to the original Session's history and preserve earlier message content; they do not replace the existing request prefix.
Known Limitations and Deferred Work
- The Host must be running to deliver reminders. Failed restoration, enqueue, or persistence leaves tasks stored and reports a warning; there is no automatic retry timer. A later task-management change, another scheduled wake, or Host restart can retry pending tasks.
- Enqueue and task writes are not atomic, so crash recovery does not guarantee exactly-once delivery. A shutdown can repeat a delivery for the same reason: sibling fibers dispose concurrently, so the storage facility can close before the delivery drain writes its acknowledgment.
- Deletion removes the task row together with its saved delivery records: future delivery stops, the task leaves
listandcatalog, andhistoryno longer resolves it. - Old Session-log reminders require explicit recreation. Previously physically deleted tasks are not restored or fabricated.
- Management edits are limited to active tasks. Pause, execution status, delivery outside the original Session, and a new Session for each run are not supported. Name, instruction, and timing updates are available to the model through
schedule_updatefor its own Session, and to the Web detail for the selected task; a cross-Session relay workflow is not supported, product permission policy remains undecided, and the Session-binding check is not caller authorization. createandupdaterefuse a Session a delegated child owns withsubagent_sessionwhen the Agent's delegation depth is above zero — the accounting the delegation cap itself enforces, read from the persisted session header so it survives a cold resume. Delivery resolves the bound Session through the Session controller, which rejects the live Agent of a delegated child, so a task bound there could never be delivered. Restricting the caller belongs to the tool layer:@deepseek-ai/dsh-tool-schedulerefuses all four of its tools for a delegated caller.list,catalog,history, anddeletekeep serving that Session, so a task stored before this rule stays removable.- Cron uses the five-field Vixie dialect, so the smallest interval is one minute and sub-minute scheduling is unsupported. Secondary expressions are not accepted:
L,W,#, month or weekday names,@daily-style macros, and a seconds field are rejected. The stored record keeps only the canonical expression, so the exact spelling supplied at creation is not retained. - Daily, weekly, and cron future targets use the Host's current IANA data; decoding and restarting never recompute an already committed target. Only UTC target years 0001–9999 are supported; exhaustion retains the task as inactive after delivery.
- Saved delivery records are pruned on append to the configured
deliveryHistoryDayswindow, measured back from each receipt'sdeliveredAt, and to thedeliveryHistoryRecordscap; the appended latest receipt always survives, and a pruned window marks the task's earlier records unavailable. The JSON backend stores the Schedule domain in oneschedule.jsondocument, so every task mutation rewrites all retained tasks and their histories, and the Host loads all retained history into memory. History pagination bounds returned record count, not storage growth, retained memory, prompt bytes, or write cost. - Tasks without saved history expose only their existing latest receipt until new deliveries append records. Unsaved earlier deliveries and prompt snapshots cannot be recovered; saved records do not establish model execution results.
Dev Note
Working context for maintainers — click to expand
None.