dsh-status-plugin
woshi-tom/dsh-status-plugin
A dsh status plugin with host-plane and browser halves: harness runtime health (process, listener, API-key presence, memory, uptime, plugin inventory) as JSON plus server-pushed SSE alerts, surfaced as a header badge with an expanded panel in the Web UI
安装
dsh plugin --profile web add github:woshi-tom/dsh-status-pluginREADME
dsh-status-plugin
English | 中文
A status plugin for DeepSeek Harness (dsh). Two planes in one package:
-
Host plane — HTTP endpoints exposing the running harness's runtime health as JSON: process, listener, API-key presence, memory, uptime, and the live plugin inventory.
-
Client plane — a header badge in the web UI (top-right of a conversation session) that shows uptime, opens a detail panel, and raises toasts when the host reports overload or memory-pressure alerts.
-
Package:
dsh-status-plugin -
Runtime: host (ESM) + browser bundle (a CJS factory wrapped for the dsh client-modules
__ModuleLoader__contract), built withtsc+ esbuild tolib/. -
Language: TypeScript.
Install
dsh plugin --profile web add dsh-status-plugin
The CLI reconciles dsh.profile.bundles automatically: because the manifest declares dsh.bundle.patch, the package joins the profile's bundle layer stack. Stop the running process and restart the profile to load it:
dsh web # or: dsh --profile <name>
Verify the plugin joined the composed tree without booting:
dsh --profile web --dump-config
The client manifest (package.json → dsh.client) declares the browser entry; the profile's client-modules scanner picks it up and injects dsh-status-plugin/client.js into the web app automatically — no bundle or overlay configuration needed.
Usage
The plugin registers two exact routes on the profile's web server:
GET /api/status # JSON snapshot on demand
GET /api/status/events # Server-Sent Events stream
GET /api/status
Example response:
{
"ok": true,
"timestamp": "2026-08-14T03:50:00.000Z",
"host": {
"hostname": "host",
"platform": "linux",
"arch": "x64",
"nodeVersion": "v22.23.2",
"pid": 23185,
"cwd": "/root/.dsh",
"uptimeSeconds": 3600,
"loadAvg": [0.1, 0.1, 0.1],
"cpuPercent": 12.4,
"memory": { "rss": 123456, "heapTotal": 654321, "heapUsed": 432100, "external": 12345 },
"systemMemory": { "total": 17179869184, "free": 4294967296, "used": 12884901888 },
"lanAddresses": ["192.168.5.227"]
},
"webServer": {
"host": "0.0.0.0",
"port": 3080,
"url": "http://localhost:3080"
},
"apiKey": {
"configured": true,
"source": "env"
},
"plugins": {
"entries": [
{ "entryId": "llm", "moduleName": "@deepseek-ai/dsh-llm", "enabled": true, "fiberPhase": "active" }
]
}
}
Fields
| Field | Source |
|---|---|
host.* | process + node:os (pid, uptime, process memory, LAN IPv4 addresses); cpuPercent is CPU utilization sampled from os.cpus() deltas and works on every platform; loadAvg is the Unix load average — always [0, 0, 0] on Windows; systemMemory.* is machine-wide memory (os.totalmem() − os.freemem()) |
webServer.* | ctx.webServer (bind host and actual listening port) |
apiKey | DEEPSEEK_API_KEY in process.env, else the working directory .env, ~/.env, or $DSH_HOME/.env (checked in loadLayeredEnv priority order) — presence only, never the value; an empty assignment (DEEPSEEK_API_KEY="") does not count as configured |
plugins.entries | ctx.pluginInventory.list() (live Cordis Loader entry state) |
The API key check reports only whether a key is configured and where it was found; the value itself never leaves the process.
GET /api/status/events (SSE)
The host pushes to open browser streams — the server decides when the page needs new state, so idle pages make zero requests:
snapshot— a full status snapshot, emitted immediately on connect and then everyheartbeatMs(default 30 s). Each snapshot card dissects tocpuPercent,loadAvg,systemMemory, and process-memory fields insidehost.*for alert-driven UIs.alert— emitted when an indicator enters or leaves its alert band. Entering requiresvalue > threshold; an active alert only clears when the value drops belowthreshold × (1 − hysteresis), so a value hovering near the threshold does not flap. Events are emitted on every transition and re-synchronized on connect so a page that opens mid-alert still learns about it:
event: snapshot
data: {"ok":true,"timestamp":"...","host":{...},"plugins":{...}}
event: alert
data: {"active":true,"reason":"cpu","value":0.87,"threshold":0.8}
Default thresholds (configurable via the plugin config in the profile's cordis.yml):
| Config | Default | Meaning |
|---|---|---|
cpuWarning | 0.8 | CPU utilization above which a CPU overload alert fires |
memoryWarning | 0.85 | system memory pressure above which a memory alert fires |
hysteresis | 0.1 | recovery margin: an alert clears only below threshold × (1 − hysteresis) |
heartbeatMs | 30000 | snapshot push interval |
checkIntervalMs | 5000 | alert monitor sampling interval |
authToken | '' | shared secret required on both routes; empty disables auth. See Authentication |
maxSubscribers | 32 | SSE subscriber cap; new connections over the cap fail with an error response |
maxBufferedBytes | 65536 | per-subscriber write-buffer high-water mark; a slow consumer over it is dropped |
Authentication
When authToken is set, both routes require it. The token can travel in either channel:
GET /api/status—Authorization: Bearer <token>header, or?token=<token>.GET /api/status/events—?token=<token>query parameter; a nativeEventSourcecannot set custom headers.
A rejected request answers 401 with { "ok": false, "error": "unauthorized" }. Comparison is constant-time (crypto.timingSafeEqual), so a wrong token does not leak its length. Because the query parameter can appear in logs and history, prefer header auth for GET /api/status and keep the SSE stream on a loopback-only webserver.
The built-in browser badge has no channel to receive the host's token (the client manifest cannot read the host config), so enabling authToken disables the badge's status views; a custom UI can authenticate by sending the header/query token above. Instances that need the bundled UI should leave authToken empty (the default) and rely on the webserver's loopback binding.
The browser side subscribes with a native EventSource (auto-reconnects on drop) and renders:
- a compact badge in the conversation header (status dot + uptime, click to open);
- a detail panel with process/resource/service/plugin sections and the last update time;
- a toast on every alert transition (auto-dismisses after 6 s) plus a pulsing badge while an alert is active;
- a gray badge dot when the stream is disconnected or no snapshot arrived for 90 s — a monitoring widget must say unknown, not healthy, when it loses contact.
Failure behavior
- A collection error inside the handler returns
500with{ "ok": false, "error": "<message>" }— structured, no stack leak, never a hung socket. pluginInventoryis optional: when the service is absent,plugins.entriesis[]rather than an error.- Response handlers and both periodic timers (heartbeat and alert sampler) are exception-isolated: a throwing collection is logged, never propagated as an uncaught exception that could crash the harness the plugin monitors.
- Responses carry
cache-control: no-store(runtime data must not be cached); the SSE stream usestext/event-streamwithx-accel-buffering: no. - SSE streams are bounded: at most
maxSubscribersconcurrent streams; a subscriber whose write buffer exceedsmaxBufferedBytes(or whose socket write stalls) is dropped so a slow consumer cannot pin the process. Plugin teardown ends every open stream.
Requirements
- dsh profile with the web bundle (
@deepseek-ai/dsh-web-app) — providesctx.webServerandctx.pluginInventory. - Node
^22.19 || >=24. - The browser entry renders in the conversation session header (
conversation.session.header.utilitiesslot); it is not shown on the empty/home screen. - The load-average row in the panel is a Unix concept: on Windows it is always
0.00. CPU utilization and memory metrics work on every platform.
Development
pnpm install
pnpm run build # host tsc + client typecheck + esbuild bundle
pnpm test # vitest unit tests
npm pack --dry-run # verify tarball contents (prepack runs the build)
Publish
This package is a dsh bundle: the npm tarball ships cordis.patch.yml and the manifest's dsh.bundle.patch points at it, so installing the package into any dsh profile automatically mounts the plugin layer. Tag the repository dsh-plugin for discoverability in the dsh plugin topic.
License
MIT