Bundle
@lijian-ui/dsh-schedule-view
Cron-based scheduled task plugin for DeepSeek Harness (dsh): pure UI management, zero LLM tools, cross-session timer, multi-level notifications.
- Source
- lijian-ui
- stars
- 1 stars
- License
- MIT
- Updated
- Updated yesterday
Readme
# dsh-schedule-view · Scheduled Task Plugin
**English** | [中文](./README.zh-CN.md)
> 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
```bash
dsh plugin add @lijian-ui/dsh-schedule-view
```
### Local Development
```bash
# 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 **Settings** → **Scheduled 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
| 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`'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
- [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/dsh)
- [Design Document](./design.md)Install
dsh plugin --profile web add github:lijian-ui/dsh-schedule-view
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install lijian-ui-dsh-schedule-view from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.