dsh-schedule-view
A cron-based scheduled task plugin for DeepSeek Harness (dsh) desktop: create / edit / delete / fire-now tasks from the settings panel, with cross-session agent follow-up and multi-level notifications. Zero LLM tools — purely human-driven scheduling.
- Stars
- 0
- Language
- JavaScript
- Created
- Aug 24, 2026
- Updated
- Aug 26, 2026
Introduction
dsh-schedule-view · Scheduled Task Plugin
English | 中文
A cron-based scheduled task plugin for DeepSeek Harness (dsh) desktop: create / edit / delete / fire-now tasks from the settings panel, with cross-session agent follow-up and multi-level notifications. Zero LLM tools — purely human-driven scheduling.
Features
| Feature | Description |
|---|---|
| 7 Schedule Types | Interval / Daily / Weekly / Monthly / Yearly / Once / Cron |
| Cron Expression | Full 5-field cron with live validation and human-readable preview ("every weekday at 09:00") |
| Cross-Session Fire | Timer lives at host process level; fires even when the target session is closed |
| Agent Follow-up | Injects a user-role prompt (请根据系统指令开始执行任务。) into the target agent via followup |
| Execution Lifecycle | Tracks delivered → running → completed/failed via session/event with AI reply excerpt |
| Multi-Level Notifications | Page toast (8s auto-dismiss) + WebAudio chime (zero files) + Electron desktop notification + unread badge |
| Model Selection | Per-task provider / model override, falls back to deployment default |
| Working Directory | Per-task cwd binding for context-aware execution |
| Catch-Up Policy | Skip missed windows or run once on next tick (for app restarts / sleep) |
| Zero LLM Tools | No schema overhead — purely UI-driven, no schedule_* tools registered |
Background
dsh's official @deepseek-ai/dsh-schedule plugin has several limitations:
| Limitation | Official | This Plugin |
|---|---|---|
| Schedule granularity | after_seconds / at / every_seconds | Full cron + 7 types |
| Session scope | Session-local only | Cross-session (host-level timer) |
| Notifications | In-chat only | Toast + chime + desktop + badge |
| UI management | None (pure LLM tool) | Full settings panel |
| LLM schema cost | 3 tools (schedule_create/list/delete) | Zero tools |
Installation
Prerequisites
- DeepSeek Harness (dsh) desktop
- Node.js >= 18
Install
dsh plugin add @lijian-ui/dsh-schedule-view
Local Development
# Enter the plugin directory
cd extensions/dsh-schedule-view
# Install dependencies
npm install
# Build
npm run build
# Watch mode
npm run watch
# Type check
npm run typecheck
Build output goes to lib/ and is automatically synced to node_modules/@lijian-ui/dsh-schedule-view via junction. Restart the desktop app after each build to load the new bundle.
Usage
- Open dsh desktop
- Navigate to Settings → Scheduled Tasks
- In the task list:
- Click New Task to create a scheduled task
- Toggle the switch to enable/disable
- Click Edit to modify schedule, prompt, or model
- Click Fire Now to trigger immediately
- Click Delete to permanently remove
- Click Run History to view execution records
Task Fields
| Field | Required | Description |
|---|---|---|
| Title | Yes | Task name |
| Schedule Rule | Yes | 7 types: interval / daily / weekly / monthly / yearly / once / cron |
| Prompt | Yes | Instruction injected into the target agent when fired |
| Target Session | Yes | The session where the agent will receive the prompt |
| Working Directory | No | Bound cwd for the task's agent (absolute path) |
| Model | No | Provider/model override; falls back to deployment default |
| Timezone | No | Local timezone for cron resolution (default: system tz) |
Schedule Types
| Type | Example | Description |
|---|---|---|
| Interval | Every 60 minutes | Fixed interval in minutes |
| Daily | 09:00 every day | Wall-clock time |
| Weekly | Mon, Wed, Fri at 09:00 | Selected weekdays |
| Monthly | 1st day of month at 09:00 | Day of month (1-31 or last day) |
| Yearly | January 1 at 09:00 | Month + day |
| Once | 2026-09-01T09:00:00 | One-shot, auto-disables after fire |
| Cron | 0 9 * * 1-5 | Full 5-field cron expression |
Catch-Up Policy
When the app was closed or asleep and missed a scheduled window:
| Policy | Behavior |
|---|---|
| Skip | Drop the missed window, wait for next scheduled time |
| Once | Run once on the next tick to catch up (default for once-type tasks) |
Execution Lifecycle
When a task fires:
delivered → running → completed/failed
| Stage | Trigger | What happens |
|---|---|---|
| Delivered | agent.followup succeeds | History record created, desktop notification sent |
| Running | user/message event matches injected messageId | AI reply excerpt captured, duration tracked |
| Completed | turn/end event | Final status, endReason, toast + chime notification |
| Failed | turn/end with error | Status marked failed, persistent toast, error chime |
| Skipped | Agent not live at fire time | No followup, skipped notification, history marked skipped |
Technical Architecture
Directory Structure
extensions/dsh-schedule-view/
├── src/
│ ├── index.ts # Host entry (install settings section + start timer)
│ ├── remote.ts # Host RPC methods (list/create/update/delete/fireNow)
│ ├── timer-runtime.ts # Core timer engine (cron parsing + tick polling + fire)
│ ├── schedule-core.ts # Schedule computation (next-fire calculation)
│ ├── lifecycle-tracker.ts # Session/event listener for run lifecycle
│ ├── notify.ts # Multi-level notifications (toast + chime + desktop)
│ ├── guarded.ts # Fault isolation wrapper (try-catch for all callbacks)
│ ├── types.ts # TimerTask, RunRecord, TaskSchedule types
│ ├── schema.ts # Config schema (schemastery)
│ └── client/
│ ├── index.ts # Client entry (settings section registration)
│ ├── TimerSettingsSection.tsx # Main settings UI (list + form + history)
│ ├── client-i18n.ts # i18n (zh/en)
│ ├── config-api.ts # Client-side RPC wrapper
│ ├── model-catalog.ts # Model selection UI
│ └── chime.ts # WebAudio chime synthesis
├── lib/ # Build output
├── cordis.patch.yml # Bundle patch declaration
├── package.json
└── tsdown.config.ts
Host Side (src/)
| Module | Responsibility |
|---|---|
index.ts | Plugin bootstrap: install settings section, start timer, sync on config change |
remote.ts | RPC API: list / create / update / delete / fireNow / runs |
timer-runtime.ts | Cron parsing, tick polling, agent followup injection, lifecycle tracking |
lifecycle-tracker.ts | Listens to session/event, matches injected messageId, updates run status |
notify.ts | Toast (React portal) + chime (WebAudio) + desktop notification (Electron IPC) |
guarded.ts | Wraps all callbacks in try-catch; plugin failure never crashes the host |
schema.ts | Config validation via schemastery |
Client Side (src/client/)
| Module | Responsibility |
|---|---|
index.ts | Registers settings section via ctx.slots.inject |
TimerSettingsSection.tsx | React component: task list, create/edit form, run history panel |
client-i18n.ts | Chinese / English translations |
config-api.ts | RPC client wrapper for host methods |
model-catalog.ts | Model selection dropdown UI |
chime.ts | WebAudio dual-tone chime synthesis (zero audio files) |
Persistence
| Data | Storage | Notes |
|---|---|---|
| Task definitions | dsh-settings | UI-editable, revision conflict protection |
| Execution history | dsh-storage-domain | Structured KV, capped at 500 records |
Tick Polling Strategy
Uses setInterval tick polling (default 15s) rather than per-task setTimeout:
- Avoids
setTimeout's2^31 - 1ms (~24.8 days) ceiling - Restart recovery: recalculates
nextFireMapfrom persisted tasks - Missed windows: caught on first tick after restart, handled by catch-up policy
- Trigger precision: bounded by
tickSeconds(acceptable for scheduled tasks)
Known Issues & Solutions
Session ID collision after restart
Issue: After desktop restart, the persisted sessionId in task config may collide with existing agent sessions, causing agents.create to fail with "session already exists".
Root Cause: sessionId was persisted to settings; on restore, the old ID clashes with the agent's own session log on disk.
Our Solution: sessionId is not persisted. On each fire, a fresh session is created via agents.create with a new UUID. The sessionId in config is transient — only used to track the live agent handle during a session's lifetime.
Stale agent handle after agent disposal
Issue: Agent is disposed externally (e.g. user closes session), but the runtime still holds a stale handle.
Our Solution: ensureAgent checks if the handle is stale (disposed or not in agents.list()). If stale, it disposes the handle and rotates the sessionId to create a fresh session.
ENOENT when session log file is deleted
Issue: User deletes the session log file while the agent is still alive; agent can't write logs.
Our Solution: agent/error listener detects ENOENT → disposes agent → rotates sessionId → creates a new session.
Archived sessions
Issue: dsh archives sessions by marking archivedSessionIds but doesn't dispose the agent. agents.get() still returns the agent, and followup executes normally but the user can't see it in the UI.
Our Solution: ensureAgent checks workspaceRegistry.archivedSessionIds. If archived, it disposes the agent and rotates sessionId to create a fresh session.
Model selection without preset mount
Issue: agents.create without a setup callback doesn't mount the standard preset, so the agent has no tools and the prompt assembly can't resolve {{provider}} / {{model}} variables.
Our Solution: All agents.create / agents.resume calls include a setup callback that mounts agentPresets ('standard') and installs model selection before the agent is published.
Internationalization
Supports Chinese and English. Translation files are in src/client/client-i18n.ts. Language follows the dsh desktop language setting.
Tech Stack
- Language: TypeScript
- Build: tsdown (rolldown)
- Frontend: React 18
- Cron Parsing:
cron-parser(~30KB) - Human-Readable Cron:
cronstrue - Config Schema:
@deepseek-ai/schemastery - Settings Persistence:
@deepseek-ai/dsh-settings - Storage:
@deepseek-ai/dsh-storage-domain
License
MIT