DSH Plugins Marketplace

DSH Plugins

Plugins

/

Development & Infrastructure

/

dsh-proactive

m

dsh-proactive

Manifest valid★ 1

DSH plugin: wakes the agent when a long background task (download, install, wait) finishes after the turn has ended, so it reports back on its own.

hasBundlePatch

dsh-proactive · the night watchman

A DSH plugin that lets the agent speak up on its own when a long job finishes after its turn has already ended — no more coming back to ask "is it done yet?".

tests license

中文说明 →


What it does

You ask the agent to do something that takes hours — pull down a 40 GB model, wait for a compile, sit through an installer. The agent starts the job, the turn ends, and it goes to sleep. The job is still running. Nothing tells the agent when it finishes, so nothing tells you either: you have to wander back and ask.

This plugin makes DSH watch the condition for you. When it is met, the plugin injects one message into the session and wakes the agent — so the agent reports to you by itself.

you:      fetch that 20 GB thing and tell me when it's done
agent:    (starts the download → turn ends → asleep)
            ...hours later...
plugin:   *sees the file size stop changing* → *injects a message* → *wakes the agent*
agent:    "Done — 20.3 GB, and I verified the checksum against the published one."

Why it exists

An agent is a request/response process. Once a turn is over it has no clock, no timer and no way to learn that a curl it launched has exited or that a file has appeared on disk. Under DSH the only thing that can hand it that fact is a message arriving in its inbox — which is exactly what a session looks like when you type something. If nothing sends one, the agent stays asleep until a human pokes it.

So the missing piece is not the waiting, it is the waking. This plugin is the thing that wakes it.

Mechanics

The wake-up uses the same call DSH itself uses when a subagent reports back to its parent agent (see dsh-subagent/lib/continuation.js):

agent.steer(createUserMessage({
    content: [{ type: 'text', text: message }],
    source: { kind: 'plugin', plugin: 'dsh-proactive' },
}))

agent.steer(...) is the door into a session that is not currently running a turn; createUserMessage(...) builds the message object that door expects. This is DSH's own API surface, not a private back channel.

source.kind = 'plugin' is the default, and it matters: a plugin-originated message is not rendered in the UI. You do not see a fake bubble pretending to be something you typed — you only see the agent's reply, which is the whole point. source.kind = 'user' is available per-entry if you deliberately want the message rendered as a user bubble, and it is used by the test suite to prove the difference is real.

Trigger types

typeFires whenRequired fields
file-stableThe file size is unchanged across rounds consecutive checks — the standard "download finished" testpath, rounds (default stableRounds, 3)
file-existsThe path exists as a file (a directory does not count)path
file-sizeThe file exists and its size is ≥ bytespath, bytes (default 0)
process-exitThe named process is gone — the job it was doing is overprocess (e.g. curl.exe)
port-openA TCP connection to host:port succeeds — the service came upport, host (default 127.0.0.1)
timeThe clock passes at — an alarmat (ISO date-time; bare = local, Z = UTC)

Unknown types never fire; they are skipped silently rather than guessed at.

Common fields

FieldMeaning
idIdentifies the entry in boot.log and in the default message. Any string.
messageWhat the agent reads when it wakes. Write it as an instruction to the agent, not as a note to yourself.
sessionIdOptional. Wake only this session. If it is not open, the entry is skipped for that tick and stays armed — it is never redirected to another session. Omit to wake the most recently active one.
sourceKindOptional. plugin (default, not rendered) or user (rendered as a user bubble).
formOptional, with sourceKind: 'plugin'. Free-form tag on the source; notice also carries a summary (first 120 chars of message).
repeatSecondsOptional. Re-arm instead of firing once — see below.

Which session gets woken

When several DSH sessions are open, exactly one is chosen:

  1. sessionId set → only that session. If it does not exist the entry is skipped, logged, and left armed. Refusing to fire is deliberate: a reminder delivered to the wrong session is worse than no reminder.
  2. sessionId absent → the most recently active session. Activity means a message arrived in its inbox, its status turned running, or the session started. Not "the last one registered" — that older rule woke the wrong session when two were open, and it is pinned by five tests (C. session selection).

One-shot versus repeating

A fired entry is stamped fired: true and never fires again. With repeatSeconds the entry is re-armed instead, and firedCount / lastFiredAt record the history:

  • time → at moves to now + repeatSeconds.
  • file-size → bytes is raised to the current file size + 1, so the next byte appended fires it again.
  • Any other type → repeatSeconds is meaningless and the entry silently degrades to one-shot.

Entries whose files are actually a moving target (a growing log, a progress file) are what repeatSeconds is for.

Install

The plugin imports @deepseek-ai/dsh-llm, so it has to sit somewhere Node can resolve that package from — a normal place for that is inside the profile's module tree.

# 1. put the plugin next to (or inside) your DSH profile
git clone https://github.com/mili1360/dsh-proactive.git ./dsh-proactive

# 2. register it in <profile>/package.json
#    dependencies:        "dsh-proactive": "link:<absolute path to the plugin>"
#    dsh.profile.bundles: [ "dsh-proactive" ]      <- add to the array

Then verify the import resolves from the plugin directory:

node -e "import('@deepseek-ai/dsh-llm').then(m => console.log('createUserMessage =', typeof m.createUserMessage))"
# createUserMessage = function

If that fails, link the package the way DSH exposes it — for example a node_modules/@deepseek-ai junction inside the plugin directory pointing at <profile>/node_modules/@deepseek-ai.

Restart DSH. dsh.marketplace.requiresRestart is true, and a newly registered bundle is not picked up without one. Afterwards the plugin directory contains boot.log, and three lines mean it loaded:

apply() 被调用  enabled=true  intervalMs=15000
定时器已挂载
守夜人就绪

No boot.log means the bundle never loaded — check the profile registration again.

Requirements / compatibility

After installing, confirm it actually works — a profile that boots is not proof. A plugin can load cleanly, serve its routes and still break the host; we have seen exactly that. So the acceptance test is: create a new session and send it one message (the plugin is idle until a watch fires, so a quiet log is expected), then confirm the session exists and the agent answered. If new sessions cannot be created at all, remove the mount line above and restart.

Developed and tested against DSH 0.1.0-rc.7 — a customised build, not a stock upstream release. Another version may register its services or name its events differently: this plugin uses ctx.on, ctx.effect, ctx.agents, the agent/created, agent/session-start, agent/inbox/inserted, agent/status and agent/disposed events, and agent.steer + createUserMessage from @deepseek-ai/dsh-llm.

If your build does not match, the plugin deactivates and says so — it does not stop the profile from booting. Nothing keeps running (no listeners, no timer), and boot.log gets ONE line instead of the ready line:

注册失败,插件停用: ctx.on 不可用(这个 DSH build 没有它)

If the profile will not start at all after installing, remove the mounted lines from <profile>/cordis.patch.yml (or drop "dsh-proactive" from dsh.profile.bundles, if that is how you installed it) and restart:

- insert:
    - id: dsh-proactive
      name: dsh-proactive
      config:
        enabled: true
        intervalMs: 15000
        stableRounds: 3
        maxRetries: 5

Configuration

Set through the bundle patch (cordis.patch.yml) or the profile's own plugin config:

KeyDefaultMeaning
enabledtruefalse skips loading entirely — no timer is even created
intervalMs15000How often watch.json is re-read and evaluated. Floored at 3000.
stableRounds3Default rounds for file-stable
maxRetries5Failed wake-ups tolerated before the entry is abandoned
sourceKind'plugin'Default source kind for injected messages
watchFile<plugin dir>/watch.jsonOverride the watch list path (used by the test suite for isolation)
logFile<plugin dir>/boot.logOverride the log path (same)

There is no built-in default watch, no default path and no bundled reminder: with an empty watches array the plugin does nothing at all. watch.example.json is a template with every type filled in — copy it to watch.json and edit.

Watch lists are read from disk on every tick, so editing watch.json takes effect within one interval. No restart. When the plugin writes the file back it preserves every other top-level key, so your own _note / _README comment keys survive.

Safety model

SituationWhat happens
A condition is met while the agent is mid-turnagent.steer(...) is called anyway. The message lands in the session; it does not interrupt or abort the running turn. The agent sees it when it next looks at its inbox.
DSH restartsThe timer is gone but watch.json is not: entries that had not fired are re-read on the next boot and keep watching. Entries already marked fired stay fired. There is no separate state database.
A watch fires repeatedlyOnly with an explicit repeatSeconds, and only on time / file-size, which re-arm by moving their own threshold forward. Everything else fires exactly once and is stamped fired: true.
No session is available (all closed, or the pinned sessionId is gone)The wake-up is refused, retries is incremented, and the entry stays armed. It is retried on later ticks up to maxRetries, then marked fired and abandoned with a line in boot.log. It is never delivered to a different session.
agent.steer throwsCaught, logged, counted as a retry. Nothing propagates.
watch.json is missing, empty, a bare array, or corrupt JSONA missing file is created; a bare array is accepted as the watch list; corrupt JSON reads as "no watches this tick" and is retried next tick. None of these throw.
Anything else failsEvery registration in apply() is guarded. A missing or refusing ctx.on / ctx.effect logs ONE line (注册失败,插件停用: …) and deactivates the plugin — no listener or timer is left behind. A failure in this plugin cannot stop DSH from booting — the worst case is a line in boot.log.

Two more properties worth stating plainly:

  • It cannot fake your input by default. Injected messages are tagged plugin, which the UI does not render as something you said.
  • It does not touch anything but its own two files. The plugin reads watch.json, writes watch.json and appends to boot.log. It does not read, write or delete session logs, credentials, or any other DSH state.

Test suite

npm test          # node test/test.mjs

68 cases, all green, in two layers plus a session-selection group:

  • A. checkCondition unit tests — every type at its boundaries: file present / absent / a directory, size at and below the threshold, size stable / growing / vanishing mid-watch, process present and absent, port listening and closed, time in the past, in the future and unparseable, unknown type, missing fields.
  • B. apply() integration tests — a fake ctx and fake agents driving the real code path: fire and delivery, fired de-duplication, retry counting without a session, a throwing steer, giving up past maxRetries, corrupt JSON, the bare-array format, the auto-created watch file, enabled: false, the deactivation path when ctx.effect is missing or refuses the registration, a hostile ctx (on throwing, on absent, ctx null) that must not throw and must not leave a timer running, sourceKind overrides, the default message, delivery after dispose, an empty list, and preservation of non-watch keys on write-back.
  • C. Session selection — pinned session honoured, pinned-but-absent session not misdelivered, most-recently-active chosen over most-recently-registered, status: running counting as activity, and a disposed session never being woken.

Everything runs in a temp directory; the real watch.json and boot.log are never touched. The suite takes about 90 seconds, because proving that something does not fire means waiting out a full timer window.

Bugs the suite caught and that are fixed in this tree: the file-stable counter counted the first observation as a baseline, so rounds: 3 actually needed four checks; the write-back threw when the watch file's directory did not exist; and session selection used registration order rather than activity, which woke the wrong session when two were open.

Distribution helper

npm run dist        # writes dist/
npm run verify:dist # 25 checks against dist/
npm run check       # dist + test + verify:dist

npm run dist produces a single self-contained Markdown file — all source embedded, plus installation instructions written for another agent to follow — and a zip of the same sources. Because the packager reads the live source files, the package cannot go stale relative to the code.

npm run verify:dist plays the recipient's agent: it pulls every fenced block back out of the Markdown, compares each one byte-for-byte (SHA-256) against its source file, restores the whole plugin into a temp directory, runs node --check and the full test suite on the restored copy, and asserts that the instructions still contain the sections an installing agent needs.

Known limitations

  • The bare-timer fallback is gone (Unreleased). Up to 0.2.1, a failing ctx.effect fell back to a plain setInterval that lived until the process exited. That fallback was removed: a plugin that cannot register is now deactivated with one line in boot.log, so a plugin instance can always be fully unloaded.
  • watch.json is module-level state. The path is shared across apply() calls in one process, so two loaded instances would fight over it. Fine for one plugin per DSH process — which is the supported shape.
  • file-exists ignores directories on purpose: "the directory exists" is not the same claim as "the download finished".
  • process-exit relies on tasklist on Windows. Where the process lookup fails, the condition reports "unknown" and the entry is skipped for that tick instead of firing — a missing tasklist cannot cause a false positive.
  • file-stable detects "size stopped changing", not "transfer completed" — a stalled download looks identical to a finished one. Use file-size with a known expected size when it matters.
  • Watching is poll-based. With intervalMs: 15000 a condition can be met up to one interval before the agent is woken. Shorter intervals cost CPU; the floor is 3000 ms.

License

MIT — see LICENSE.

Comments

Loading…

From the same category

awesome-dsh-plugin

by awesome-dsh-plugin

A curated list of plugins for DeepSeek Harness (dsh) · DeepSeek Harness 插件精选列表

Development & Infrastructure

★ 18.5k

CC0-1.0

Python

Oct 12, 2026

Index only — not installable

by 0xsline

DeepSeek Harness (DSH) ecosystem: curated plugins, tools, and infrastructure from dsh-external/hub and the public dsh-plugin topic.

Development & Infrastructure

★ 1.2k

CC0-1.0

Python

Oct 10, 2026

Index only — not installable

by pax-beehive

Open-source CLI, schemas, resolver, and DSH agent tools for DSH Plugin Hub

Development & Infrastructure

★ 400

MIT

TypeScript

Oct 6, 2026

Index only — not installable

by xiajiajun516

DeepSeek Harness (DSH) backup & restore plugin — export, import, migrate and sync your complete DSH configuration, plugins, MCP servers, skills and workspace. One-click migration to another machine.

Development & InfrastructureManifest valid

★ 185

MIT

TypeScript

Oct 13, 2026

dsh plugin --profile web add dsh-config-manager

by yjh051108

推荐组件(非必须):DeepSeek Harness 运行时注入器;已随 dsh-routing-suite 单仓库化保留,本仓库继续维护/发布。

Development & InfrastructureManifest valid

★ 166

TypeScript

Sep 18, 2026

dsh plugin --profile web add @dsh-external/dsh-super-injector

by jigjoy-ai

A CLI that turns a goal into a pull request - and a sandbox for testing concurrent AI coding agents on the Mozaik runtime.

Development & Infrastructure

★ 124

MIT

TypeScript

Oct 12, 2026

Index only — not installable