dsh-superagent is a public research prototype from DeepSeek: a proactive coding agent for one GitHub repository, packaged as a DeepSeek Harness bundle. Coding agents are not scarce. What makes this one worth reading is that it turns three questions into code: who may assign work, how far that work may go, and how it recovers after failure.
This report is based on a file-by-file reading of commit 8c32a9d (2026-10-31). The src tree is about 15,587 lines across 60 spec files. Every judgment points to a file and function. None relies on paper designs that the prototype does not implement.
Start with the conclusion: the novelty is in governance and orchestration. That means a Gate plus Mandate authorization model, follow-up ownership, observe-mode shadow decisions, a transactional effect queue, and a Ticket state that does not follow its Session. Model capability, sandboxing, and multi-repository collaboration are not its strengths.
Reading order: runtime shape → domain model → task flow → authorization → reliability → worker execution → Sources → model interfaces → what is reusable and what is not. Each section ends with code locations that can be checked.
Code teardown · dsh-superagent · commit 8c32a9d · 2026-11-04
The hard parts of a proactive agent are authorization, scope, and recovery: who may ask it to work, how far it may go, and how it resumes after failure. Writing code comes later. This teardown follows those four questions.
The shape: a plugin bundle inside DSH
dsh-superagent has no model, session, or tool runtime of its own. Every @deepseek-ai/dsh-* package is a peer dependency; the repository contributes orchestration only.
| Entry | Registration | Responsibility |
|---|---|---|
| Main plugin | apply() in src/index.ts:10 | Builds Runtime and registers the Cordis service orchestrator |
| GitHub Source | src/sources/github.ts:112 | Registers the kind and starts notification and repository polling |
| Feishu Source | src/sources/feishu.ts:132 | Registers the kind and starts live events, history backfill, and topic polling |
| Panel | DSH client bundle | Uses only /orchestrator/api/*, never imports host code |
cordis.patch.yml also defines two agent presets: dsh-superagent-worker for executors and dsh-superagent-router for the main Session. The worker keeps files, search, shell, web, skills, todo, and subagents; the router removes writes, web, and subagents, leaving the minimum needed to route.
That means it is not an app you start with node index.js. Reading it requires accepting DSH's session, tool, permission, and workspace model first.
Layers: three dependency rules
The tree has six layers: core, app, executor, sources, infra, and client. What matters is not the folders but three rules.
| Layer | Contains | Dependency rule |
|---|---|---|
| core | Domain types, Engine, five parts, ports | No imports outside itself; no I/O |
| app | Composition root, Store, Settings, Prompts, Sessions, Effects, Outbox, HTTP, tools | May depend on core and infra |
| executor | DshExecutor, slots, DSH event handling | Calls Engine through a narrow interface |
| sources | Source protocol, GitHub, Feishu, channels, readers | Never imports src/app |
| client | React panel, routes, copy | Types and pure values from the host only |
- Engine alone writes Triggers, Threads, Decisions, Tickets, and Assignments; a Source writes only its cursor and Thread meta.
- Callers receive
Pickviews such asEngineForSources,EngineForMainSession,EngineForPeople,EngineForSettings, andEngineForExecutor; methods outside a role do not exist in the type. coreperforms no I/O andsourcesnever depend back onapp, so the domain rules run against in-memory repositories (src/core/memory.ts).
Code: dependency rules in docs/007-architecture.md:255-274, role interfaces in src/core/engine.ts:261-270, ports in src/core/ports.ts.
Domain model: five aggregates, one main chain
The system has one main chain: Triggers enter a Thread, a batch on that Thread enters Eval, Eval produces a Decision, an accept produces a Mandate and a Ticket, and the Ticket binds an Assignment to an Executor.
- 01TriggerImmutable event, deterministic id,
threads[0]is the primary Thread - 02ThreadTimeline plus subscriptions; at most one Eval runs on a Thread
- 03EvalOne judgment over a batch; records strength, basis, deadline, and primary
- 04Decisionaccept, ignore, stop, or unavailable, with reasons and audit
- 05MandateImmutable authorization: strength, policyVersion, basis, actor, scope
- 06TicketUnit of work; holds Mandate, Channels, Inputs, Assignments, follows
- 07AssignmentBinds a Ticket to one Executor run; pending → running → ended
- 08Report / ReplyWorker progress and result; the main Session turns them into human-facing messages
| Concept | Location | Invariant |
|---|---|---|
| Trigger | src/core/model.ts:22-39 | One Trigger can start at most one Ticket |
| Thread | src/core/model.ts:157-182 | At most one Eval per Thread |
| Evaluation | src/core/model.ts:136-154 | The clock starts after the candidate reaches the Session |
| Mandate | src/core/model.ts:126-133 | Immutable after creation; only a stop or failed revalidation revokes it |
| Ticket | src/core/model.ts:277-308 | open → ended, resumable by a human message |
| Assignment | src/core/model.ts:218-233 | Only the latest may be pending or running |
Three shared predicates keep the rules in one place: followupOwners decides which Ticket owns a human message, waitsForPerson decides whether automated input is held, and ticketStage derives the panel stage. The five Engine parts reuse them instead of re-implementing the rules.
Task flow: from a message to a delivery
The task changes carrier and decision maker at every stage. Every later module implements this table.
| Stage | Carrier | Decision maker | Output |
|---|---|---|---|
| Ingress | Trigger | The Source's scope() | Stored or filtered |
| Evaluation | Candidate | Gate plus the main Session | Decision |
| Authorization | Decision | Engine rechecks the Policy | Mandate and Ticket |
| Execution | Assignment | Worker Session | Report or PR |
| Delivery | Report or Reply | Main Session | Human-facing message |
| Ending | ticket.ended | Source fact, worker, or person | outcome |
An allowed sender addresses the agent
Strength is explicit; the main Session routes it in one call with orchestrator_route: accept, attach, stop, or ignore, without a full assessment.
Not addressed directly, but possibly within a rule
The Gate marks it inferred; the main Session calls orchestrator_eval for a bounded thread window, optionally eval_read, then eval_decide with direction, rule assessments, and evidence.
- An Eval's deadline starts when the candidate reaches the main Session; an undelivered candidate never expires.
- An expired Eval requeues twice; the third expiry records
unavailable. A processing pause shifts the deadline forward. - A Trigger that already started a Ticket cannot start a second one; a repeated accept returns the original Ticket.
- After a Ticket ends, a new human message on its Thread resumes it under the same Mandate, with no new Eval.
Code: src/core/intake.ts:19-62, src/core/evals.ts:71-156, src/core/tickets.ts:37-56, src/core/model.ts:75-88.
Authorization: the model proposes, the host disposes
This is the heaviest part of the project. Authorization is deterministic code in three layers: Gate, admission table, and Mandate. It is not a prompt asking the model to follow rules, and the model proposes only inside that boundary.
| Gate order | Result |
|---|---|
| Non-human: has an actor and is on the Source's bot list | inferred at most |
| Human: on the allowed list and addressed | explicit |
| Human: look up the admission cell for the receiving scope | explicit / inferred / ignore |
| Arrives on a Thread that already has Tickets | follow-up: explicit for listed senders, inferred otherwise |
| Admission cell | Choices | Default |
|---|---|---|
| On the list, not addressing | inferred / ignore | inferred |
| Not on the list, addressing | explicit / inferred / ignore | ignore |
| Not on the list, not addressing | inferred / ignore | ignore |
Three accept modes decide the fate of inferred candidates: off produces none; observe assesses normally but records only a would_accept audit; auto actually creates Tickets. The default is observe: shadow decisions first.
A Mandate records its basis: source, receiving scope, row, sender, matched rules, and via (addressed, followup, admission, or bot). Before an ended Ticket resumes, revalidate checks that the Source still listens, the sender is still allowed, the admission cell has not weakened, and inferred work still has auto mode with its rules enabled.
One rule:External text, quotes, and repository files are untrusted data. They may influence what a message asks for, but cannot change who may authorize work or what the Policy is. After the model judges, the host checks the same Policy again.
Code: gate and revalidate in src/policy.ts:189-240, assessmentOutcome in src/policy.ts:40-61, policyEffects in src/policy.ts:143-165.
Runtime: a two-second clock and durable side effects
Runtime is the composition root and holds no domain rules. It manually builds Store, Setup, Prompts, Effects, SourceRegistry, Sessions, Settings, DshExecutor, MasterAssessor, Outbox, Engine, and Passes in dependency order. Because Executor and Assessor call back into Engine, they receive a getter rather than a constructed object.
| Mechanism | Behavior | Code |
|---|---|---|
| Passes | Every two seconds; one pass at a time, with requests during a pass coalesced into the next | src/app/passes.ts:28-61 |
| Effects | Enqueue inside the transaction, dispatch after commit; coalesces by kind/key and preserves queue position | src/app/effects.ts:61-66 |
| Store | One SQLite file; generated columns and partial indexes let a pass scan only active rows | src/app/store.ts:26-40 |
| Outbox | Orders by Ticket and conversation; a later message waits until an earlier one has failed three times | src/app/outbox.ts:39-51 |
| effect | Handler | Concurrency | Retry |
|---|---|---|---|
| assessment | MasterAssessor | 4 | 2s → 60s |
| launch | DshExecutor | free slots | 5s → 300s |
| delivery | DshExecutor | 4 | 2s → 60s |
| relay | Sessions | 1, ordered | 2s → 60s |
| notice | Sessions | 1, ordered | 2s → 60s |
| reply | Outbox | 1 | 5s → 300s |
Later is the key to this queue: it means the work cannot run now, such as while paused, while slots are full, or while a Source is not installed. It does not count as a failure; it simply reschedules after the initial delay, so normal backpressure never looks like an error.
Side effects land in the database first and execute after commit, so a crash leaves at most a pending row, never an action that happened without a record. That is the basis of recovery.
Execution: one Ticket, one Session, one directory
The worker runs in a DSH Session; its code is not in this repository. The host starts the Session, delivers messages, and collects reports. Investigation, implementation, testing, and PR creation happen in the worker model.
- 01Accept computes execution metadataShort name, directory, assignment text; creates only a pending Assignment, not the directory
- 02launch effectRecheck Ticket/Assignment → take a slot → prepare the directory (10-minute limit)
- 03Create or restore the Session
worker-<ticketId>, worker preset, role model, prompt/skills/tools, danger-full-access - 04First assignment messageAssignment up to 1,500 chars plus the source Trigger and batch; later messages carry only the delta
- 05Worker turnInvestigate, implement, run checks; long commands go to the background; each Input is visible at the next step
- 06reportprogress, waiting, ready, blocked, failed, or dropped; questions use attention
- 07End or continueready, PR close, dropped, or stop ends it; a human message can resume it
| Resource | Rule |
|---|---|
| Worker slots | maxWorkers defaults to 3; a human message or status request may use one extra slot; wakes outrank new launches |
| Background work | After a turn ends, background work holds the slot for up to 30 minutes; shell jobs are killed at the limit, subagents continue |
| Handoff | ready, attention, blocked, or failed hands off to a person and frees the slot immediately |
| Transient errors | Up to three tries: launch uses effect backoff; a worker turn continues after 15, 30, and 60 seconds |
| Report | Ticket effect |
|---|---|
progress | Updates the summary without interrupting work |
waiting | Idle, waiting for the next Trigger or Input |
ready | No follows → completed; with a PR → ends as merged or closed after the close |
blocked / failed | Enters a person stage and holds automated input |
dropped | Ends; if followed PRs already closed, takes that outcome |
Code: src/executor/dsh.ts:97-111, 247-285, 424-470; slots at dsh.ts:165-199; background and handoff at dsh.ts:588-672.
Sources: translating the outside world into Triggers
A Source is a pluggable event adapter. A kind declares its configuration schema, panel fields, listening scope, channel, thread reader, Policy part, environment variables, and health entries. The Registry owns instances, exclusive health claims, and Policy part merging.
| Source | Ingress | Dedupe and cursor | Failure handling |
|---|---|---|---|
| GitHub | Notifications, repository-wide Issue/PR/comment streams, default-branch CI, ticket PR checks, subscribed Threads | Cursor moves back five seconds plus deterministic ids; ETag/304 while the cursor is unchanged | A failed read stays on its Thread; three failed polls report a Source error; 410 is deleted, 404 is retried |
| Feishu | Live event stream, history backfill, chat-name refresh, subscribed topic polling | Live events and history share one Trigger rule; topics use cursors | The consumer reports every failure; topic polling keeps errors on the Thread |
A subscribed Thread keeps polling while it has a live Ticket or its last activity or end is within 14 days (POLL_HORIZON_MS). After that window, only ordinary notifications or live events bring new messages.
A Source decides what is pushed. Workers still read what they need with gh or lark-cli; subscriptions only decide which messages are pushed to a Ticket.
Code: the protocol in src/sources/kind.ts, pollRound in src/sources/github.ts:280-296, and the 14-day window in src/sources/poll.ts:6-15.
Model interfaces: 14 + 6 tools and an attention budget
The main Session and workers have separate tool surfaces and prompts. The host separates routing from implementation, so the main Session does not become a second engineer.
| Role | Tools | Key constraint |
|---|---|---|
| Main Session | orchestrator_read / route / eval / eval_read / eval_decide / reply / ticket_retry / request_status / note / trigger_requeue / thread_unsubscribe / settings_save / policy_sync / prompts_save | Does not read code or logs, does not solve worker blockers; shell waits are limited by noWait |
| Worker | orchestrator_read / ack / report / thread_unrelated / thread_follow / trigger_handback plus files, search, shell/jobs, web, skills, todo, and subagents | No direct IM; tool guard refuses calls after the Ticket ends |
Assessment context is deliberately bounded: the Trigger is capped at 4,000 characters; the window is the first five messages plus five on each side of the Trigger, with 1,200 characters for near neighbours, 400 for others, and 50-character stubs; full text requires orchestrator_eval_read by id.
Prompt assembly is fixed: shared → role → three team sections (Communication, Ticket scope, Delivery requirements) → subscriptions → replies → dispatch or work → admission → handoff → execution environment → Policy. Policy always comes last; teams may override built-in parts but not the generated sections.
Code: tools in src/app/tools/main.ts and executor.ts; assessment context in src/app/assessment.ts:36-37,183-220; prompt order in src/app/prompts.ts:102-118; drift checks in tests/model-surface.spec.ts.
What is novel: what is worth copying
Most parts come from mature fields: capability-based authorization, workflow engines, transactional outbox, and event sourcing. What is rare is the combination and the insistence on boundaries in an agent setting.
- Gate + Mandate + revalidation:It takes who may ask the agent to work out of the prompt and turns it into a versioned, auditable, revocable capability. The host rechecks after the model judges.
- Follow-up ownership:A human message on a Thread with a Ticket belongs to that Ticket's worker; the main Session may only stop it; the worker may hand it back once. This prevents duplicate dispatch and message stealing, the usual failure of multi-session agents.
- Observe-mode shadow decisions:Inferred candidates are assessed normally, but the result is recorded only as
would_accept; no Ticket is created. Rules are calibrated on real traffic before switching to auto. Rare in agent products, common in operations. - Transactional effect queue:Assessment, launch, delivery, relay, notice, and reply all land in SQLite before execution, each with its own concurrency, backoff, and ordering. Agent orchestration gets workflow-engine reliability with one database file.
- Ticket state independent of its Session:A Ticket can replace Assignments, stay idle after its Session exits, and be resumed by a human message under the same Mandate. The execution instance and the authorization object are separate, so recovery is written down instead of inferred from whether a Session still exists.
If you copy only three things: first, Gate + Mandate + revalidation; second, follow-up ownership and handback; third, observe plus would_accept. The rest is worth learning but is engineering you get by following the pattern.
Limits: what it explicitly does not solve
The README states that this is a research prototype, not production software, and that some paper designs are not implemented here. Stating the limits clearly matters more than treating it as a platform.
| Limit | Evidence | Consequence |
|---|---|---|
| Not a security sandbox | Workers run as the operator, with danger-full-access; shims can be bypassed by absolute path | Isolation is a default-path guardrail, not a permission boundary |
| Single machine, single database | One SQLite file, a directory lock, one process | No horizontal scaling, multi-repository support, or team isolation |
| Source abstraction is incomplete | Outbox, links, resources, PR notices, and repository reads still call GitHub or Feishu directly | A new Source still requires touching a few generic modules |
| Unbounded data growth | Triggers, Decisions, ended Tickets, outbox receipts, and prompt history are never pruned | Long-running deployments need their own archival or pruning |
| Judgment still comes from a model | Whether direction is settled and a rule matches are semantic judgments | Schemas and evidence fields reduce variance; they do not prove the judgment |
One overlooked cost: the main Session is deliberately kept from reading code. That keeps routing clean, but when an assessment needs technical feasibility, it relies on thread context and limited repository reads; mistakes surface later as a worker handback or drop.
Code index: where to start
With one hour, read in this order. Start with each file's JSDoc, then jump to the core method.
- model.tsDomain types and invariants; start with Trigger, Mandate, Ticket, and Assignment.
- engine.tsThe single write entry; read the role interfaces and each command's JSDoc.
- evals.tsBatching, gate, timeout, and Decision; understand how accept creates a Ticket.
- tickets.tsLifecycle, subscriptions, Inputs, resume/stop/retry; the densest part.
- dsh.tsWhere a Ticket becomes a Session: launch, delivery, slots, recovery.
- effects.tsThe durable effect queue; understand
Laterand backpressure. - policy.tsGate, admission, Mandate revalidation, and policyEffects.
- sources/kind.tsThe Source plugin protocol; read the fields and optional capabilities.
- tools/main.tsThe main Session's 14 tools; read alongside prompts/dispatch.md.
- prompts/What the model actually reads; start with shared, orchestrator, dispatch, work, and admission.
The tests are documentation too: tests/core-engine.spec.ts covers domain rules, tests/runtime.spec.ts runs the whole app on a simulated DSH host, and tests/model-surface.spec.ts checks the model-facing surface for drift.
Conclusion: use it as a reference architecture, not a platform
What makes dsh-superagent worth reading is that it decomposes proactivity into auditable steps: who triggers, who authorizes, who executes, and who decides the end. It wraps the model's judgment in deterministic code instead of asking for a smarter model.
It is not a production platform to copy wholesale: no real sandbox, no multi-tenancy, and no data pruning. As a reference architecture, however, it grounds the four hardest parts of a proactive agent (authorization, ownership, recovery, and attention budget) in concrete files and functions.
Reusable checklist
- Make authorization a Mandate, not a sentence in a prompt.
- Give every human message one owner and one handback.
- Run observe-mode shadow decisions before enabling auto.
- Persist every external action before executing it; recover from the table.
- Let the Ticket own the state; the Session is only an execution instance.