Bundle
dsh-nightshift
dsh third-party plugin: queue during peak hours, drain off-peak, report the savings.
- Source
- mikasaxin529
- stars
- 2 stars
- License
- MIT
- Updated
- Updated 4 days ago
Readme
# dsh-nightshift (Night Shift) ๐
English | [็ฎไฝไธญๆ](README.zh.md)
> Queue during peak hours. Drain off-peak. Read the savings in the morning.
A third-party plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh). During the daytime peak windows your tasks wait in a queue; when the off-peak window opens, nightshift dispatches them one at a time into your sessions, retries failures with backoff, auto-continues max-tokens truncations, and books the real token spend against your peak/off-peak prices. When the queue empties (or peak returns) it writes a per-day savings report.
```
daytime (peak) night (off-peak)
โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ
โ ๐ ็ญไฝ่ฐท 2ๅฐๆถ13ๅ โ โโโโถ โ ๐ ๅค่ชไธญ 8ๅฐๆถ40ๅ โ
โ queued: 3 tasks โ โ โถ task 1/3 done โ
โ (frozen, no spend) โ โ โถ task 2/3 running โ
โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ
2026-09-01 report:
12 tasks ยท 8.4M tokens
cost ยฅ8.40 ยท saved ยฅ8.40
```

## Why
DeepSeek's off-peak pricing (roughly UTC+8 night hours โ check the current announcement) is a fraction of the peak price. But agents run when *you* are at the keyboard, which is exactly when compute is most expensive. Nightshift inverts that: write the task now, let the queue hold it until the price drops, wake up to finished work and a report of what the patience bought.
## Features
- **One-chip enqueue** โ a ๐ chip next to the conversation composer hands the current draft to nightshift (default target: continue the current session); the "new session" button runs it in a fresh session instead (directory follows the current workspace and joins its sidebar group, or the dsh default when ungrouped).
- **Peak freeze** โ no dispatch during peak windows; a task already running when peak arrives is left to finish naturally (finished work is never interrupted).
- **Off-peak drain** โ one task at a time, `drainGapMs` between dispatches, cold sessions auto-resumed by the harness.
- **Failure policy** โ `turn/end` reasons are classified: `error`/`interrupted` โ exponential backoff retry (bounded); `max-tokens` โ auto-send the continuation prompt (bounded); `blocked` โ parked for a human; `aborted` โ treated as your cancellation.
- **Savings ledger** โ per-turn token deltas (four buckets) are priced at the window in effect *when they happened*, so cross-window and retried tasks book faithful money.
- **Daily reports** โ generated when the queue empties or peak returns; same-day re-runs merge without double counting.
- **Durable queue** โ a restart re-queues orphaned running tasks (at-least-once); nothing is lost to a reload or crash.
- **Panel** โ sidebar entry with window banner, live task row, queue with run-now/cancel, history, and the latest report.
## Requirements
- dsh with the `web` profile (the host process must stay resident โ it does, independent of the browser tab).
- Node.js โฅ 20.
## Install
**Official channel (recommended)** โ dsh's built-in plugin command does everything in one line (link + auto-added to the profile's bundles; no file edits):
```powershell
dsh plugin --profile web add github:mikasaxin529/dsh-nightshift
```
Swap the profile name for another (`headless`, โฆ); pin a commit with `github:mikasaxin529/dsh-nightshift#<sha>`; uninstall with `dsh plugin --profile web remove dsh-nightshift`. Restart dsh to activate. From a local clone, pass the repo path instead of the `github:` spec.
**Script** โ from this repo's directory (wraps the official channel; falls back to manual mode when `dsh` is not on PATH):
```powershell
.\install.ps1 # installs into the web profile
.\install.ps1 -Profile headless # another profile
```
**Manual fallback** (no `dsh` CLI) โ three steps:
```powershell
.\install.ps1 -Manual -Target "$env:USERPROFILE\.dsh\profiles\node_modules" -Profile "$env:USERPROFILE\.dsh\profiles\web"
```
Or fully by hand:
1. Link or copy this folder into the `node_modules` the profile resolves plugins from, named `dsh-nightshift` (the default dsh layout shares `~\.dsh\profiles\node_modules`).
2. Append to the **target profile's** `cordis.patch.yml`:
```yaml
- insert:
- id: nightshift
name: dsh-nightshift
```
3. Restart dsh (or let the loader hot-apply the patch).
The plugin shares the host's own `@deepseek-ai/*` instances (declared as optional peerDependencies on purpose โ bundling a second copy would break cordis service identity). Do not `npm install` its dependencies into the plugin folder on the host side; devDependencies are only for running the test suite.
## Configuration
All keys live under the `nightshift` entry in your loader config (defaults shown):
| key | default | meaning |
|---|---|---|
| `timeZone` | `Asia/Shanghai` | IANA zone the windows are interpreted in |
| `peakWindows` | `09:00โ12:00, 14:00โ18:00` | `[start, end)` wall-clock windows; may cross midnight; `[]` = never peak |
| `peakPricePerMTok` | `2` | price per million tokens during peak (fill your real number) |
| `offPeakPricePerMTok` | `1` | price per million tokens off-peak |
| `currency` | `ยฅ` | display only |
| `tickMs` | `30000` | window-check / dispatch tick |
| `maxRetries` | `3` | retries for `error`/`interrupted` turns |
| `retryBaseMs` / `retryFactor` / `retryMaxMs` | `60000 / 2 / 1800000` | exponential backoff base, factor, ceiling |
| `continuationLimit` | `3` | auto-ใ็ปง็ปญใsends per task on `max-tokens` |
| `continuationPrompt` | `็ปง็ปญ` | the continuation prompt text |
| `drainGapMs` | `5000` | pause between dispatches |
| `allowRunNow` | `true` | whether the panel's run-now button is honored |
| `reportRetentionDays` | `30` | report pruning |
| `exposeTool` | `false` | reserved for v1.1 (`nightshift_enqueue` tool) |
## How it works
- The **host half** (`index.js`) is a cordis function plugin: one self-rescheduling `setTimeout` tick (never `setInterval`), a `session/event` listener that reacts to `turn/end`, two GET exact-fetch routes (`/api/nightshift/state`, `/api/nightshift/report`) and three POST webServer routes (`enqueue`, `task/cancel`, `task/run-now`) guarded by the connection's request rejection. Queue and reports persist through `ctx.storageDomain` (zod row schemas).
- The **client half** (`client.js`) is a hand-authored `window.__ModuleLoader__.load` factory โ a classic script registering two slots: the composer chip (`conversation.input.dock`) and the sidebar panel (`sidebar.footer.action`). It polls the state route (30s when the panel is open, 2min closed) and never holds UI state that a reload cannot rebuild.
- Display helpers live in `lib/format.js` (shared semantics, re-inlined in the client factory, which cannot reach the host's module graph).
## Differences from adjacent plugins
- vs **sleep-send / scheduled tasks**: those send at a *time you name*; nightshift follows the *price window* โ you configure windows once, not per task, and re-configuring re-freezes/unfreezes the whole queue.
- vs **session-guard / input-traffic**: those shape or gate interactive input; nightshift never touches your live sessions. Freeze means "no dispatch", not "session frozen" โ manual work keeps flowing at any hour.
## Known limitations
- **Cache pricing is simplified**: all four token buckets bill at the same window price (cache reads are overcharged versus DeepSeek's real cache pricing, so the reported savings are conservative on that axis).
- **At-least-once**: a crash mid-task re-runs the task on restart. The task is idempotent or re-runnable at your discretion.
- **One task at a time** โ a deliberate v1 invariant.
- Prices are *your declared numbers*; nightshift has no balance/pricing API to check against.
- Changing config triggers an HMR reload (timers/domain/listeners are disposed cleanly); queued tasks survive in storage.
## Develop
```bash
npm install # devDependencies only (test tooling + type-real imports)
npm test # 117 vitest cases
```
Layout and the full design contract live in [SPEC.md](./SPEC.md).
## License
[MIT](./LICENSE)
Install
dsh plugin --profile web add github:mikasaxin529/dsh-nightshift
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 dsh-nightshift from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.