Skip to content
dsh.fish
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

  • 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.
Source