dsh-plugin-durable-subagent
Durable subagent execution for DeepSeek Harness. It registers a durable provider on ctx.subagents and a model-facing durable_subagent tool, both driving pi-durable-subagents.
The point of the pairing is one guarantee: a delegation the harness accepted before a crash is read back, not run again.
Where the guarantee comes from
DeepSeek Harness durably records execution intent and does not promise exactly-once effects. Its session checkpoint policy states the convention this plugin implements:
"The policy durably records execution intent, not generic exactly-once effects. Side-effecting tools should forward
exec.callIdas an idempotency key when their provider supports one." —@deepseek-ai/dsh-session-checkpoint-policy
pi-durable-subagents accepts exactly that key as --request <id>: the first submission under an id is decided once, and every later retry carrying the same content gets that first outcome. So the harness supplies durable intent plus the key, and the durable orchestrator supplies the single decision.
The join is a read-before-write state machine. Every attempt reads describe --key <id> first and only submits when the id is undecided, so a process that died between deciding a submission and reporting it leaves a durable decision that the next attempt reads instead of executing. The keyless test suite pins that path; the traced CLI invocation sequence is describe → run → describe with one execution recorded.
Install
dsh plugin --profile web add dsh-plugin-durable-subagent
The bundle patch inserts one row, durable-subagent. It requires pi-durable-subagents on PATH (npm i -g pi-durable-subagents), or a command that names it; without one, every start fails with a message saying so rather than retrying.
Config
| Field | Default | Meaning |
|---|---|---|
provider |
durable |
Registry name on ctx.subagents. |
command |
pi-durable-subagents |
Executable, argv[0] only. |
commandArgs |
[] |
Arguments before the subcommand, for a launcher rather than a bare executable. |
home |
— | DSA_HOME for every invocation. |
agent |
worker |
The pi agent definition a delegation runs as unless the call names another. |
cwd |
the harness process's working directory | Working directory of every invocation. A run's cwd is part of the content the CLI hashes. |
idempotency |
unique |
How the provider mints request ids: unique (one per start) or content (one per identical delegation). The tool always uses the call identity. |
attempts |
3 |
Submission attempts before a start fails. |
settleTimeoutMs |
600000 |
How long a run handle waits for the durable verdict. |
pollIntervalMs |
1000 |
Poll interval while waiting. |
timeoutMs |
120000 |
Per-invocation deadline for the CLI. |
enableTool |
true |
Register the model-facing tool. |
tool |
durable_subagent |
Model-facing tool name. |
How a call becomes a durable request
- The harness checks the call's intent durably, then runs the tool body with
exec.callId. - The tool derives a request id from the calling session id and
exec.callId, so a replay of the same call re-derives the same id. - The plugin reads the id's durable state. Only
absentandpendingproceed torun;runningandaskingreturn as they are, andsealed,rejected, andprunedare terminal. run --request <id> --spec - --cwd <cwd> --jsoneither decides the request, returns the first outcome, or reports75(not decided yet), in which case the next attempt reads the durable state again before resubmitting.
The provider path reaches ctx.subagents instead, where no call identity is available (ResolvedSubagentStartRequest carries none). It therefore mints ids from configuration: unique never conflicts but is not replay-stable, content is replay-stable but two identical delegations from one session collide as request-conflict. Pick content only when the delegations are content-distinct.
Model Experience
The tool
durable_subagent takes action (run, status, answer, steer, notify, stop), plus task, agent, to, message, qid, and rev as each action needs.
run returns as soon as the delegation is decided, not when its work finishes, and reports sealed, running, pending, or asking with the request address. status reads that address again; an asking state carries the question id and revision that answer needs. Nothing is polled on the model's behalf, so a long delegation costs no tokens while it runs.
Token effect
A run reply is one short line. Reading a finished delegation returns its output; reading a running one returns a state name. No child transcript enters the parent's history.
KV cache effect
Tool calls append after the reusable history prefix. Nothing here rewrites earlier messages.
Known Limitations and Deferred Work
- A linked plugin resolves its imports from its own repository.
dsh plugin add <path>records alink:, and Node follows the symlink to the plugin's real path, so@deepseek-ai/*is resolved from the plugin directory and never from the profile. The local install must therefore carry a complete closure:npm installresolves this package's harness peers, whilenpm install --legacy-peer-depsleaves@deepseek-ai/dsh-toolswithout the nine packages it declares as peers, and the profile then fails to boot withfailed to import loader entry durable-subagent (dsh-plugin-durable-subagent): Cannot find package '@deepseek-ai/dsh-scope'. A published install from npm does not have this problem. cwdis part of the submission digest. A retry from a different working directory is arequest-conflictrather than the first outcome. The default is stable for the life of the harness process; a per-sessioncwdis not yet wired.- The provider cannot be replay-stable.
ResolvedSubagentStartRequestcarries no call identity, so only the tool path gets one execution per call. Making the provider path exact needs a durable run identity on the request; that is a harness change. dispose()stops a run but does not await quiescence. The orchestrator is a separate process and owns its own teardown, so the run handle cannot observe the child's exit.- No continuable children and no Agent Teams membership.
prepareContinuableis deliberately not implemented:spawn_teammateand cold resume need a durable child session this provider does not create. - No tool filtering, persona, or delegation depth. The provider reports
outputSchemaonly; a request needing another capability is rejected loudly by the subagent service rather than silently ignored. outputSchemavalidation is delegated.pi-durable-subagentsvalidates areportagainst the schema it was handed before sealing, so a sealeddatapayload is schema-valid; the plugin does not re-validate it.- The orchestrator is not supervised. The plugin neither starts nor restarts
pi-durable-subagents; it relies on the orchestrator's own lazy start, lock, and idle exit. Running it underctx.subprocesswould make harness disposal kill it. - A failure is per call. A workflow reports
status: "done"even when every call inside it failed, andoutput/errorlive on the calls.summarize()reads them there; the read-onlystatusview alone would call a failed delegation completed.
Verification
Phase 0 exercised the real pi-durable-subagents 1.0.39 CLI against a real orchestrator, and mounted the bundle into a real DeepSeek Harness profile.
| Check | Result |
|---|---|
npm run typecheck against published @deepseek-ai/dsh-subagent/dsh-tools 0.1.1-rc.2, cordis 4.x, schemastery 3.x |
clean |
node --test keyless suite (fixture CLI, no harness and no pi) |
32 passed |
dsh-plugin-doctor |
10 passed, 0 errors; remaining items are a new repository's age and remote |
npm run build + npm pack --dry-run |
emits lib/*.js + lib/types/*.d.ts; 22 files |
describe --key <id> --json reply shapes |
confirmed against the real CLI; payloads pinned in test/fixtures/real-replies.json |
converge() against the real CLI, after a "crashed" caller left a durable decision |
one read, zero submissions; the retry returned the first wid |
| Identical resubmission under one id | exit 0 with the same wid and created: false |
| Same id, different content | exit 3 request-conflict, state echoed |
Bundle installed with dsh plugin --profile web add, composed tree |
row durable-subagent present with its config |
| Headless boot with the plugin mounted | loader entries activate; the run stops at MISSING_CREDENTIAL, not at the plugin |
A model-driven durable_subagent call in a live session |
verified end to end: run → status ×2 → sealed, addressed by request |
| The same call driven by the wid instead of the request id | returns absent; the render now leads with request= for this reason |
A real pi 1.0.2 child completing a model call |
not verified: the throwaway pi had no hydrated model catalog and reported Unknown provider: unknown |
| An orchestrator crash between decision and execution | not verified; the crash window tested is the caller's |
Phase 0 evidence
Submitting one id twice against the real CLI:
$ pi-durable-subagents run --request spike-1 --spec spec.json --json
{"request":"spike-1","wid":"01M4THR3CMT54R65BASRA9QX3V","created":true,"spec_digest":"3a8fdf42…"}
$ pi-durable-subagents run --request spike-1 --spec spec.json --json
{"request":"spike-1","wid":"01M4THR3CMT54R65BASRA9QX3V","created":false,"spec_digest":"3a8fdf42…"}
Driving this plugin's own converge() at the real CLI through a shim that logs every invocation, where the first caller submitted and then "died" without reading its reply:
CLI invocations: run -> describe -> describe -> run -> describe -> describe -> run
converge after the crash: running (wid 01M4THR3CMT54R65BASRA9QX3V)
The three run calls are the crashed caller, one fresh-id convergence, and a deliberate conflicting probe. converge issued none of them for the already-decided id.
A workflow whose only call failed reports status: "done":
$ pi-durable-subagents describe --key spike-1 --json
{"state":"sealed","status":"done","calls":[{"status":"failed","ok":false,"error":"lost ×5","output":""}]}
This is why summarize() reads success per call. The run cost nothing: usage was 0 in and 0 out.
Phase 0b evidence
A real DeepSeek Harness headless session, with the bundle mounted and a real pi-durable-subagents 1.0.39 CLI on PATH, was asked to delegate one task. Its own report:
- Request id: df9631829b6-call-00-xnXLGCeOhmS8M4PETs0m0219-e847ef97bab53919
- Final state: sealed
- Succeeded: No — the delegation failed.
- Output / error (verbatim):
sealed: request=df9631829b6-…-e847ef97bab53919 (wid 01M4TJKGB6Q3ZX7XNCKW5DX2PG) —
failed: lost ×5; last error: Unknown provider: unknown
Three things came out of this run. The handle is addressable: the model read request= from the render and every status call resolved, where the first attempt (which rendered only the wid) got absent four times. The call-level failure mapping works in production: a workflow whose only call died is reported as failed with the child's own error, not as a completed delegation with empty output. And the request id is now on-grammar — the tool call id was call_00_zkMrBAcVpchoWOd2TzA93108, whose underscores are outside [A-Za-z0-9][A-Za-z0-9._:-]*.
The child itself never reached a model: the throwaway pi 1.0.2 installation had no hydrated model catalog, so it started as unknown/unknown. pi auth check --provider deepseek reported invalid_state both before and after DEEPSEEK_API_KEY was supplied from the harness credential store, which is a property of that fresh installation rather than of this plugin.
License
MIT. pi-durable-subagents is MIT, © purboo, and adapts parts of pi-subagents (MIT, © Nico Bailon), which this plugin neither vendors nor redistributes. DeepSeek Harness is MIT.
No comments yet. Be the first to write one.