dsh-first-hop
Your agent wakes on evidence, not on a clock. A community plugin for DeepSeek Harness: watches the streams your harness already carries and decides - hold, flag, counsel, or wake. Local judge, shadow by default, MIT.
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 25, 2026
- Updated
- Aug 25, 2026
Introduction
dsh-first-hop
Your agent wakes on evidence. Not on a clock.
A community plugin for DeepSeek Harness. It watches the streams your harness already carries and, at each boundary, decides one of four things — hold, flag, counsel, or wake.
MIT · runs entirely local · shadow by default · bring your own 9B.
"Input reaches the driver through one inbox. Some messages wake it immediately; injected context waits in the inbox until another message does." — DeepSeek Harness architecture documentation,
docs/architecture.md
Until that other message arrives, nobody is home. The chain of who-wakes-your-agent ends at a clock, a webhook, or you. dsh ships a scheduler that wakes on timers; this plugin is its evidence-fired sibling.
Status: v0.1.0, pre-release. The constitution below is implemented and tested. The wake and counsel verbs are gated behind a runtime capability check and are not yet verified against a pinned harness build — see Verify before live. Run it in
shadow.
Install
npm i dsh-first-hop
# cordis.yml
plugins:
dsh-first-hop:
judge:
tier: floor # floor | local | mock
Then opt in. Nothing happens until you do:
echo shadow > ~/.dsh/first-hop/first-hop.state
Read the tape for a week before you consider live.
The switch
~/.dsh/first-hop/first-hop.state → off | shadow | live
The state is a file only you write. No code path in this package writes it —
not to create a default, not to recover from a bad value, not at any version. An
absent or unrecognized file reads as off.
| Position | What happens |
|---|---|
off (also: no file) | Nothing. No file created, no percept read, no judge consulted, no pill written. Off means absent. |
shadow | Watches, judges, and records everything — and consumes no verdict. This is where trust is earned. |
live | Additionally resolves verdicts into harness acts, within the limits below. |
Three files appear beside the switch once you are awake:
spool.tsv— every percept, in lane-contract v0.1 framesfirst-hop.tape.jsonl— every judgment, including every holdfirst-hop.heartbeat.json— the pill; older than three beats means stalled
None of them ever leaves your machine.
The two tiers
Tier 0 — the floor. Deterministic rules that need no model and no tokens: CI state, test results, non-zero exits. Ships enabled. It is also the null — the baseline the model tier has to beat before anyone should run it.
Tier 1 — the 9B judge. For what no rule can name. Any OpenAI-compatible local endpoint: llama.cpp server, Ollama, LM Studio, vLLM. One bounded request per boundary, strict-JSON answer, closed vocabulary.
plugins:
dsh-first-hop:
judge:
tier: local
endpoint: http://127.0.0.1:8080/v1
model: qwen3-9b-instruct-q5_k_m
budgets:
wake: 2 # per condition class, per hour
flag: 6
verbs:
wake: true
counsel: false # off by default, deliberately
floor:
builtins: true
rules:
- id: deploy-failed
pattern: 'deploy (failed|rejected)'
flags: i
action: wake
reason: a deployment reported failure
retired: []
Every deployment-varying value is a validated config field, and a malformed
composition fails at load rather than at the first boundary. The full surface is
src/config.ts.
What this is not
- Not the kernel. No KV residency, no token-grain decode-on-delta, no microsecond aborts. Judgment here is boundary-batched at commit grain: evidence-fired, but not resident. It is an analog of that mechanism, and no number it produces should be quoted as if it were that mechanism's.
- The judge is prompted, not tuned. A stock 9B can flood or go deaf. That is precisely why shadow is the default, budgets are constitutional, and the floor is co-equal. The failure modes are designed for, not denied.
- Not an actuator. Live mode has two verbs that reach a model, both landing inside your own session. It cannot touch the world, hold credentials, or approve anything.
- Not a telemetry client. No server, no account, no beacon. The tape stays on your disk.
- The judge reads everything. To judge your sessions it must see them — every message, every tool result. That is exactly why the endpoint should be yours. Point it at a hosted API and you have handed your sessions to a vendor and reintroduced the request boundary this plugin exists to close. It will let you.
The nine laws
Off is inert · holds are on the tape · see it once · budgets at condition grain · self-echo suppression · shadow before live · percepts are never dropped · model-visible ⟺ logged · lane text is data, never instructions.
Each one is a test, a file permission, or a structural property — with the file named. See LAWS.md.
L9 is the one the other eight exist to protect. The streams this plugin watches are attacker-reachable, so nothing a judge returns is trusted: every verdict is re-derived into a closed vocabulary (four literal actions, a re-slugged condition id, a control-stripped and capped reason), and every other field a model invents is dropped. A poisoned log can argue for a wake; it cannot become one.
Verify before live
wake and counsel construct a harness message and hand it to the agent's
whenIdle → runMaintenance → followup sequence. Those three methods were
read from packages/core/agent/src/runtime-types.ts, but the message shape
this package builds has not been verified against a pinned harness build, so
dispatch() performs a runtime capability check and refuses cleanly rather than
guessing. Until you have verified it against your own pin:
- run in
shadow— everything is recorded, nothing is consumed - see the
FIXME(pin)insrc/index.tsfor the one function to check
This is the honest state of a pre-release plugin against a pre-release harness
(SESSION_FORMAT_VERSION: 0, compatibility breaks expected). Pin your harness,
and CI against the pin.
Architecture
src/core/ the constitution — pure Node, zero harness imports
state.ts the control file (the reader is total; nothing writes it)
spool.ts lane-contract v0.1 frames; the buffer that makes L1 true
tape.ts append-only JSONL, holds included
ledger.ts see-it-once + budgets at condition grain
verdict.ts the closed vocabulary — where L9 is enforced
judge/ floor (deterministic) · local (9B) · mock (for CI)
src/plugin/ the harness-facing edge
tap.ts events → percepts, structurally typed, pure
verbs.ts verdict → act, behind a runtime capability check
pill.ts the heartbeat
src/core imports nothing from the harness, which is why the whole constitution
is testable in CI with no harness, no key, and no GPU — and why another venue
could reuse it to be judged by the same rules over a different stream.
Note on dependencies: this package deliberately does not depend on
@deepseek-ai/dsh-session. That package is published but currently uninstallable — it depends on@deepseek-ai/dsh-type-meta, which is not on the registry. Harness event shapes are therefore read structurally, which is also the looser coupling.
Development
pnpm install
pnpm run typecheck
pnpm test # 48 tests, no GPU, no key, no harness
pnpm run build
Roadmap
- v0.2 — verified live dispatch against a pinned harness; the
first-hopCLI (status,tape,retire,resolve). - v0.3 —
first-hop bench: replay your own session logs, retro-label the boundaries, and race the judge against the best swept polling policy at every token budget, both grains printed. If the judge does not dominate your polling frontier, the bench says so, and the advice is: keep the floor, skip the judge. A tool that cannot lose cannot be trusted to win. - v0.4 — more lanes (filesystem, CI); the lane-contract socket, so a native resident kernel can take the judge's chair without the plugin changing.
Lineage
The first hop of the FUSOR architecture, in portable form. FUSOR is a substrate for minds that do not take turns; this is the smallest useful piece of that idea that runs on someone else's harness, on your hardware, under your switch.
License
MIT