History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
19 KiB
dsh as a cheap worker
Owner instruction, 2026-09-02 (Discord/session, verbatim): "你再帮我整个dsh,然后 协调通,让dsh里面的agent也当你的sub agent,他们还便宜" — stand up dsh, wire it in so agents running inside dsh act as sub-agents for the coordinator, because they are cheaper.
What dsh is
dsh = DeepSeek Harness, DeepSeek's own official agent CLI —
@deepseek-ai/dsh on npm, MIT, built on the @deepseek-ai/cordis plugin
kernel (https://github.com/deepseek-ai/deepseek-harness). It is a different
agent runtime in the same slot claude (GOD_MOD) and the old dsh web
button occupied — see
WillTools.extension/WillTools.tab/Lab.panel/GodMod.pushbutton/script.py's
own comment on what it replaced, and tools/aiextract_run.py's module
docstring, which documents dsh's local RPC wire contract in detail (dsh
serves POST http://127.0.0.1:3080/api/<method> with session.create /
session.prompt / session.history / session.cancel, confirmed against a
real reply on 2026-09-01).
Config lives under $DSH_HOME (default ~/.dsh): profiles/<name>/ (a
patch-layered plugin stack), settings.yaml (models/providers), and
credentials via a credentials service. dsh --version reports
0.1.1-rc.2.
The route actually in use — NOT DeepSeek's own endpoint
The owner's own words, 2026-09-02: "dsh我弄好了,不过用的是opencode go的API, 不过一样能用。" — he got dsh working, but through an opencode go API route rather than DeepSeek's official one, and it works the same either way. Confirmed live on this machine by reading his config (never its secret value) rather than assuming:
- Profile:
headless— dsh's own one-shot mode, unchanged from the original plan.dsh --profile headless "task text": "Answer one task, print the final assistant message, and exit" (its own--helptext). - Route:
~/.dsh/settings.yamlcarries a top-levelagent-default-modeloverride that the built-indsh-agent-default-modelplugin (whose compiled-in default isprovider: deepseek-official) does not show indsh --dump-config— that command prints the composed bundle tree, not the runtime settings-file layer, so the override is real but invisible to that flag. The live override points at provider idopenocde-go(that is the literal string in his config, letters swapped from "opencode" — his file, not a typo this repo should "fix") — declared underllm-pi-ai.providers.openocde-gowithapi: openai-completions,baseURL: https://opencode.ai/zen/go/v1. - Model:
deepseek-v4-flash, per that sameagent-default-modeloverride. The provider's own model list also carriesglm-5.3-flash,deepseek-v4-pro,deepseek-v4-flash-vision-exp, andhy4-previewas options, butdeepseek-v4-flashis what a plaindsh --profile headlesscall actually boots. - Credential mechanism: dsh's own credentials service
(
~/.dsh/.credentials.yaml, thedsh-credentials-localplugin), keyed by the env-var name the provider declares (apiKeyEnv: OPENOCDE_GO_API_KEY) — not an OS environment variable.OPENOCDE_GO_API_KEYis unset in every shell on this machine; dsh resolves it through the credentials file instead, which is why the original worker's assumption (export DEEPSEEK_API_KEY) never applied here and a plain shell round trip works without that variable being set anywhere visible toecho $OPENOCDE_GO_API_KEY.
None of this was changed to write this document — it is a description of
what was already there, confirmed with dsh --dump-config, dsh --help,
and reading (not printing) settings.yaml and .credentials.yaml's key
names, never their values.
This means the worker must not hardcode a profile. A different machine
with a real DEEPSEEK_API_KEY and no settings.yaml override gets dsh's
stock deepseek-official route through the exact same headless profile
and the exact same tools/dsh_worker.py — the profile is the only thing the
CLI needs told, and it is now $DSH_WORKER_PROFILE / --profile,
defaulting to headless, never baked in.
A Windows-specific bug this task found and fixed
dsh installs on Windows as a dsh.CMD shim (npm install -g puts it at
%APPDATA%\npm\dsh.CMD) that forwards its arguments to node <shim-dir>\node_modules\@deepseek-ai\dsh\lib\bin.js. Handing that shim a
multi-line task through Python's subprocess.run([...]) (no shell=True)
still routes through cmd.exe to resolve the .CMD file, and cmd.exe's own
command-line parser treats a newline inside an argument as a command
separator — silently truncating the task at the first newline, exit 0, no
error. Measured 2026-09-02: a two-line task reached dsh as its first line
alone, and dsh answered a real, well-formed reply to that truncated text —
the most dangerous shape of this bug, because nothing about the reply looks
wrong on its own; it just answers a different, shorter question than the one
sent.
tools/dsh_worker.py's _resolve_launch_cmd now detects a .cmd/.bat
shim on Windows and calls node.exe on the resolved bin.js directly,
bypassing cmd.exe's argument parsing entirely. Off Windows, or when the
shim can't be resolved to a real bin.js next to it, the shim is invoked
exactly as before. There is also a plain UnicodeDecodeError this same code
path hit before the fix (Windows console codepage vs. UTF-8 dsh output) —
subprocess.run(..., encoding='utf-8', errors='replace') is now explicit
rather than left to text=True's platform default.
The worker: tools/dsh_worker.py
python tools/dsh_worker.py "task text on the command line"
python tools/dsh_worker.py --file task.md
echo "task text" | python tools/dsh_worker.py -
python tools/dsh_worker.py --profile headless --timeout 300 "task"
python tools/dsh_worker.py --plain "task text" # reply/reason only, no JSON
Prints exactly one JSON object to stdout and sets the exit code to match:
{"ok": true, "reply": "<the agent's final message>"} exit 0
{"ok": false, "refused": true, "reason": "<what went wrong>"} exit 1
$DSH_WORKER_PROFILE overrides the default profile (headless);
$DSH_WORKER_TIMEOUT_S overrides the default per-call timeout (300s). Both
are also settable per-call with --profile / --timeout. --plain is the
one non-default output shape — reply text only on success, reason text only
on refusal — meant for a human at a terminal; every caller that is code
should use the default JSON shape, which is the "structured mode" that lets
a caller tell a reply from a diagnostic without parsing prose. --json also
exists as an explicit no-op for a caller that wants to say so.
Two real transcripts, captured 2026-09-02 against the live config above
Trivial round trip:
$ python tools/dsh_worker.py "reply with exactly: pong"
{"ok": true, "reply": "pong"}
$ echo $?
0
(3.5s wall clock.)
Realistic task — a mechanical rewrite with a checkable output, the exact shape this worker is for (docstring text below is illustrative, not a real file in this repo):
$ cat task.txt
Rewrite the following Python docstring in plain, non-jargon English, at
most 3 sentences. Reply with ONLY the rewritten text, nothing else.
Original docstring:
"""
Resolve the effective knowledge index for a job by merging DEFAULTS, the
catalogue layer, the plant layer, the job layer, and the project config
layer, in that order, and return the merged mapping along with a list of
provenance tuples (layer_name, key_path) for every key that was overridden
by a layer after DEFAULTS. Raises ConfigMissingError if a layer pointer is
present but the referenced file cannot be found on disk.
"""
$ python tools/dsh_worker.py --file task.txt
{"ok": true, "reply": "Merges the default settings with the catalogue,
plant, job, and project configuration layers in that order, then returns
the combined settings along with a list showing which keys each later
layer changed. Raises ConfigMissingError if a layer points to a file that
cannot be found."}
$ echo $?
0
(10.2s wall clock. The rewrite is accurate and reads well — see "What this is actually good for" below for how far that generalizes.)
Both of these were refusals under the original worker before the Windows
argv fix and the credential setup landed: the first version of this file
recorded a real MISSING_CREDENTIAL refusal (correct behaviour for a
missing key, at the time it was true), and a naive re-test of the second
example — multi-line task text through the un-fixed worker — silently
truncated to the first line and returned a plausible-looking but wrong
answer, which is exactly the failure mode CLAUDE.md's "could not judge" vs.
"judged, found nothing" rule exists to prevent one layer further down than
the check that already guards it: this was a transport bug, not a dsh
refusal, so no DshRefusal fired for it, and the fix had to be in the argv
path, not the refusal table.
Failure/refusal behaviour
| what happened | what the worker returns |
|---|---|
| dsh not on PATH | refusal naming the missing binary and the install command |
| empty task text | refusal, nothing sent |
| dsh exits non-zero | refusal quoting dsh's own stderr verbatim |
| dsh times out | refusal naming the timeout budget |
| the process cannot even start (e.g. permissions) | refusal naming the OS error |
| dsh exits 0 but prints nothing | refusal — "exited 0 but printed no reply ... could not judge, not judged-empty" |
The last row is still the one worth calling out by name: an exit-0,
empty-stdout result is the exact silent-pass shape CLAUDE.md's skip-probe
table exists to prevent for Ticket QC checks. This worker refuses that shape
rather than returning {"ok": true, "reply": ""}.
Tests: WillTools.extension/tests/test_dsh_worker.py — faking the
which() / runner() subprocess boundary, plus dedicated cases for
_resolve_launch_cmd's Windows-shim resolution, so the argv-truncation
fix is covered without needing a real Windows .cmd shim on disk.
The pool: tools/dsh_pool.py
dsh_worker.py alone is one task at a time. The actual point of a cheap
backend is running many small tasks at once — that is what
tools/dsh_pool.py adds: a bounded ThreadPoolExecutor fan-out over
dsh_worker.run_task, N tasks in, N results out, in input order, each
result independently {"ok": true, ...} or {"ok": false, "refused": true, ...}.
python tools/dsh_pool.py --tasks tasks.json --out results.json
python tools/dsh_pool.py --tasks tasks.json --max-workers 8 --task-timeout 120
tasks.json is a JSON array — either plain strings, or objects carrying a
"task" key plus whatever else the caller wants echoed back (an "id", a
"label") so results can be matched by identity rather than array position:
[
{"id": "t1", "task": "Reply with exactly: one"},
{"id": "t2", "task": "Reply with exactly: two"},
{"id": "t3", "task": "What is 17 * 3? Reply with just the number."},
{"id": "t4", "task": "In one short sentence, what does 'refactor' mean to a programmer?"}
]
Concurrency cap: --max-workers / $DSH_POOL_MAX_WORKERS (default 4).
Per-task timeout: --task-timeout / $DSH_POOL_TASK_TIMEOUT_S (default:
same as dsh_worker's own, 300s).
A real fan-out run, captured 2026-09-02 (4 tasks, --max-workers 4)
$ python tools/dsh_pool.py --tasks tasks.json --max-workers 4
[
{"id": "t1", "ok": true, "reply": "one"},
{"id": "t2", "ok": true, "reply": "two"},
{"id": "t3", "ok": true, "reply": "51"},
{"id": "t4", "ok": true, "reply": "Refactoring means restructuring
existing code to improve its internal design (readability,
maintainability, performance) without changing its observable
behavior."}
]
$ echo $?
0
5.6s wall clock for 4 tasks that individually take several seconds each —
the fan-out is real, not serialized. 17 * 3 = 51 came back correct;
worth noting for the "what this is good for" section below, since a wrong
arithmetic answer here would have been a much cheaper thing to catch than
one buried in real work.
One task failing never takes the batch down: _run_one (inside
dsh_pool.py) catches both DshRefusal and any other exception per task
and turns it into that task's own refusal-shaped result, so task 3 raising
never prevents tasks 1, 2, and 4 from completing or reorders them.
The CLI's own exit code reports whether the batch ran to completion, not
whether every task inside it succeeded — a caller checks each result's
"ok" field for that, by design: a batch exit code that went non-zero on
any single refusal would make a script stop trusting the N-1 tasks that
actually worked.
Tests: WillTools.extension/tests/test_dsh_pool.py — ordering preserved
under out-of-order completion, one failure isolated (both a DshRefusal
and an arbitrary exception), a refusal shape kept distinguishable from a
short successful reply, the worker cap actually measured under load (not
just accepted as an unused argument), and the tasks-file loading/echo
behaviour.
What to route to it, and what not to — grounded in what was actually observed here, not a guess about model quality
The two transcripts above and CLAUDE.md's own measured history line up exactly, and the second is the harder-won lesson:
A cheap-model extraction pass over one document drop in this repo's own history carried a 42% page-error rate on independently sampled pages — inverted inequalities, misidentified parts, specs blurred into vagueness. A sub checking its own family's work reported zero errors on the same material where an independent auditor found that 42%. That is not "dsh is bad" — the two transcripts in this document show it doing accurate, useful work on scoped tasks (a docstring rewrite that reads correctly, correct arithmetic, a fan-out where every one of four small, independent tasks came back right). It is specifically that a cheap model cannot be trusted to audit itself, or anything else, at the standard this repo demands of a number that lands somewhere consequential — the checker has to be a different pass, ideally a different model or a human, not the same model grading its own paper.
Route to dsh (cheap, routine, and — this is the load-bearing word — CHECKABLE by something other than the same cheap model):
- A well-scoped drafting task with a clear, verifiable output — a docstring rewrite, a commit-message draft, a regex, a summary of a file — where the caller (a human or a second, independent pass) reviews the output before it lands anywhere that matters.
- Mechanical rewrites with a spec the caller can check itself — the docstring example above is the template: input and output are both short enough that verifying the rewrite is faithful takes seconds, even though writing it took the model 10 seconds of "thinking."
- Wide fan-out where one bad answer in isolation is cheap: N similar small tasks (dsh_pool.py's whole reason to exist) where a wrong result is one wrong row a caller can eyeball or spot-check, not a silent poison in an aggregate.
- Summarization and other reductions where "close enough, and skimmable fast" is genuinely the bar, not "exactly right."
Do NOT route to dsh, ever:
- Anything landing a NUMBER in a knowledge layer. This is exactly what
knowledge/library/RUNBOOK.md's three-stage discipline (extract cheap, audit independently, land off the page) is built to prevent a cheap model from doing unsupervised — a number from a dsh reply is a note, never a landed value, and it lands only after a human or an independent pass re-reads the primary source. - Any audit of another model's work — cheap or not. The 42%-vs-zero result above is the concrete, measured reason: self-audit (or same-family audit) is not audit, and dsh grading dsh's own output (or a Claude subagent's) inherits that exact blind spot.
- Anything touching the live Revit bridge, a Transaction, or the open
model. Those go through
tools/rvt.pyunder a human's own session per CLAUDE.md §1/§3 — not through a detached headless worker with no view of what it is touching and no way to be stopped mid-mutation. - Anything needing the coordinator's actual conversation context. A
headless
dsh --profile headlesscall starts a fresh session with only the task text handed to it — no memory of anything the coordinator has done or decided this session, confirmed by this task's own transcripts (each call above got exactly and only what was in the task string, no more). - Any task whose failure mode is expensive and hard to notice — a plausible
wrong number, a wrong fact presented fluently. Cheap-model output for that
class of task needs the same landing discipline
knowledge/library/RUNBOOK.mdrequires everywhere else: land only off the primary source, never off a model's note, cheap or expensive.
The exact command a caller uses
For one task:
python tools/dsh_worker.py "task text" --profile headless --timeout 300
(Omit --profile to use $DSH_WORKER_PROFILE or the headless default;
omit --timeout for $DSH_WORKER_TIMEOUT_S or 300s.) Parse stdout as one
JSON object; check "ok" before trusting "reply".
For a batch:
python tools/dsh_pool.py --tasks tasks.json --out results.json \
--max-workers 4 --task-timeout 300
Parse results.json (or stdout, if --out is omitted) as a JSON array in
the same order as tasks.json; check each entry's "ok" independently —
one refused task must never be read as the whole batch having failed, and a
successful-but-short reply must never be mistaken for a refusal or vice
versa (both are enforced by construction: "reply" only appears on
ok: true, "reason"/"refused" only on ok: false).
Cost, relative to a Claude subagent
DeepSeek's own published API pricing (api-docs.deepseek.com, checked 2026-09-02) for the V4 Flash / Vision-Exp tier: off-peak input $0.22 / output $0.66 per million tokens (cache-miss), roughly half that at peak hours 01:00-04:00 and 06:00-10:00 UTC weekdays, with cache-hit input far cheaper still. That undercuts a Claude subagent's per-token API price by roughly an order of magnitude on the numbers checked. The actual route in use here goes through the opencode go API rather than DeepSeek's own endpoint, so that published pricing is DeepSeek's own model cost, not necessarily what opencode go itself charges for the same route — treat the ratio as "cheaper, by a lot," not a guaranteed multiplier, and re-check before quoting a precise number in anything the owner will rely on financially.
What is NOT built
This is a synchronous-per-task, bounded-concurrency CLI bridge. It is not:
- a persistent multi-turn session manager (that already exists, in
tools/aiextract_run.py'ssession.*RPC client, for the one caller that needs to poll a long-running vision pass); - an automatic task router that decides for the coordinator which tasks go to dsh vs. a Claude subagent — that decision is made by whoever is dispatching work, using the routing section above;
- a retry/backoff layer — a refused task in a pool run stays refused; a caller that wants a retry re-submits that one task itself, since retrying automatically would blur "this dsh call failed" into "this dsh call succeeded on attempt 2," which is a different, noisier signal than the one this file is built to keep honest.