DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

lion231226 /

lion231226/dsh-peak-hours

Verified

Run-window switch for DeepSeek Harness: run always, or only in off-peak hours - every task pauses at China peak-price windows and resumes at the boundary.

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@4d92cd65

dsh-peak-hours

A run-window switch for DeepSeek Harness (dsh).

It adds a control to the right of the composer's mode buttons that chooses between running always and running only in off-peak hours. In the second mode every dsh task — including one that is already running — is paused when a peak-price window starts and resumed automatically when it ends.

Available in English (this file) and 中文.

中文文档 topic: dsh-plugin License: MIT

Run windows

Off-peak is the half-price window. Peak, in Beijing time (UTC+8), is:

When Phase
Monday–Friday, 09:00–12:00 and 14:00–18:00, excluding Chinese statutory holidays peak
Everything else — 00:00–09:00, 12:00–14:00, 18:00–24:00, all weekend, all statutory holidays idle

Make-up workdays (weekends that are worked to offset a holiday) stay idle: the rule only looks at the weekday and whether the date is a statutory holiday.

Install

From the dsh CLI:

dsh plugin --profile web add github:lion231226/dsh-peak-hours

The plugin is pure JavaScript with no build step and no runtime dependencies, so a source install is enough.

For a desktop profile that is currently running, this repository also ships install.ps1, which copies the package into %DSH_HOME%\profiles\<profile>\node_modules\ and maintains one managed row in the profile's cordis.patch.yml. It validates the patch file with js-yaml before writing (a malformed patch file stops the Loader from reconciling the whole profile), waits for the plugin to answer, and re-activates the entry with a fresh row id if a previous attempt left it failed. No restart is required.

powershell -ExecutionPolicy Bypass -File .\install.ps1
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Uninstall

Using it

The button sits in the composer tool row, immediately to the right of the mode buttons (the permission and plan selectors). It has three states:

State Label Dot
Run always 全天运行 / Run always neutral
Off-peak only, currently idle 空闲时段 · 运行中 / Off-peak · running green
Off-peak only, currently peak 空闲时段 · 已暂停 / Off-peak · paused amber

Clicking it opens a panel with the two modes, the current Beijing time, the next switch and a per-second countdown, today's holiday (when there is one), a warning when the holiday calendar has no data for the year, and a note while a clock override is active.

While a peak window is in force, a banner above the composer card states that submitted tasks will start automatically at the end of the window, with a live countdown.

How the pause works

The host half hooks two waterfalls and only acts in idle-only mode:

  1. agent/pre-step — before every model call. An abort during the wait is re-thrown, which is the control flow the agent loop already expects (signal.throwIfAborted() follows the waterfall).
  2. tools/pre-execute — before every tool body is dispatched, covering a step that runs into a peak window. On abort it returns the runtime's designed { kind: 'cancel' } instead of throwing, because a throwing listener is classified as a scheduler failure and fails the whole step.

The gate releases immediately when the phase is idle, and while a peak window is in force it registers a waiter and arms a single timer at the exact next transition instant — no polling, no busy loop. When the timer fires, the phase is re-evaluated and every waiter is released, so the work continues on its own.

llm/stream is deliberately not gated: it returns an AsyncIterable rather than a promise, and the agent loop has a call path that bypasses it, so the primary gate already covers every model call.

Nothing is lost while waiting: messages and tool calls stay in the session log, no model request is made, and the wait is abortable (pressing stop cancels it).

HTTP API

Registered on the host's loopback web server; no GUI token is required on loopback.

Method Path Body / result
GET /api/peak-hours/health { ok, plugin, version }
GET /api/peak-hours/state the state envelope below
POST /api/peak-hours/mode { "mode": "always" | "idle-only" }
POST /api/peak-hours/clock { "offsetMs": <int|null>, "ttlMs"?: <int> }
{ "ok": true, "state": {
  "mode": "always", "phase": "idle", "reason": "off-hours",
  "now": 1791541815888, "nowText": "2026-10-09 18:30:15",
  "nextTransitionAt": 1791766800000, "nextTransitionText": "2026-10-12 09:00:00", "nextPhase": "peak",
  "msUntilTransition": 224984112, "paused": false, "waitingTasks": 0,
  "holidayToday": null, "holidayCoverage": "known", "unknownHolidayYear": null,
  "timeZone": "Asia/Shanghai (UTC+8)", "peakWindows": ["09:00-12:00", "14:00-18:00"],
  "clockOverride": null, "config": { "allowClockOverride": true } } }

clockOverride shifts the host's notion of "now" so a peak window can be simulated without waiting for one. It always carries a TTL (10 minutes by default) and clears itself: without that, a session that is held by the window it created has nothing left to release it. Set DSH_PEAK_HOURS_ALLOW_CLOCK=0 to disable the endpoint.

Holiday data

The built-in calendar lists every statutory day off in 2025 (28 days) and 2026 (33 days), taken from the State Council holiday notices. Make-up workdays are not part of the set. Each entry keeps its source titles and URLs in HOLIDAY_SOURCE_DETAIL inside lib/core/holidays.js.

The 2027 notice has not been published yet, so that year reports holidayCoverage: "unknown" with unknownHolidayYear: 2027 and treats the date as a non-holiday (a weekday inside a window is peak); the UI surfaces the warning. Adding a year is one data entry.

Callers can overlay their own dates with createCalendar({ extra: { '2027-01-01': '元旦' } }); extra wins and is not restricted by years.

Configuration

Key Default Meaning
allowClockOverride true Whether POST /api/peak-hours/clock is accepted
version from package.json Version reported by /health

The selected mode is persisted in %DSH_HOME%\peak-hours-state.json with serialized atomic writes; a corrupt file falls back to always and logs once. Disposing the plugin releases every listener, timer and route.

Known limits

  • A model call that has already been issued is not interrupted; the task pauses at its next safe boundary.
  • Background processes in a terminal, and background shells started as jobs, are not agent steps and are not gated.
  • A message submitted during a peak window is not dropped — it waits and continues when the window ends.
  • waitingTasks in the state envelope is the host-wide gate count, so it can include tasks from other sessions.

Tests

node --test                          # author suites + the independent verification suite
node --test "test/**/*.test.mjs"     # author suites only
node --test "verify/**/*.test.mjs"   # independent verification only
node tools/smoke-host.mjs            # host half against a fake context, over real HTTP
node tools/show-monday.mjs           # print one day's phase segments

test/ holds the author suites (core schedule, host gate/routes/store, client). verify/ holds an independent suite written against the same contracts: a day-by-day check of the holiday calendar against the official notices, a boundary matrix at one-second resolution, property tests over the transition function, gate semantics under a fake clock, adversarial cases, client bundle checks, and a harness that mounts the plugin under the real cordis runtime. verify/EVIDENCE.md records each command with its raw output.

Node 24 rejects a directory argument for node --test, hence the glob patterns above.

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 4d92cd6598b9

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout