deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
PROJECT TOPICS
PROJECT README
An unofficial DSH plugin bundle and Linux setup guide, not a fork of DSH. Keep upstream DSH installed; add Smart-DSH for:
llm-pi-ai OpenCode Go route current with every model the subscription actually serves (scripts + guide).This is an unofficial community extension; it is not affiliated with DeepSeek.
Compatibility is tested against DSH 0.1.2-rc.1; mobile styles use version-specific
selectors. Turn-end notifications describe agent turn termination, not independent
verification that every requested task succeeded.
A self-contained DeepSeek Harness (DSH)
bundle (dsh-notify-push) plus setup notes for the paired remote-access infrastructure
(Tailscale Serve + phone). When the agent calls ask_user_question, your phone receives
a Web Push notification with the question text — even when no browser is connected.
When a turn ends, you get a completion notification whose title reflects the end reason
(作業完了 / token-cap truncation / blocked / error / aborted); subagent turns do not
notify — only top-level sessions. Tapping a notification focuses the app.
A second, dependency-free bundle (dsh-esc-stop) adds the keyboard gesture the
composer's stop button already performs: Escape cancels the turn that is running now.
Menus, dialogs, the queue-message editor, and IME composition keep their own Escape,
and the whole gesture has one ON/OFF row in Settings → General — no file editing and no
restart to toggle it. See Esc-to-stop.
Status: working setup on Arch Linux, verified 2026-09-07 with real deliveries to Android Chrome and desktop Firefox. Host-specific identifiers are omitted from these setup examples.
| Requirement | Why |
|---|---|
DSH 0.1.2-rc.1 |
The bundle relies on webServer.register, connection.requestRejection, and the { prepend: true } listener option — verify these exist if your DSH differs (dsh --version) |
Node >= 22.19 (23 excluded) |
DSH's own requirement |
pnpm |
dsh plugin is a thin pnpm forwarder |
Any Chromium-based browser or Firefox with PushManager |
Verified on Android Chrome (FCM) and desktop Firefox (Mozilla autopush) |
| A secure context for the phone | HTTPS via Tailscale Serve (below) — plain http://<ip>:3080 cannot subscribe to push |
| Android: nothing extra. iOS: Add-to-Home-Screen | iOS WebKit only delivers push to installed PWAs (16.4+) |
ask_user_question (tool)
└─ host: ctx.on("user-questions/request", listener, { prepend: true }) ← outermost
├─ sendPushToAll() web-push → FCM / Mozilla autopush (fire-and-forget)
└─ return next() delegates to api-remotes forwarder → browser composer
(the waterfall is NEVER consumed)
turn completion (top-level sessions only)
└─ host: ctx.inject(["agents"]) → agentCtx.on("session/event", listener)
├─ filter: agents.roots().includes(agents.get(session.id)) ← subagent turns excluded
└─ sendPushToAll(buildTurnEndPayload(session.id, event.data.reason))
titles by reason.kind: completed / max-tokens / blocked / error / aborted
| Component | File | Role |
|---|---|---|
| Host half | dsh-notify-push/lib/index.js |
prepended user-questions/request waterfall listener + session/event (turn/end) listener (root sessions only) + Web Push fan-out + HTTP routes (/api/push/* authed via connection.requestRejection, /push/sw.js static with Service-Worker-Allowed: /) |
| Client half | dsh-notify-push/lib/client.js |
window.__ModuleLoader__.load({...}) wrapper; local Notification while the page is alive; /notify popupSelect command for permission + push subscribe/unsubscribe |
| Service worker | dsh-notify-push/sw/sw.js |
push → showNotification (requireInteraction: true), notificationclick → focus/open window |
| State | $DSH_HOME/notify-push/ (0600, not in this repo) |
vapid.json (generated on first start, must persist across restarts) + subscriptions.json (auto-pruned on 404/410) |
The state directory follows DSH_HOME (default ~/.dsh), so nothing here is
hard-wired to a specific user.
# 0. Where to keep the bundle source — any persistent path works; ~/.dsh/profiles/web/bundles-src/
# is just what this setup uses. Adjust BUNDLE_SRC freely.
BUNDLE_SRC="$HOME/.dsh/profiles/web/bundles-src/dsh-notify-push"
git clone https://github.com/hikarioyama/Smart-DSH.git /tmp/Smart-DSH
mkdir -p "$(dirname "$BUNDLE_SRC")"
cp -r /tmp/Smart-DSH/dsh-notify-push "$BUNDLE_SRC"
# 1. The bundle's own dependency ("web-push"). Order relative to step 2 does not matter,
# but run this BEFORE the first restart.
cd "$BUNDLE_SRC" && pnpm add web-push@^3.6.7
# 2. Register into the web profile (adds the dependency as link: AND appends
# dsh-notify-push to dsh.profile.bundles via its dsh.bundle.patch declaration)
cd ~/.dsh/profiles/web && dsh plugin --profile web add "$BUNDLE_SRC"
# 3. Verify composition read-only (never touches a running server)
dsh --profile web --dump-config | grep dsh-notify-push # expect: "- id: dsh-notify-push"
# 4. Verify the dependency resolves (from the profile dir)
cd ~/.dsh/profiles/web && node --input-type=module -e "await import('web-push'); console.log('web-push resolvable')"
Dependency note:
dshprofiles usenodeLinker: hoistedin theirpnpm-workspace.yaml, so thepnpm addin step 1 lands inside the profile'snode_modulesand resolves from the linked bundle. If step 4 reports thatweb-pushcan't be resolved, re-run step 1 and thenpnpm installin the profile dir.
Then restart and enable:
systemctl --user restart dsh-web.service
# In the DSH UI on each device: /notify → ON → grant the notification permission.
Expected result: ~/.dsh/notify-push/vapid.json + subscriptions.json appear on first
start; each enabled device appears as one subscription; asking the agent a question that
triggers ask_user_question produces a notification on every enabled device.
On DSH 0.1.2-rc.1, upstream client-hmr opens a permanent developer-update
connection for each tab. Enough tabs can exhaust a browser's HTTP/1.1 slots,
preventing fresh session-list requests and WebSocket handshakes. This is unrelated
to the dsh-notify-push notification plugin and does not mean sessions were deleted.
Smart-DSH includes a version- and checksum-guarded workaround that shares one HMR connection across tabs. From this checkout:
node scripts/apply-shared-hmr.cjs --check # read-only; resolves the dsh on PATH
node scripts/apply-shared-hmr.cjs --apply # explicit write with a private backup
node --test scripts/test-shared-hmr.cjs scripts/test-apply-shared-hmr.cjs
It does not restart DSH or alter browser preferences/session data. Unknown versions
or existing local edits are rejected. This is a separate, optional install step;
copying only dsh-notify-push does not apply it. See the
workaround guide for explicit paths,
rollback, browser requirements, live-update limitations, and rechecking after DSH
upgrades.
Press Escape while the agent is working and the running turn is cancelled — the same
cancellation the composer's 停止生成 button performs. ON by default, with one toggle
in Settings → General → Esc で推論を停止; the value is the host setting
esc-stop.enabled in the user settings document, so it follows the account across
devices and toggling needs no file edit or restart.
Escape is a shared key, so the listener refuses whenever a nearer surface owns it:
| Refusal | Why |
|---|---|
Ctrl/Meta/Alt/Shift held, or a key repeat |
Browser/OS chords, and holding the key must not fire repeatedly |
| IME composition | The input method owns the key |
event.defaultPrevented |
A handler that already consumed Escape wins |
the target is a native input/textarea/select |
The queue-message editor closes itself on Escape |
an [aria-modal="true"] element is open |
The Settings panel owns Escape while it is open |
a [role="listbox"]/[role="menu"] element is open |
The command menu, a popupSelect, or a picker owns Escape |
| the toggle is OFF, nothing runs, the session was removed, or a subagent is on stage | Nothing to stop, and the button itself is absent in those states |
The listener is capture-phase on document with a one-microtask deferred judgement, so
Escape never closes a menu and stops the turn: a nearer handler's consumption, or the
overlay it just closed, is read exactly once — after the key has been routed.
The accepted path is the button's own (sessions.scope(id).get("conversation").cancel()),
so failure presentation is identical: the message lands in the session's promptError.
Stop cancels the in-flight turn only — queued messages stay and resume in FIFO order.
Install is the same shape as the dsh-notify-push steps with dsh-esc-stop substituted,
plus one extra registration step (the bundle has no runtime dependency of its own, so the
extra pnpm add step does not apply). dsh plugin only forwards to pnpm and the profile
is a pnpm workspace root, so the dependency needs -w; the layer list is edited directly:
BUNDLE_SRC="$HOME/.dsh/profiles/web/bundles-src/dsh-esc-stop"
git clone https://github.com/hikarioyama/Smart-DSH.git /tmp/Smart-DSH
mkdir -p "$(dirname "$BUNDLE_SRC")" && cp -r /tmp/Smart-DSH/dsh-esc-stop "$BUNDLE_SRC"
cd ~/.dsh/profiles/web && dsh plugin --profile web add "$BUNDLE_SRC" -w
node -e 'const fs=require("fs"),p=process.env.HOME+"/.dsh/profiles/web/package.json",m=JSON.parse(fs.readFileSync(p,"utf8")),b=m.dsh.profile.bundles;if(!b.includes("dsh-esc-stop"))b.push("dsh-esc-stop");fs.writeFileSync(p,JSON.stringify(m,null,2)+"\n")'
dsh --profile web --dump-config | grep dsh-esc-stop # read-only composition check
systemctl --user restart dsh-web.service # never from the session being restarted
Until that restart the running server keeps its boot-time composition: the settings row and the listener appear only afterwards.
Details, guard-by-guard rationale, and limitations: dsh-esc-stop/README.md.
The parent agent spawns named subagents with one exclusive verb each — worker
(edits only the paths the assignment names, then checks them), explorer
(measures, never edits), hacker (attacks the assignment's premises; exactly three
hacks in MiL, no experiments), and reviewer (judges the assigned diff against the
assignment citing loc:=path:line). Children never see the parent conversation:
the assignment is the whole task, and a worker launch without concrete named paths
is refused by the harness.
Reports flow through a sibling hub as one-line MiL facts addressed to a living
roster id; prose reports are rejected (report:=∅ ∵ ¬MiL) and the child is nudged
once to restate. They land in a subagent conversation tab in the web UI, and
task logs persist under $DSH_HOME/tasks/. ask_user_question inside a child
is refused and logged, so a child can never stall on a question it cannot see the
answer to; subagent turns do not notify.
Install (no runtime dependency — the pnpm add step that dsh-notify-push needs
does not apply; the -w flag is because the profile is a pnpm workspace root):
BUNDLE_SRC="$HOME/.dsh/profiles/web/bundles-src/dsh-tasks"
git clone https://github.com/hikarioyama/Smart-DSH.git /tmp/Smart-DSH
mkdir -p "$(dirname "$BUNDLE_SRC")" && cp -r /tmp/Smart-DSH/dsh-tasks "$BUNDLE_SRC"
cd ~/.dsh/profiles/web && dsh plugin --profile web add "$BUNDLE_SRC" -w
node -e 'const fs=require("fs"),p=process.env.HOME+"/.dsh/profiles/web/package.json",m=JSON.parse(fs.readFileSync(p,"utf8")),b=m.dsh.profile.bundles;if(!b.includes("dsh-tasks"))b.push("dsh-tasks");fs.writeFileSync(p,JSON.stringify(m,null,2)+"
")'
dsh --profile web --dump-config | grep dsh-tasks # read-only composition check
systemctl --user restart dsh-web.service # never from the session being restarted
The unit suite (25 tests: tool policy, the MiL reporter, both bundle halves)
resolves its DSH imports through the profile's hoisted @deepseek-ai/*, so it runs
from $BUNDLE_SRC after registration — a bare clone shows ERR_MODULE_NOT_FOUND.
Details: dsh-tasks/README.md.
DSH ships the OpenCode Go provider with a frozen in-catalog model list, so new
models the subscription actually serves (and ones the catalog dropped) drift apart.
This directory is a small kit that reads the live Go model list and rewrites
llm-pi-ai.providers.opencode-go.* in $DSH_HOME/settings.yaml:
| File | Role |
|---|---|
gen.mjs |
emits the catalog delta (bundled-catalog route + two declaration-only routes for catalog-unknown IDs) from the bundled pi-ai catalog table |
apply.mjs |
splices the delta into settings.yaml through the YAML Document API (comments preserved), with a backups/ + restore.sh snapshot |
verify.sh |
probes every configured model over the real wire (streaming chat/completions, plus the /responses route) and prints an OK/SKIP/NG table |
set-default-model.cjs |
test helper: swap agent-default-model in a disposable settings copy |
cd opencode-go
node gen.mjs # review the 29-model catalog (3 routes)
node apply.mjs --dry-run # diff only
node apply.mjs # write (live-watched by DSH — no restart)
sh verify.sh # per-model wire probe
Three notes verified against the live endpoint on 2026-09-29:
muse-spark-* additionally needs the "trains on request data"
consent. Both live in the OpenCode console's Privacy page (which is not linked in
its navigation — open …/console/<org_id>/settings/privacy directly).deepseek-v4.1-flash, mimo-v2.6-*, space-bunny-free,
gpt-6-luna, grok-4.7, …) cannot inherit from the frozen pi-ai catalog, so
they are declared on separate opencode-go-completions / opencode-go-responses
routes that carry their own api/baseURL — the main route must stay protocol-mixed.kimi-k2.6, omen-alpha, minimax-m2.7) are excluded; grok-4.7
runs non-reasoning because xAI rejects reasoning_effort: "none".Details: opencode-go/README.md.
Minimal, reproducible form — the flock/guard/URL-file plumbing in the author's setup
is machine-specific and intentionally not part of this repo:
# ~/.config/systemd/user/dsh-web.service (minimal working form)
# Adjust the ExecStart path to your dsh install location (`which dsh`).
[Unit]
Description=DSH web server
After=network.target
[Service]
Environment="DSH_HOME=%h/.dsh"
ExecStart=%h/.local/bin/dsh web --host 127.0.0.1 --port 3080 \
--trusted-host <machine>.<tailnet>.ts.net --no-open
Restart=on-failure
[Install]
WantedBy=default.target
# Expose to the tailnet only (current tailscale CLI syntax; run once, persists):
tailscale serve --bg 3080
# → https://<machine>.<tailnet>.ts.net (proxying http://127.0.0.1:3080)
tailscale serve status # verify
Notes learned during setup:
/push/sw.js route is reachable from tailnet devices only.--trusted-host value must match the hostname your phone uses, or the browser-trust
fence will reject the connection.tailscale serve syntax differs across versions (serve --bg 3080 on current CLI,
serve https / http://... on older ones) — check tailscale serve --help.A self-contained node --test suite ships with the bundle (writes only to a temp
DSH_HOME, touches no real state):
cd dsh-notify-push && pnpm install && npm test
# expect: pass 1 / fail 0
What it covers:
ctx (ctx.on/effect/inject + webServer.register collector +
Readable.from request bodies): route registration, auth 401/403 on /api/push/*,
subscription validation + persistence, waterfall delegation (next() value passes
through), and the Service-Worker-Allowed: / header on /push/sw.js.ENOTFOUND against an invalid endpoint),
which proves the pipeline up to the network.lib/client.js with a window.__ModuleLoader__ shim and a stub ctx (assert the
user-questions/request listener passes next()'s value through, and that the
/notify popupSelect contribution registers).dsh-esc-stop ships its own suite, which does cover the client half through the same
ModuleLoader shim (decision guards, listener wiring, disposal, and the settings row):
cd dsh-esc-stop && npm install && npm test
# expect: tests 14 / pass 14 / fail 0
/notify ON/OFF per device. Revoking browser permission → auto re-subscribe
fails at the next startup and the stored toggle drops to OFF.$DSH_HOME/notify-push/vapid.json still exists after restart (fresh keys would orphan
every subscription — delete subscriptions.json too if you intentionally reset keys).vapid.json survived the restart, (2) no
notify-push errors in the server log, (3) the site's notification permission is
"Allow" and Chrome's system-level notifications are on, (4) subscriptions.json still
has entries.dsh.bundle.patch + dsh.client three-layer plugin pattern;
see dsh-notify-push/cordis.patch.yml and package.json for the minimal declarations./notify command copy are
Japanese by default (選択肢, (他 N 件), 通知: ON). Edit buildPayload in
lib/index.js, questionSummary/statusLabel/command labels in lib/client.js.mailto:root@localhost in configure() is a placeholder; some push
services warn about it — set your own contact address.PushManager passes the /notify availability
check; only Android Chrome and desktop Firefox have been verified.requireInteraction: true in sw/sw.js keeps the notification on screen; lower it
if you prefer transient banners.The knowledge-graph node mirroring this repo:
~/knowledge/nodes/agents/dsh-notify-push-bundle-pattern.md.
At viewport widths up to 767 CSS pixels, the collapsed sidebar occupies only the
logo corner; the chat column uses the full viewport width. Tap the DeepSeek logo
to open the session list in one step, not the intermediate icon rail. Question
and plan cards use the column width (100%) instead of the 680px content floor,
with 2.5% side padding, so a narrow phone does not clip either edge. Desktop
layout is unchanged.
Upstream expanded sidebar panels retain their normal behavior. CSS-module selectors
are version-specific: recheck after a DSH upgrade. No notification logic is changed.
Browser regression check against an existing DSH server (does not start/restart DSH):
PLAYWRIGHT_MODULE=/path/to/playwright DSH_LOGIN_URL_FILE=/path/to/private-login-url.txt node scripts/test-mobile-layout.cjs.
Uses isolated browser contexts, verifies 360/412/767/768/1280 CSS-pixel widths, toggle
round trips and cleanup. The login URL file must be private; never commit it.
scripts/test-esc-stop.cjs drives the shipped dsh-esc-stop listener inside a live DSH
page (same private login URL input, no DSH restart):
PLAYWRIGHT_MODULE=/path/to/playwright \
DSH_LOGIN_URL_FILE=/path/to/private-login-url.txt \
node scripts/test-esc-stop.cjs
It asserts that the composed provider bundles (dsh-api-session-controller,
dsh-client-ui-renderer, dsh-client-ui-settings) are present, then dispatches real
KeyboardEvents against stub sessions — one cancel on a running turn, and no cancel for
idle, toggle-off, key repeat, modifier chords, an open modal, an open list overlay, a
focused native input, an already-consumed key, disposal, and the named refusal reason. Stub sessions mean no real
turn is cancelled; the whole bundle is also loaded into the page to prove it parses and
registers with __ModuleLoader__. The login URL file must be private; never commit it.
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。