Back to home@lijian-ui

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
GitHub repo

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

FeatureDescription
7 Schedule TypesInterval / Daily / Weekly / Monthly / Yearly / Once / Cron
Cron ExpressionFull 5-field cron with live validation and human-readable preview ("every weekday at 09:00")
Cross-Session FireTimer lives at host process level; fires even when the target session is closed
Agent Follow-upInjects a user-role prompt (请根据系统指令开始执行任务。) into the target agent via followup
Execution LifecycleTracks delivered → running → completed/failed via session/event with AI reply excerpt
Multi-Level NotificationsPage toast (8s auto-dismiss) + WebAudio chime (zero files) + Electron desktop notification + unread badge
Model SelectionPer-task provider / model override, falls back to deployment default
Working DirectoryPer-task cwd binding for context-aware execution
Catch-Up PolicySkip missed windows or run once on next tick (for app restarts / sleep)
Zero LLM ToolsNo schema overhead — purely UI-driven, no schedule_* tools registered

Background

dsh's official @deepseek-ai/dsh-schedule plugin has several limitations:

LimitationOfficialThis Plugin
Schedule granularityafter_seconds / at / every_secondsFull cron + 7 types
Session scopeSession-local onlyCross-session (host-level timer)
NotificationsIn-chat onlyToast + chime + desktop + badge
UI managementNone (pure LLM tool)Full settings panel
LLM schema cost3 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

  1. Open dsh desktop
  2. Navigate to SettingsScheduled Tasks
  3. 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

FieldRequiredDescription
TitleYesTask name
Schedule RuleYes7 types: interval / daily / weekly / monthly / yearly / once / cron
PromptYesInstruction injected into the target agent when fired
Target SessionYesThe session where the agent will receive the prompt
Working DirectoryNoBound cwd for the task's agent (absolute path)
ModelNoProvider/model override; falls back to deployment default
TimezoneNoLocal timezone for cron resolution (default: system tz)

Schedule Types

TypeExampleDescription
IntervalEvery 60 minutesFixed interval in minutes
Daily09:00 every dayWall-clock time
WeeklyMon, Wed, Fri at 09:00Selected weekdays
Monthly1st day of month at 09:00Day of month (1-31 or last day)
YearlyJanuary 1 at 09:00Month + day
Once2026-09-01T09:00:00One-shot, auto-disables after fire
Cron0 9 * * 1-5Full 5-field cron expression

Catch-Up Policy

When the app was closed or asleep and missed a scheduled window:

PolicyBehavior
SkipDrop the missed window, wait for next scheduled time
OnceRun once on the next tick to catch up (default for once-type tasks)

Execution Lifecycle

When a task fires:

delivered → running → completed/failed
StageTriggerWhat happens
Deliveredagent.followup succeedsHistory record created, desktop notification sent
Runninguser/message event matches injected messageIdAI reply excerpt captured, duration tracked
Completedturn/end eventFinal status, endReason, toast + chime notification
Failedturn/end with errorStatus marked failed, persistent toast, error chime
SkippedAgent not live at fire timeNo 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/)

ModuleResponsibility
index.tsPlugin bootstrap: install settings section, start timer, sync on config change
remote.tsRPC API: list / create / update / delete / fireNow / runs
timer-runtime.tsCron parsing, tick polling, agent followup injection, lifecycle tracking
lifecycle-tracker.tsListens to session/event, matches injected messageId, updates run status
notify.tsToast (React portal) + chime (WebAudio) + desktop notification (Electron IPC)
guarded.tsWraps all callbacks in try-catch; plugin failure never crashes the host
schema.tsConfig validation via schemastery

Client Side (src/client/)

ModuleResponsibility
index.tsRegisters settings section via ctx.slots.inject
TimerSettingsSection.tsxReact component: task list, create/edit form, run history panel
client-i18n.tsChinese / English translations
config-api.tsRPC client wrapper for host methods
model-catalog.tsModel selection dropdown UI
chime.tsWebAudio dual-tone chime synthesis (zero audio files)

Persistence

DataStorageNotes
Task definitionsdsh-settingsUI-editable, revision conflict protection
Execution historydsh-storage-domainStructured KV, capped at 500 records

Tick Polling Strategy

Uses setInterval tick polling (default 15s) rather than per-task setTimeout:

  • Avoids setTimeout's 2^31 - 1 ms (~24.8 days) ceiling
  • Restart recovery: recalculates nextFireMap from 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

Related Links