dsh-job-watch
A long-running job you did not start, pinned to the conversation that cares about it. The sidebar row of that conversation turns pink while the job runs, green once it has finished (come back and analyse the result), and amber when the job id cannot be found.
Not affiliated with DeepSeek. This is an independent community plugin for DeepSeek Harness (DSH). "DeepSeek Harness" is named only to describe compatibility.
The problem
An agent submits a job that takes six hours — to a cluster scheduler, to a bare-metal box over SSH, to
tmux somewhere — and then the session goes quiet. The job's state lives in a completely different program
(squeue, bjobs, tmux list-sessions), so the only ways to find out are:
- open a terminal and poll it by hand, or
- keep a monitoring tab open, or
- notice four hours later that it finished and you never looked.
The GUI already knows which conversation you were working in. dsh-job-watch attaches the job id to that
conversation, queries the provider for you, and paints the conversation's row — so "which conversation still
has something running, and which one finished while I was away" is answered by the sidebar you are already
looking at.
□ conv A · (no mark) nothing to do
▌ conv B · ● pink, pulsing a tracked job is running
▌ conv C · ● green finished — come back and analyse
▌ conv D · ● amber the id cannot be found by its provider
Green is deliberately sticky: it does not expire, because it exists to make you come back. It clears the moment you actually say something in that conversation — and injected context (workspace instructions, skill catalogs, runtime snapshots) does not count as "saying something", because that would clear it silently and defeat the whole feature.
Screenshot

A real screenshot of the sidebar: three tracked conversations, each with the running bar (
#ec4899, the exact colour the plugin paints) on the left edge of its row. The conversation titles are covered by a solid block — they are the author's private session names, and the crop is verified pixel by pixel to contain no text at all. No green example appears here simply because no tracked job had finished when the shot was taken; green means "finished, come back and analyse".
How it works
| Half | Runs in | Does |
|---|---|---|
host (lib/index.js) |
the dsh process |
the job ledger, the provider poller, the job_watch agent tool, /api/job-watch/* |
browser (lib/client.js) |
the Web GUI | reads the state endpoint and paints the sidebar rows |
The plugin owns no job logic. A provider is a shell command plus one regex applied per output line —
so an HPC scheduler, tmux on a bare-metal box, or your own status CLI are all the same thing to it.
Nothing about a cluster, queue or path is hard-coded.
Queries happen on four triggers, all of them cheap and skipping each other:
| Trigger | Why it exists |
|---|---|
every pollMinutes (default 30) |
the normal rhythm |
| a 60 s heartbeat that compares the wall clock against the monotonic clock | a suspended machine freezes timers, so a timer firing "on time" proves nothing; clock drift > 90 s is the evidence of a sleep, and the poll catches up right away |
| opening / refocusing the GUI | you are here, so the state you are shown should be current (the host only re-queries if the ledger is older than requestStaleSeconds) |
right after register |
the colour must be correct immediately, not in 30 minutes |
Install
dsh plugin --profile web add dsh-job-watch
Restart dsh web afterwards (--profile must come after plugin — the other order is rejected).
Then configure at least one provider in your profile patch, ~/.dsh/profiles/<profile>/cordis.patch.yml:
- id: job-watch
config:
pollMinutes: 30
providers:
# 1) a scheduler that prints one job per line (Slurm here: id, state, name)
- id: cluster
label: Cluster
command: 'ssh login01 "squeue -u $USER -h -o ''%i %T %j''"'
linePattern: '^(?<id>\S+)\s+(?<stat>\S+)\s+(?<name>\S+)$'
# 2) a bare-metal box where "a job" is a tmux session name
- id: lab
label: Lab box
command: 'ssh lab "tmux list-sessions -F ''#{session_name}''"'
linePattern: '^(?<id>[A-Za-z0-9_.-]+)$'
linePattern is applied per line; it needs a named group id, and may add stat and name. Lines that
match nothing are ignored, so headers, banners and warnings cost nothing. ANSI colour codes are stripped
first, which matters as soon as your command runs through a shell that thinks it is talking to a terminal.
Provider reference
| Key | Required | Meaning |
|---|---|---|
id |
✅ | the name used by job_watch(provider=…) and shown in the tooltip |
label |
human-readable name for messages | |
command |
✅ | run through the platform shell; write the SSH call yourself, so no remote details live in the plugin |
linePattern |
✅ | regex per line, named group id (required), stat, name (optional) |
runningStates |
overrides the default list (RUN, PEND, QUEUE, WAIT, …) |
|
doneStates |
overrides the default list (DONE, EXIT, FAIL, ABORT, KILL, …) |
Other config: pollMinutes, heartbeatSeconds, commandTimeoutSeconds, minPollGapSeconds,
requestStaleSeconds, unknownAfterSeconds, pruneDays (finished + acknowledged rows are dropped after
this long), missingMeans (done = a job vanishing from its provider's listing has finished; unknown =
paint it amber instead), statePath (default $DSH_HOME/job-watch.json).
A non-zero exit from a provider command is not fatal: tmux list-sessions exits 1 when there are no
sessions, and several schedulers print the listing while warning on stderr. stdout is parsed either way and
the failure is reported alongside the state.
Agent integration
The host injects a short rule into every agent's system prompt, and exposes one tool:
job_watch(action='register', jobIds=['40217'], provider='cluster', etaMinutes=540, note='sweep A')
job_watch(action='list')
job_watch(action='refresh')
job_watch(action='clear')
registerwithoutjobIdsauto-claims: every job the providers currently list as running and that no conversation tracks yet. Useful after the fact ("what did I forget to register?").- Registering is not optional. No colour can be inferred from a job that nobody told the plugin about —
a job submitted in
tmuxor on a cluster leaves no trace in the ledger. This is why the rule lives in the system prompt rather than in a doc nobody reads. - Every report ends each job with a recommended finish time. It is arithmetic (registration time + the ETA you registered) or an explicit "cannot be estimated, here is how to measure it" — the plugin never invents a time. An ETA that has already elapsed while the provider still reports the job as running is called out as overdue rather than assumed finished.
SKILL.mdis the same contract written for an agent that loads skills.
Limitations (measured, not guessed)
- The sidebar has no decoration API. The browser half finds a row's session id through the React fiber
chain hanging off the row DOM node and injects the marker into the row. This is inherently tied to the
current sidebar implementation; when it breaks, the console and
window.__dshJobWatch.stats()say so instead of failing silently. registeris a manual step — see above. There is no way around it for jobs DSH did not spawn.- Only rows the sidebar renders get a colour. A conversation that is not in the visible list is not painted (the rollup is still computed, so it paints as soon as the row appears).
linePatternis yours to get right. A pattern that matches the header row as a job will track a fake job; a pattern that matches nothing silently tracks nothing (which is whyrefreshreports how many ids each provider listed).- No test of the GUI itself.
node --test test/kernel.test.mjscovers the kernel — listing parsing, STAT folding, the real-prompt discriminator, and the finish-time contract.
Prior art, and what is actually new here
Job listing is not new, and none of it is claimed as an idea of this plugin. In the DSH ecosystem:
| Project | What it covers | Why this is different |
|---|---|---|
official @deepseek-ai/dsh-client-ui-jobs (the ui-jobs entry) |
background jobs DSH itself spawned, in the session header | this is for jobs DSH did not start: a scheduler, a tmux session, another machine |
lql341/dsh-scnet |
SCNet HPC as tools + a skill (submit, status, logs, cancel) | on-demand tool calls for one specific platform; no conversation-level colour, no ETA contract |
Gczmy/dsh-node-sched |
a dashboard for their own sched daemon |
requires that daemon; a main-panel view rather than the conversation list |
Luaphes/dsh-web-attention-badge |
session attention state in the browser tab bar (amber = waiting on you, green = done) | a session has finished, not a job; painted in the tab strip, not on the conversation row |
catsenior507/dsh-command-center |
a panel for running background jobs | a panel you open, not a colour on the row you are already reading |
The increment claimed here is narrow: an external job id → a colour on that conversation's sidebar row, plus the finish-time contract (arithmetic or an explicit refusal to guess) and the sleep/wake-safe poller. Everything else — the job tools, the dashboards, the attention badges — belongs to the projects above.
License
MIT. See LICENSE.
中文速览
问题:作业交出去要跑几小时,状态却在另一个程序里(squeue / bjobs / tmux list-sessions),于是
要么手动开终端轮询,要么四小时后才发现早跑完了。
做法:把作业号绑到当前对话上,插件自己按配置的 provider 去查,侧边栏那一行直接变色 —— 🩷 粉 = 在跑 | 🟢 绿 = 已结束,回来分析 | 🟠 橙 = 查不到。绿色不会自己过期(它就是要喊你回来), 你在那个对话里真的说话之后才消失(注入的上下文不算说话)。
provider 就是「一条命令 + 一行一条的正则」,所以调度器、裸机 tmux、你自己的状态 CLI 都是同一种东西: 插件里不写死任何集群、队列、路径。查询有四条触发(定时 / 心跳发现休眠唤醒 / 打开页面 / 注册后立刻)。
必做:dsh plugin --profile web add dsh-job-watch,重启 dsh web,然后在
~/.dsh/profiles/<profile>/cordis.patch.yml 里配 providers(见上文)。每次提交作业后必须登记
——没人登记的作业不会有任何颜色,这是设计约束,不是 bug:
job_watch(action="register", jobIds=["40217"], provider="cluster", etaMinutes=540)
每条报告都会给出推荐结束时间:要么是「登记时刻 + 你给的 ETA」算出来的,要么明确说无法可靠推算 并告诉你从哪取数(作业自身的 elapsed 列 / 日志里的进度行)——不编时间。
与既有项目的关系:官方 ui-jobs 管的是 DSH 自己起的后台任务;dsh-scnet 是 SCNet 的工具+技能;
dsh-node-sched 是自家 sched 守护进程的看板;dsh-web-attention-badge 是标签页里的会话注意力状态;
dsh-command-center 是要打开的作业面板。本插件只占一格:外部作业 → 对话列表那一行的颜色 +
结束时间口径 + 抗休眠的轮询。
No comments yet. Be the first to write one.