deepseek-harness
deepseek-ai
DeepSeek Harness: Everything is a Plugin.
PROJECT TOPICS
PROJECT README
A resumable drop-in replacement for the dsh --profile headless runner.
@deepseek-ai/dsh-headless mints its session identity inside run():
agents.create({ sessionId: SessionId(`session-${randomUUID()}`), … })
and dsh --profile headless accepts no flag beyond --help. So an unattended
run is unaddressable: its transcript lands on disk under an id nobody wrote
down, and nothing can ask for it back. A process that dies — a Fly deploy
SIGTERMs in-flight work on every deploy, and an agent build can run for hours
— takes the whole run with it.
Everything needed to fix that is already in dsh core. ctx.agents.resume() is
published API, the JSONL persistence backend is mounted in the headless bundle
already, and its crash-repair path is specified and (see below) works. The only
missing piece was a runner that reaches them.
The shipped headless bundle patch is a declarative entry list, so this is a
replacement, not a fork: the overlay disables the stock headless-runner
row and inserts this package in its place. It depends on dsh's published
packages; it patches nothing and vendors nothing.
Not on npm — install it from git, which puts it at the same path (npm derives
the folder from package.json's name, verified rather than assumed):
npm i github:Outblock/dsh-headless-resumable
dsh --profile headless --patch node_modules/@outblock/dsh-headless-resumable/cordis.patch.yml "<the brief>"
| env | effect |
|---|---|
DSH_RUN_SESSION_ID |
deterministic session identity. Required to ever resume. Omitted → session-<uuid>, i.e. stock behaviour. |
DSH_RUN_RESUME=1 |
restore that session's persisted log and CONTINUE it |
DSH_RUN_CONTINUATION |
what to say to a resumed session (has a default) |
DSH_RUN_MARKER |
path for a terminal-outcome JSON marker |
DSH_RUN_SETTLE_MS |
SIGTERM settle budget, default 20000 |
Every one of those is an ordinary plugin config key (sessionId, resume,
continuation, markerPath, settleTimeoutMs); the environment mapping lives
in the patch file, so a caller who prefers literals can write them there
instead. Configuration is environment-driven rather than flag-driven because
the shipped headless-startup provider parses exactly one positional and
--help — extending the CLI would mean forking a second package for nothing.
A supervisor decides re-queue vs. abandon from these alone.
| code | meaning |
|---|---|
0 |
the turn completed |
1 |
the turn ended not-completed, or the driver threw |
17 |
resume was asked for and the session could not be loaded |
18 |
resume with no sessionId |
75 |
SIGTERM/SIGINT settled the turn; session flushed and resumable (EX_TEMPFAIL) |
17 is distinct because it is the one failure where retrying the same command
changes nothing.
17. A fallback here spends a second
full build and reports success: indistinguishable from a slow build, and
double the bill. Loud failure is recoverable by a human in a minute.keepInbox, waits for quiescence under a bounded timeout, flushes, writes
the marker, and exits through the same ctx.appExit the happy path uses so
the tree still disposes in order.Against @deepseek-ai/dsh 0.1.0-rc.6 on deepseek-v4-flash (the headless
bundle's default), on a task with one counted external side effect per step:
75, marker interrupted/flushed: true,
resume continues the same session directory, on-disk seq continues the stored
log, and no completed step is repeated.Also proven once on a real 38-minute agent build (deepseek-v4-pro, SIGTERM at
10 minutes): one session directory throughout, seq 44496 → 44498 → 164406,
deliverable produced and its own validation gate passed. The work done before
the kill was not redone — the input documents were read 5 times before it
and 0 times after, and the resumed agent's first act was to inspect what it had
already written.
The persistence backend's own contract (its README) is that every durable
append is fsynced, an incomplete trailing frame is truncated and re-encoded
with synthetic tool/step/turn closers, an interrupted tool call is replayed to
the model as TOOL_OUTCOME_UNKNOWN, and a defect at or before the last
committed turn/end is corruption and rejects. This runner relies on that
contract and adds nothing to it.
sessionId — a re-queue
that races a still-draining container is the one way to corrupt a log that
this runner cannot defend against.17 (loudly, but for a misleading reason).DSH_HOME must outlive the container. Everything above is moot if the
sessions root is on ephemeral disk; that is what the id was for.20000 wants a kill timeout of ~30s so the tree disposal that
follows it also fits. Fly's default is 5s and Kubernetes' is 30s; under a
short one, SIGKILL arrives first and the run resumes through torn-tail repair
instead — which works, but pays a repaired tool call for it.test/kill-resume.mjs is the proof: it launches the
real binary, kills it at a randomised point, relaunches with resume, and
asserts on outcomes — the task completes, the session directory is the same
one, the on-disk seq continues, and the pre-kill side effects are not redone.
It reads no filename or event shape from a constant; it finds the transcript by
walking what the writer left. Nothing is mocked across the persistence
boundary, because a fixture built by the reader can encode the same wrong
assumption on both sides — which is exactly how we once shipped a resume
that never worked once in its life while its suite stayed green.
DEEPSEEK_API_KEY=… node test/kill-resume.mjs --signal both --rounds 3
It drives a real model, so it costs a few cents and is not a CI-shaped test.
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。