Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add bgjobs
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
README
English · 中文
Run commands outside the DSH process: jobs are handed to the Windows Task Scheduler service, so closing DSH or the web page does not stop them. A toast appears in the web UI when a job finishes; live output is always one refresh away; and when DSH is offline you can still manage jobs with the standalone CLI/GUI.
Built for long-running work — large downloads, batch scripts, compilation, data sync/export. Submit and walk away; check back anytime.
Feature overview
| Capability | Description |
|---|---|
| Runs outside DSH | Jobs are hosted via schtasks; DSH crashes/shutdowns don't matter |
| Live output panel | Floating panel (bottom-right) refreshes every second: draggable, minimizable to a bubble, collapsible to a job list, theme-aware; grouped by workspace, resizable. Rows show a live Runtime (growing while running, total time once done), and each field can be placed in the list / detail view / hidden |
| Manual cleanup | 🧹 click the cleanup icon to open the trash bar: drag a single finished job to delete, or bulk-clean (>24h only / all; follows the view filter) |
| Completion notice | Toast on exit (does not interrupt the session); optionally notify the creating agent (notify param) |
| In-session waiting | bgjob_wait lets the agent wait for results without hogging the conversation: unlimited by default (configurable via "Default wait timeout" in Settings), released immediately when a new inbound message yields or the user stops it; supports any-race and all-conjunctive (fail fast) modes |
| Reconnect & track | Auto-recovers after a DSH restart; old job ids can still be queried from disk |
| Offline management | CLI / GUI that don't need DSH: list / status / log / submit / kill / cleanup |
| Optional sandbox | bgjob_submit_pwsh optional sandbox constrains job file permissions to no more than the current session mode |
| MCP tool calls | bgjob_submit_mcp submits one MCP tool call as a background job (settings-page switch, off by default); register servers by hand or import them from the DSH config; each server has a "Pre-warm / Cold start / Disabled" three-state control (disabled servers refuse agent calls) |
| No residue | A finished job removes its own scheduled task; done jobs stay visible until you clean them up |
Install / uninstall
Prereqs: DSH (@deepseek-ai/dsh), PowerShell 7, and Node.js (^22.19.0 or ≥24), Windows. (Verified on DSH 0.1.2-rc.1 ~ 0.1.7-rc.2 · Windows 10 · PowerShell 7 · Node.js 24).
The MCP engine (
bgjob_submit_mcp) runs on the plugin's own Node dependencies:@modelcontextprotocol/sdkandyamlship with the package and are installed bydsh plugin add; a local source checkout needs onepnpm install. DSH's bundled Node is enough — nothing else to install.
Method A (recommended, npm release)
$pf="web"; dsh plugin --profile $pf add bgjobs || dsh plugin --profile $pf approve-builds koffi; dsh plugin --profile $pf add bgjobs && Write-Host "✓ bgjobs installed successfully!" -ForegroundColor Green
Replace
webwith your own profile name and paste the whole line into PowerShell (pwsh). The firstaddraisesERR_PNPM_IGNORED_BUILDS(koffi build script not approved);||then automatically runsapprove-buildsto approve and build koffi, the secondaddsucceeds, and "bgjobs installed" is printed.
Method B (from GitHub, always latest)
$pf="web"; dsh plugin --profile $pf add github:bitsmug/dsh-bgjobs || dsh plugin --profile $pf approve-builds koffi; dsh plugin --profile $pf add github:bitsmug/bgjobs && Write-Host "✓ bgjobs installed successfully!" -ForegroundColor Green
Pulls the default branch directly from the GitHub repo — always the newest code (published and unpublished alike), no registry-lag. Both methods install under the name
bgjobs, so the uninstall command is the same.
From dsh-market install fails with ERR_PNPM_IGNORED_BUILDS?
The plugin depends on the native library koffi; installing it triggers a build script, and pnpm ≥10 blocks dependency build scripts by default (a GitHub install also runs prepare). The error looks like:
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: koffi@3.2.1
dsh: pnpm failed in profile directory <your DSH home>\profiles\<profile>
Fix (one-time fix):
$pf="web"; dsh plugin --profile $pf approve-builds koffi; dsh plugin --profile $pf add bgjobs
The first command approves and runs koffi's build script; the second add then succeeds.
If your DSH version has no approve-builds subcommand, edit pnpm-workspace.yaml (the error prints its full path) manually: the failed add left a placeholder set this to true or false — change it to true, then re-run add:
allowBuilds:
koffi: true
Only the first install needs this — once koffi is compiled it stays compiled, and upgrades/reinstalls don't repeat the step.
Restart DSH afterwards: the "Background Jobs Monitor" panel appears bottom-right of the web page and the agent gains bgjob_submit / bgjob_submit_pwsh / bgjob_submit_mcp / bgjob_mcp_tools / bgjob_status / bgjob_wait tools (the two MCP tools require turning on the "MCP jobs" switch in Settings first).
Method C (local source)
- Put the repo in a local plugin directory (avoid non-ASCII in the path), e.g.
D:\dsh\plugins\bgjobs; - Make DSH's module resolver find it (junction the plugin dir to DSH's
node_modules\bgjobs, or add the dir to DSH's plugin scan paths); for local dev also runpnpm installonce inside the plugin dir (sandbox runner deps, below); - Append the mount to
<DSH_HOME>\profiles\<profile>\cordis.patch.yml:
- insert:
- id: bgjobs
name: bgjobs
Uninstall: dsh plugin --profile <profile> remove bgjobs
Usage (agent tools)
bgjob_submit(name, command, workdir, [wait], [notify], [notify_mode])— submit a background job (commandis bat syntax);wait= seconds to wait in place after submitting (0/omitted = return immediately; >0 behaves likebgjob_waitwith no ids — wait for any of the current session's jobs to finish, falling back to the just-submitted job when no session info);bgjob_submit_pwsh(name, command, workdir, [wait], [sandbox], [justification], [notify], [notify_mode])— submit a background job (commandis PowerShell syntax, UTF-8 logs, safeexit <code>semantics);waitsame as above;bgjob_submit_mcp(name, workdir, tool, [arguments], server | server_config, [timeout_seconds], [wait], [notify], [notify_mode])— submit one MCP tool call as a background job (third engine, alsoschtasks-hosted, visible in the panel, wait/notify supported).serveris a name registered on the settings page,server_configis an inline config ({transport:"stdio",command,args,env,cwd}or{transport:"streamable-http",url,headers}) — give exactly one. The job connects to that server and calls the tool once; the result lands in the log and<jobDir>/result.json(channelrecords whether it hit a pre-warmed resident connection or did a cold start). Exit codes:0success /1tool reported an error /2connect or call failed /3timeout;timeout_secondsis unlimited by default (when that server has atimeoutMsregistered on the settings page, that value applies), or pass any positive number of seconds. Off by default — turn on the "MCP jobs" switch in Settings first (calls are refused otherwise). As in DSH, the session access mode (read-only, …) does not restrict MCP jobs. Check tool names withbgjob_mcp_toolsfirst;bgjob_mcp_tools(server | server_config, [refresh])— list that MCP server's tools (name/description/required fields) to confirm tool names and argument shape before submitting;refresh: truebypasses the 10-minute cache. DSH's already-registeredmcp__<server>__*tools are used first (zero startup cost); only on a miss does it actually connect/spawn the server to probe. Also gated by the "MCP jobs" switch;bgjob_status(jobId)— query status / exit code / log tail; for a look at the current state only — do not poll it in a loop (usebgjob_waitto wait);bgjob_wait(jobId | jobIds, [timeoutSeconds], [logic])— wait for background job(s) and return immediately with exit codes and log tails (timeoutSecondsdefaults to the "Default wait timeout" set in Settings — unlimited when that setting is unset, i.e. it waits until the job finishes; pass0/a negative number to force unlimited for this call, or a positive number to get atimedOut: truesnapshot at that point). Two modes (logic):any(default): a singlejobIdwaits for that job; ajobIdsarray is any-race (returns as soon as one finishes, with the finisher + the restpending) — the default posture when several jobs run in parallel: handle whichever lands first and keep waiting for the rest, no need to wait for all; omitting both waits for any job of the current session to finish;all(conjunctive): returnsallDone: trueplus each job's exit code/log tail only when all jobs succeed; any failure returns at once withfailed: true+failedJobId+ the finished jobs'results+ the rest inpending(a non-zero exit code, or a cleaned-up/unknown job, both count as failure) — no waiting for the stragglers. Use it only when "everything must succeed before continuing" or when "one failure means stop now"; to make progress as results land, use the defaultany. OmittingjobIdswaits for all jobs of the current session;
bgjob_list— list all jobs submitted by the current agent session (id/status/exit code); used together with the wait tools' default mode.- Do not poll with sleep: wait with
bgjob_wait; don't usesleep/Start-Sleep/timeout, nor a "loop overbgjob_status" (it occupies the turn and blocks incoming messages).
Just tell the AI (name the workdir and job name, and say whether you want it to wait for the result / notify you):
Run this whole chain in the background — clone the Linux kernel into
D:\work\linux, thenmake -j16— and notify me when it finishes (notify: on-exit); don't let the build tie up the conversation.
Start two background jobs in parallel: one downloading a dataset, one rebuilding; show me whichever finishes first and let the other keep running (any-race, no need to wait for all).
Convert the 30 CSVs under
D:\datato UTF-8 in one batch with the pwsh engine; I need all of them to succeed before continuing — stop as soon as any one fails (logic: 'all').
Submit one MCP call as a background job: server
glm, toolweb_search, query「latest LLM progress」, and send the result back to this session when it's done.
Then:
- Job output is streamed live to
<workdir>\.dsh\bgjobs\<jobId>\stdout.log; - On exit,
<workdir>\.dsh\bgjobs\<jobId>\exitcode.txtgets the exit code and a toast pops in the web page; - By default, completion does not interrupt the session; when you want the agent to know and wrap up, pass
notify: on-exit(oron-completionsuccess-only /on-failfailure-only), plus optionalnotify_mode(wakeupwake an idle session /quietinbox-only /always). - Delivery marker (notify view): each job records whether its result has been delivered into the session context — a completion notice that was injected (
notified·notify) or abgjob_waitthat returned it (notified·wait).bgjob_pending_listlists the session's not-yet-delivered jobs (the notify view), and the default mode ofbgjob_wait(includinglogic: 'all') waits only on that view, so an already-delivered result is never returned twice. The web panel and the offline GUI both show a "notified / pending" marker.
Web panel
Top bar, left to right: cleanup (opens the bottom trash bar: drag a finished job to delete, or bulk-clean >24h / all), collapse (to a compact job list), minimize (floating bubble anchored at the button). Toolbar toggles: "Only this session" (show only the current session's workspace jobs) and "Full access" (pre-approve full-access jobs; off by default). Click a job row to expand its live log. Rows show a live "Runtime" by default (growing while running, total time once done); move it to the detail view or hide it under Settings → Background Jobs → Field display. Panel copy follows the DSH UI language (Chinese DSH → Chinese panel, otherwise English).
The bgjobs pages in DSH Settings (gear at the bottom-left): two pages — "Background Jobs" and "MCP jobs". The panel's title bar also has two shortcuts: ⚙ opens the "Background Jobs" page, the data-gear icon opens "MCP jobs" (each can be hidden under Settings → Background Jobs → UI elements).
- "Background Jobs" (shows the current plugin version): ① sidebar-entry toggle (off by default; turning it on adds an entry at the bottom of the sidebar — click it to hide/show the floating panel); ② monitor-panel toggle (show/hide the floating panel and bubble, independent of the entry); ③ "Default wait timeout" (number input + Save): the default
timeoutSecondsforbgjob_waitwhen the agent omits it —0/empty means unlimited; ④ Open the offline GUI in its own window; ⑤ Open the tools folder in File Explorer; ⑥ Field display: place each job field in the list / detail view / hidden (includes the "Runtime" field).
MCP jobs (its own Settings page, off by default):
- MCP jobs switch: controls whether the agent may use
bgjob_submit_mcp/bgjob_mcp_tools(refused, with a pointer to the switch, while off). Takes effect immediately, no DSH restart. As in DSH, the session access mode (read-only, …) does not restrict MCP jobs; - MCP servers: register servers the agent can reference by name (name + JSON config). Each row has three text buttons — Pre-warm / Cold start / Disabled (amber / green / grey, so they cannot be mistaken for the global switch) — plus Edit (loads that server's full config — including plaintext env/headers — into the form below), "List tools" (expand tool names, click to copy) and "Delete"; the list only returns env/headers key names, never values. Pre-warm keeps a resident connection inside the DSH process so jobs reuse it instead of paying the cold start every time — valid only while DSH runs; on failure the job falls back to an in-job cold start, so correctness never depends on it (stdio benefits most; http transports spawn nothing, so the gain is small). Only failures that definitely did not execute anything fall back — a call that was already sent is never re-run when it fails or times out (no duplicate side effects); such a job ends with exit code
2/3and can be resubmitted (v0.1.84). Cold start enables the server without a resident connection. Disabled makesbgjob_submit_mcp/bgjob_mcp_toolsrefuse that server and drops its resident connection, while "List tools" still works; - Export / Import: export as a DSH YAML snippet (
@deepseek-ai/dsh-mcp-cliententries, paste intocordis.patch.yml) or bgjobs JSON (backup/migration, read back by the import box as-is). Import accepts pasted text or a picked file and auto-detects DSH patch snippets / bgjobs JSON / one-or-more server config objects (each needsserverName), with "skip / overwrite existing names"; entries containing!!jsare refused unless you tick the force box (they are never evaluated — fill in env/headers by hand). Exported text and each job'smcp.jsoncontain plaintext secrets — redact before sharing; - MCP in DSH (import): reads the
@deepseek-ai/dsh-mcp-cliententries already configured in the active profile or the globalcordis.patch.ymland imports them into the bgjobs registry with one click (read-only with respect to the DSH config). The header shows the active profile and how it was detected (command-line--profile/ module-path realpath / single profile); entries containing!!jsexpressions are never evaluated and are skipped by default (fill in env/headers manually). Both the quoted form (!!js '"Bearer " + process.env.X') and the unquoted form (KEY: !!js process.env.X) are recognised as needing manual review; if an expression sits somewhere it cannot be pinned to a single entry, every entry in that batch is flagged instead of silently importing the JS text as a plain string.
Offline CLI (works without DSH)
# run from the tools/ directory
.\dsh-bgjobs.ps1 list
.\dsh-bgjobs.ps1 status -Id <id>
.\dsh-bgjobs.ps1 log -Id <id> [-Tail 100]
.\dsh-bgjobs.ps1 submit -Name <n> -Command <c> -Workdir <dir> [-Pwsh]
.\dsh-bgjobs.ps1 kill -Id <id> [-NoDeleteDir]
.\dsh-bgjobs.ps1 cleanup [-OlderThanHours 24] # 0 = clean all
.\dsh-bgjobs.ps1 index -Workdir <dir>
GUI
Double-click tools\dsh-bgjobs-gui.bat to open a standalone window (no DSH needed): job list/log, submit (bat or pwsh), kill, cleanup (custom age cutoff or all), rebuild index. GUI and Toast copy follow the Windows UI language (zh* → Simplified Chinese, otherwise English); the CLI prints English.
Data & storage
- Job data:
<workdir>\.dsh\bgjobs\<jobId>\(job.jsonmetadata,stdout.logoutput,exitcode.txtexit code; MCP jobs also havemcp.jsonfor the call spec andresult.jsonfor the outcome); - Global state:
$DSH_HOME\bgjobs\index.json(job "map"),$DSH_HOME\bgjobs\fullaccess.json(full-access switch),$DSH_HOME\bgjobs\ui-prefs.json(web UI prefs),$DSH_HOME\bgjobs\mcp-prefs.json(MCP jobs switch),$DSH_HOME\bgjobs\mcp-servers.json(MCP server registry incl. per-server pre-warm/enabled flags),$DSH_HOME\bgjobs\mcp-tools-cache.json(tool-list cache, 10 minutes); donejobs persist by default until you clean them (panel 🧹 / CLI cleanup / GUI).
Notes & limits
workdirmust be an absolute path inside a DSH workspace;- Jobs run by default "only while the user is logged in": closing DSH/the terminal is fine, but logging out of Windows terminates jobs;
- Don't put
> log-style redirects in your command (the plugin already redirects all output and guarantees UTF-8); - Sandbox:
sandboxonly constrains file effects (writes outside the workspace/temp area are denied), network is unrestricted; it is "best effort", not a mathematical boundary — it fails if the workdir sits in an Everyone-writable location; sandboxed job dirs get Everyone:read (the script text is visible to local users); bat-engine jobs are always full-access, so restricted sessions must enable "Full access" to submit them; - In a restricted session, requesting more than the session mode triggers an approval prompt — put the reason in
justification. - MCP jobs: off by default (enable in Settings); if a job is force-killed, its stdio MCP server child may linger (a normal finish is cleaned up by the host, which also kills the pid recorded for a job when you delete it); each server has a "Pre-warm / Cold start / Disabled" three-state control in Settings — Disabled makes
bgjob_submit_mcp/bgjob_mcp_toolsrefuse that server and drops its resident connection (while "List tools" still works); the "pre-warm" connection is only valid while DSH runs and never changes the "jobs survive DSH" guarantee; the offline CLI/GUI only view and delete MCP jobs — they do not submit MCP calls. - A stopped wait is not a failed job: when you hit stop/interrupt while the agent waits, the wait itself ends as an error (the message names each job's current state and says you can wait again). That is DSH's cancellation semantics — once the caller cancels, a successful return cannot reach the model, only an error can; the job keeps running in the background, is not marked delivered, and the agent can call
bgjob_waitagain. A message from another agent during the wait returns normally instead (stoppedBy: 'message'): that result carries no message body, but the call declares the current turn finished — DSH then delivers the message (yours or another agent's) to the agent as a regular user message, queued next-turn prompts included; the agent should not paper over it with more waiting or blocking work. - MCP timeout cleanup has a 1–2s grace period: after an MCP timeout/failure the host closes the connection and reaps the server child (the SDK's
close()waits ~2s before escalating), soresult.json'sdurationMscan exceedtimeoutMsby 1–2s (the same file recordstimeoutMsfor comparison).
Development
Architecture, mechanism details, testing and release flow: see docs/developer.md.
License
MIT — see LICENSE.
Comments
Comments live in GitHub Discussions. Sign in with GitHub to post or react.