Senguk520/dsh-multitask

dsh-multitask

Multitask 减震器模式(Agent preset):主对话只做需求理解、任务下发与结果汇总,读文件、跑命令、搜索与构建全部下放子代理;含四类 worker、危险操作审批闸门,且零 DSH 源码改动。

Uncategorizedagent-presetcontext-engineeringcontext-management
dsh-multitask

dsh-multitask

The Multitask "shock absorber" mode for DeepSeek Harness — the main conversation never touches your codebase.

English · 中文 · Design notes and measured evidence · MIT License

A work mode for DeepSeek Harness(DSH) in which the main conversation does exactly three things: understand what you asked for, turn it into task briefs, and report back what the Subagents concluded. Every piece of process data — file contents, command output, build logs, search results, dead ends — is digested inside a Subagent's context and never enters the main one. The main conversation therefore holds only high-value information.

This is a mechanism, not a prompt asking the model to behave. Once the mode is selected, the main conversation (the coordinator) genuinely has a narrower tool surface: the tools that would drag in process data — writing files, running commands, searching the web — are not in its visible set. It cannot call them, so it cannot pollute its own context. A Subagent (worker) is the opposite: it gets the full tool surface, does the work, and returns only its conclusion.

Zero runtime dependencies. No changes to DSH source.

At a glance:

CapabilityWhat it gives you
Narrowed coordinator tool surfacethe main conversation cannot write, run commands or search — those tools are genuinely absent, not merely discouraged
Process data never reaches the main conversationfile contents, command output, build logs and dead ends stay inside a worker's context
Four kinds of Subagenta full-surface worker, a fork that inherits completed turns, a programmatic PTC worker and a read-only worker, chosen by task shape
Dispatch to another modehand a brief to any other installed agent preset and receive only its conclusion
Two-level approval gatesafe commands run untouched; dangerous ones raise a card in the main conversation
Corrections take effect immediatelya follow-up message stops the worker's current turn first instead of queueing behind the wrong work
Mode isolationseveral modes can share one workspace; each narrows only its own sessions
Cross-session settingspick a model per worker kind and set the parallelism limit, both in Settings
Automatic workspace permission repairon Windows the one missing permission is added at session start, and only that one
Read-only metadata tool file_infobyte counts and file kinds without reading contents, so the coordinator can verify a claim itself
Zero runtime dependenciesno changes to DSH source

How it differs from other modes

Main conversation (coordinator)Subagent (worker)
Tool surfaceread-only checks + delegation + result retrieval + interactionfull (write files, run commands, build, search…)
Contextyour request + task briefs + conclusionsall the process data
Jobunderstand, translate, dispatch, verify, reportdo the work

What the coordinator keeps (a whitelist):

  • Read-only checks — read grep glob file_info
  • Delegation — subagent subagent_fork subagent_ptc subagent_minimal
  • Subagent control — send_message interrupt_agent list_agents
  • Result retrieval — job_list job_output job_kill
  • Orchestration — workflow
  • Interaction and self-management — ask_user_question todo_write create_goal get_goal update_goal exit_plan_mode present skill

Anything not on that list is withheld by default — write, edit, pwsh, web_search, web_fetch, read_image, ssh_*, task_board_*, plugin_manager, and so on. The rule is a complement: withheld = currently visible − kept. Any tool the host adds later is therefore also withheld from the coordinator by default, with no change needed in this plugin.

The read-only tools are kept on purpose: the coordinator is meant to verify a Subagent's claims independently instead of trusting them blindly.

Mode isolation: it only narrows its own sessions

The narrowing is applied only to sessions whose live preset binding is this preset. A session belonging to another mode in the same workspace keeps its own tool roster and is not touched — so you can run several modes side by side without them eating each other's tools.

Switching a session out of the mode lifts the restriction again, and switching into it before the first turn narrows it before the first request is even made.

presetId is that ownership key. If you change it to install variants side by side, each variant narrows only its own sessions; and if it no longer matches the registered preset id, no session is narrowed at all — the mode can look installed while quietly doing nothing.

When ownership cannot be read, the guard prefers not to narrow rather than risk narrowing a neighbour's session. The cost of a registry hiccup is therefore one possibly un-narrowed turn, never a broken neighbouring mode.


Installation

dsh plugin --profile <profile> add dsh-multitask

--profile is an option and cannot be dropped — dsh plugin add dsh-multitask fails outright with error: required option '--profile <name>' not specified. <profile> is the profile to install into: the desktop app owns one called desktop, and this checkout commonly uses web; if you are unsure, look at the directories under ~/.dsh/profiles/.

The package then lands in that profile's node_modules, and DSH automatically appends dsh-multitask to the profile's dsh.profile.bundles layer list (because this package declares dsh.bundle.patch). The bundled cordis.patch.yml inserts a bare package name with no absolute paths — so it works on any machine, any user name, any install location. Restart the app afterwards — bundle layers are composed at startup.

You can also install it from the in-app Plugins page, which needs no command line.

Desktop (Electron) note. Fully quit the app first — pnpm rewrites the profile's node_modules, and the running app holding those files open will make it fail. The desktop app's own dsh launcher can manage desktop; a dsh installed separately from npm is refused outright (error: profile "desktop" is managed exclusively by the Electron application). In that case use the in-app Plugins page, or the source route below.

From a source checkout

To run the sources in your working tree (edits without going through npm), append this block to the profile's cordis.patch.yml, pointing at your local entry with an absolute file:// URL:

- insert:
    - id: multitask
      name: 'file:///absolute/path/to/dsh-multitask/lib/index.js?v=6'

Two things to watch:

  • Spaces and % in the path must be percent-encoded (common on Windows). Let a program encode it rather than typing it by hand — in PowerShell: (New-Object System.Uri('H:\my repo\dsh-multitask\lib\index.js')).AbsoluteUri.
  • The trailing ?v=N is a cache key, and it only means anything in this file:// mode (see Development → "Making source edits take effect").

Adding this row needs no restart: DSH watches cordis.patch.yml and hot-reloads it.


Usage

When creating a new session, pick "Multitask 减震器模式" in the mode selector, then just ask for what you want.

The mode can be chosen before a session's first turn starts, and a session whose first turn has not yet started can still be switched into or out of it. What is frozen is a session that has already started running its first turn — that is an existing DSH constraint (the tool surface and prompt freeze on the first turn), not a limitation of this plugin.

The coordinator translates your request into self-contained task briefs — a Subagent cannot see this conversation, so a brief must carry everything it needs (goal, verified current state, project conventions, safety rails, numbered tasks, conditional branches, stop conditions, report format). Independent briefs are dispatched in parallel, and the coordinator keeps working while they run.


The four kinds of Subagent

The coordinator has four delegation paths, and the difference is mainly the tool surface:

Delegation toolTool surfaceBest for
subagentfullthe default — writing code, running tests, editing files, execution work
subagent_forkfull + the completed turns of this conversation — it does not see the turn currently in flight, so a fact stated in the dispatching turn must be repeated in the briefreview and continuation — when the work must build on what is already established here
subagent_ptcPTC-collapsed: only run_code is directly callable; every other tool is reached from inside a program via a generated SDKprogrammatic bulk work — filtering, aggregating, or moving large data sets whose intermediate values should never enter the context
subagent_minimalread-only: write and execute tools are removed, but it keeps read grep glob read_image file_info skill todo_write present and can still search and fetch the webinvestigation, quick answers, fact-checking

Pick by the shape of the task, not by habit. The four paths have different tool surfaces, and the choice is made per delegation in the coordinator persona (section ## Choosing which worker to delegate to).

Delegation is one level deep: coordinator → worker, and a worker cannot delegate further. Jobs that need splitting are split by the coordinator into flat briefs.

What a worker does not get

The four kinds also differ in persona, not only in tool surface. A fork worker is told that the seeded turns are the coordinator's work and not its own, so it reviews instead of pretending to have done it. A PTC worker is told that its closing message is the fallback channel if run_code breaks. The read-only worker is told it may search and fetch the web, so it does not report back that it "cannot look anything up".

On top of that, five delegation tools — subagent, subagent_fork, subagent_ptc, subagent_minimal, subagent_preset — are removed from a worker's tool catalog outright, for every kind except the read-only worker. A worker therefore cannot delegate: the tool is not there rather than present-and-refused. This costs no capability, because the host's fixed depth of 1 would refuse every such call anyway; it only stops that schema being re-sent on every single request. The coordinator is untouched by this, and the read-only worker is narrowed by its own allowlist instead.

This is not a security boundary — the real block is the host's depth limit. If it fails to install it degrades silently, leaving only a log line containing WORKER-GUARD-SKIPPED, and nothing is lost functionally. Its switch is the worker-guard row's config.enabled in presets/multitask/composition.mjs (which also accepts an additive config.extraDeny), and changing it requires a restart.

If a worker's persona cannot be installed at all, that worker keeps the coordinator persona and the log shows WORKER-PERSONA-SKIPPED.

Corrections stop the worker first

When the coordinator sends a follow-up to a running worker, the target's current turn is stopped first, and only then is the message delivered — it does not queue until the next step boundary. This matters because a single step can be one long tool call: a build, a dependency pull, a recursive scan. A correction that merely queued would let the wrong instruction run to completion before your new one was even read.

Stopping is not "the task was cancelled". The inbox and any descendants are kept, every fact already accepted is still in the session log, and only the one in-flight action is dropped. A correction to work that was on the right track therefore loses almost nothing — and the message may carry information the worker could not have had.

It only ever acts in the coordinator → worker direction. A message that a worker sends upward is not preceded by an interrupt, because that would cut the main conversation's own turn. If the interrupt fails, or the deployment exposes no interrupt capability, the message is still sent normally and corrections simply revert to queueing: you see an info line, not an error. Its only switch is the interrupt-before-send row's config.enabled in the composition, and changing it requires a restart.

The read-only file_info tool

It is this package's own tool and the only self-authored tool on the coordinator's keep list. Its input is exactly one field: paths, an array of one or more path strings (up to 256), resolved by the filesystem backend with relative paths taken against the session working directory.

The output is one entry per requested path — requestedPath, resolvedPath, status, sizeKnown, sizeBytes when known, and a note explaining an odd state — plus a totals block (requested, files, directories, others, missing, errors, bytes, unknownSizes, complete).

status is one of file, directory, other, missing or error. A directory is reported as a directory with no byte count. A missing or uninspectable path is reported as such and is never reported as 0 bytes: when the backend cannot supply a size, sizeKnown is false and sizeBytes is absent rather than fabricated as zero.

totals.bytes sums only the known sizes, unknownSizes counts the rest, and complete is true only when every requested path is a known-size regular file — so a partial sum never masquerades as a total.

It never reads file contents and never modifies anything: it only resolves and stats each path. That is exactly why it is safe to keep in a mode whose entire point is keeping data out of the main conversation, and why the coordinator can use it to check a worker's claim ("five files, this many bytes") instead of trusting the report. It also serves the read-only worker, and it is deliberately not affected by the DSH_MULTITASK_MULTI_MODE switch.

If the filesystem capability is unavailable at load time the tool is simply not registered — the model never sees a route that would fail — leaving a log line containing FILE-INFO-SKIPPED; success logs FILE-INFO-ENGAGED.


Dispatching to another mode

The four paths above are this mode's own workers. One more path reaches outside this mode:

Delegation toolTargetBest for
subagent_presetany installed agent preset (including third-party modes installed from the market)when the job is genuinely something that other mode exists to do

This is what makes the coordinator a layer between you and every installed mode: it picks one by reading what that mode says about itself, hands the brief over, and only receives the conclusion. The work happens in that mode's worker, not in your main conversation.

To use it, call with list: true first to read the roster (id, name, description, and whether each mode can currently compose a session), then choose by description. The description is the only reliable statement of a mode's capabilities — do not guess from the id. A mode marked CANNOT RUN is broken and must not be chosen.

Three boundaries:

  • The brief must be fully self-contained. A cross-mode worker may not even share this mode's tool conventions, so it needs the background written out even more completely than an ordinary worker.
  • You cannot dispatch to this mode itself. That would spawn a sub-coordinator, which the depth limit then refuses to let delegate — the call is rejected outright and points you at the four built-in paths.
  • Workers cannot dispatch further. subagent_preset belongs to the coordinator; the tool refuses a worker's call itself, with its own message ("A delegated worker cannot dispatch further (delegation depth is fixed at 1)"), before any depth limit is consulted — deliberately, so the error names the real cause instead of surfacing a bare depth 2 exceeds maxDepth 1 that looks unrelated. Splitting therefore always stays with the coordinator.

A fourth boundary (measured, and worth knowing): a cross-mode worker is not governed by this mode's guards.

In setup() a subagent is attached to the target mode's scope with bindScopeParent(childKey, targetGenerationKey), and events like agent/created and tools/pre-execute reach only listeners on that scope chain. A cross-mode worker's chain is [itself, target mode] — it does not contain this mode. That cuts both ways:

  • The good side: this mode's persona injection, read-only narrowing, and approval gate never leak into someone else's mode. A task you hand to a read-only mode will not be told "you have the full tool set" by this mode.
  • The side to keep in mind: the approval gate does not cover it either. Upstream pins a child session's approval policy to never, so a cross-mode worker's dangerous operations are refused outright (fail-closed — never silently allowed), but no card is raised to ask you. It is governed by the target mode; if that mode has guards of its own, they apply.

So cross-mode dispatching is a capability router, not a security proxy. Work that needs this mode's two-level gate should still go to one of the four built-in paths.

The provider is fixed to spawn and is not a configuration option: upstream requires a provider that can select another preset to declare the agentPreset capability and not inherit the parent's context. The built-in spawn satisfies both and fork does not. Hard-coding it avoids a class of capability error that would otherwise only surface at delegation time.


Settings

Once installed, DSH's settings gain the following.

The "Multitask" page — two cards

CardFieldWhat it does
Model used by Subagentsmultitask.subagentModels[]the default model for each of the four worker kinds. Either "use one model for every Subagent" or per-kind; the default is "inherit coordinator". Applies only to sessions created afterwards
Subagent parallelism limitsubagent.maxActiveSubagentsthe same field as the row in Settings → General, and it takes effect immediately

The "General" page — two rows (alongside the built-in language / appearance / developer-tools rows)

RowFieldScope
Maximum recursion depthsubagent.maxDepthhost-level: applies to every mode, not just Multitask
Subagent parallelism limitsubagent.maxActiveSubagentssame; and it is the same value as the card on the Multitask page

The session header also shows how many Subagents are currently running.

The model card's shape is derived, never stored: all four kinds pointing at one route means "use one model for every Subagent", anything else means per-kind. Switching the shape creates no dirty state.

How the settings page behaves

  • The Save / Discard bar at the bottom of the Multitask page controls only the model card. The parallelism limit writes instantly, so editing it does not light up Save — that is not a broken button.
  • Save and Discard stay greyed out until the model mapping actually changes.
  • If the settings were changed elsewhere, a banner appears telling you to discard your draft and try again.
  • The model dropdowns are populated from a live catalogue. While it loads you see a loading message; if it fails you see an error with a Retry button and the only choice is "Inherit coordinator"; and a saved model that is no longer advertised stays selectable with a "(saved but currently unavailable)" suffix, so you can still clear it.
  • In "use one model for every Subagent" mode the first option is the placeholder "Choose a model"; picking it resets all four kinds to inherit. In per-kind mode each dropdown starts with "Inherit coordinator (default)"; selecting it removes that kind's entry.
  • The two numeric rows (recursion depth, parallelism limit) accept digits only and are committed when the field loses focus or you press Enter — not on every keystroke. They also have − / + steppers; typing something invalid turns the box red with inline correction text and the value is not written.
  • "Maximum recursion depth" carries the hint "0 disables Subagents entirely". Setting it to 0 turns delegation off altogether — the parallelism limit, by contrast, has a floor of 1 and rejects 0.
  • A deployment that stores settings read-only shows a banner saying so and disables every control; a deployment that does not serve the settings namespace says exactly that instead of rendering an empty page.
  • All of this page's copy ships in both English and Simplified Chinese and follows the DSH UI language.
  • The settings page is not the whole story: enabled, presetId, order, guardEnabled, keep and extraDeny have no UI at all and are profile-YAML only — see "Tunables (profile)" below.

The session-header counter

  • The session header shows how many Subagents are currently running, in three truthful states: a count when one or more are running, an explicit "none running" when the number is zero, and an explicit "count unavailable" rather than a guess when the projection cannot be read.
  • It is hidden entirely inside a sub-session (a worker does not show a counter for its own siblings' children) and until the model catalogue has loaded.
  • It sits alongside the host's own counter rather than replacing it, so seeing two numbers is expected.

The Subagent parallelism limit

There is only one value: the host's subagent.maxActiveSubagents. It appears in two places in Settings (the card on the Multitask page, and a row in General), but both edit the same field — change it in either and the other follows immediately. This plugin adds no cap of its own, and there is no second layer cutting you off.

That field's default is 8: with no configuration, 8 is all you get. But 8 is a default, not a ceiling — whatever you write is the limit. To run N, set it to N — in eith