dsh-baton-mcp 0.6.8 → 0.6.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,7 +53,7 @@ Switched on, the plugin registers dsh-baton for Claude Code and Codex, for every
53
53
 
54
54
  That is all. To hand a task over, end the request with "hand it to DeepSeek Harness". Updating, removing, Windows, and registering by hand when the plugin could not, are [further down](#install-details).
55
55
 
56
- **New in 0.6.4:** Codex waits in one `dsh_wait` call of up to 59 minutes, so a run that ends within the hour costs one model request to wait for, and the result arrives as an ordinary reply in the chat. Update the plugin in Harness and new Codex conversations have it, with no restart.
56
+ **Installed on the day of a release?** For about a day after a release, Harness installs the previous version, and its install dialog says so. Open the plugin's page and press **Update to x.y.z** (更新到), or add the plugin with its version, such as `dsh-baton-host@0.6.8`.
57
57
 
58
58
  <p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/readme/en/divider-dark.svg"><img src="./assets/readme/en/divider.svg" width="100%" alt=""></picture></p>
59
59
 
@@ -284,7 +284,7 @@ When you update or remove the plugin, come from dsh-bridge, run from source, or
284
284
 
285
285
  **`npx -y dsh-baton-mcp setup`** does the same registration from a terminal: for a plugin that could not register, or when you come from an MCP directory. With the plugin already registered it changes nothing, then points you to the plugin if it is missing.
286
286
 
287
- **Updating.** Harness has an update button only for its official plugins, so: Plugins → dsh-baton-host → **Uninstall**, then **Add plugin** → `dsh-baton-host`. For a while after a release, Harness installs the previous version instead (pnpm's `minimumReleaseAge` holds back versions published very recently, and the install dialog says so); to get the new one at once, type the version: `dsh-baton-host@0.6.4`. A pre-release must always be added by its exact version, such as `dsh-baton-host@0.6.0-next.0`: Harness resolves `@next` to a broken placeholder. Plain `dsh-baton-host` gets the latest release. Switched on again, the plugin brings the MCP server to the same version; new conversations use it. If the two are ever out of step, the first tool call that notices it says so, naming the older side and the newer side's exact version to install.
287
+ **Updating.** On Harness 0.2.2 and later: Plugins → dsh-baton-host → **Update to x.y.z** (更新到) under Version; if it still says up to date, press **Check again** (重新检查) first. Harness 0.2.1 has no update button for this plugin: **Uninstall**, then **Add plugin** → `dsh-baton-host`. For about a day after a release, Harness installs the previous version instead (pnpm's `minimumReleaseAge` holds back versions published very recently, and the install dialog says so); to get the new one at once, type the version: `dsh-baton-host@0.6.8`. A pre-release must always be added by its exact version, such as `dsh-baton-host@0.6.0-next.0`: Harness resolves `@next` to a broken placeholder. Plain `dsh-baton-host` gets the latest release. Switched on again, the plugin brings the MCP server to the same version; new conversations use it. If the two are ever out of step, the first tool call that notices it says so, naming the older side and the newer side's exact version to install.
288
288
 
289
289
  **Removing.** Harness → Plugins → dsh-baton-host → **Uninstall**, then `npx -y dsh-baton-mcp setup --remove`. The plugin cannot tell an uninstall from Harness quitting, so it leaves the MCP entries to that command, which also deletes `~/.dsh-baton/app`; run records stay in `~/.dsh-baton` until you delete it.
290
290
 
package/README.zh-CN.md CHANGED
@@ -53,7 +53,7 @@ dsh-baton 是一个 MCP 服务器加一个 Harness 小插件。主 Agent 用一
53
53
 
54
54
  就这些。要转交任务,在需求最后加一句“交给 DeepSeek Harness 去做”。更新、卸载、Windows,以及插件没能注册时怎么手动注册,在[后面](#安装细节)。
55
55
 
56
- **0.6.4 新变化**:Codex 一次 `dsh_wait` 最多等 59 分钟,一小时以内跑完的任务等待只花一次模型请求,结果作为普通回复回到对话里。在 Harness 里更新插件,新开的 Codex 对话就用上了,不用重启。
56
+ **刚发版就安装?** 新版本发布后大约一天内,Harness 会装上一个版本,安装弹窗里会说明。打开插件详情页点 **更新到 x.y.z**,或者添加插件时带上版本号,比如 `dsh-baton-host@0.6.8`。
57
57
 
58
58
  <p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/readme/zh/divider-dark.svg"><img src="./assets/readme/zh/divider.svg" width="100%" alt=""></picture></p>
59
59
 
@@ -284,7 +284,7 @@ Harness 插件连不上时:
284
284
 
285
285
  **`npx -y dsh-baton-mcp setup`** 在终端里做同样的注册:用于插件没能注册的情况,或者从 MCP 目录过来的用户。插件已经注册过时它什么都不改;插件没装的话,它会提示去添加。
286
286
 
287
- **更新**。Harness 只给官方插件提供"更新"按钮,所以要:插件 → dsh-baton-host → **卸载**,再**添加插件** → `dsh-baton-host`。新版本刚发布的一段时间里,Harness 会装上一个版本(pnpm 的 `minimumReleaseAge` 会暂缓刚发布的版本,安装弹窗里会说明);想马上用新版,输入带版本号的名字:`dsh-baton-host@0.6.4`。预发布版必须写确切版本号添加,比如 `dsh-baton-host@0.6.0-next.0`:Harness 会把 `@next` 解析成一个坏掉的占位包。只输入 `dsh-baton-host` 装的是最新正式版。重新启用后,插件会把 MCP 服务器也更新到同一版本,新开的对话就会用上。万一两边版本对不上,第一次发现不一致的工具调用会提示,并写明哪一边旧、另一边的确切版本号。
287
+ **更新**。Harness 0.2.2 及以上:插件 → dsh-baton-host → 版本一栏里的 **更新到 x.y.z**;如果还显示已是最新版本,先点 **重新检查**。Harness 0.2.1 没有这个插件的更新按钮:**卸载**,再**添加插件** → `dsh-baton-host`。新版本发布后大约一天内,Harness 会装上一个版本(pnpm 的 `minimumReleaseAge` 会暂缓刚发布的版本,安装弹窗里会说明);想马上用新版,输入带版本号的名字:`dsh-baton-host@0.6.8`。预发布版必须写确切版本号添加,比如 `dsh-baton-host@0.6.0-next.0`:Harness 会把 `@next` 解析成一个坏掉的占位包。只输入 `dsh-baton-host` 装的是最新正式版。重新启用后,插件会把 MCP 服务器也更新到同一版本,新开的对话就会用上。万一两边版本对不上,第一次发现不一致的工具调用会提示,并写明哪一边旧、另一边的确切版本号。
288
288
 
289
289
  **卸载**。Harness → 插件 → dsh-baton-host → **卸载**,再运行 `npx -y dsh-baton-mcp setup --remove`。插件分不清"被卸载"和"Harness 退出",所以 MCP 注册交给这条命令删,它也会删掉 `~/.dsh-baton/app`;运行记录留在 `~/.dsh-baton`,不需要的话自己删掉。
290
290
 
package/host/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-baton-host",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "description": "DeepSeek Harness plugin for dsh-baton: hand coding tasks from Claude Code or Codex to DeepSeek Harness on your machine. Adding it is the whole install: it registers the dsh-baton MCP server for Claude Code and Codex, runs each task live in the Harness window, wakes the main agent when the run ends and hands back the changed files, the project's own test results and a requirement-by-requirement checklist.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "dependencies": {
23
- "dsh-baton-mcp": "0.6.8"
23
+ "dsh-baton-mcp": "0.6.9"
24
24
  },
25
25
  "license": "MIT",
26
26
  "dsh": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-baton-mcp",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "mcpName": "io.github.JialaoLiu/dsh-baton",
5
5
  "description": "Pass the baton from Claude Code, Codex or any MCP client to a local DeepSeek Harness: an MCP server that hands off tasks, continues a specific session, shows the run live in Harness and hands the result back when it is done.",
6
6
  "type": "module",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/JialaoLiu/dsh-baton",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.6.8",
10
+ "version": "0.6.9",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "dsh-baton-mcp",
15
- "version": "0.6.8",
15
+ "version": "0.6.9",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
package/src/cli.mjs CHANGED
@@ -91,6 +91,7 @@ async function main() {
91
91
  const total_s = Number(flags.timeout) || 24 * 3600
92
92
  const waitStarted = Date.now()
93
93
  const st = await core.api.wait({ run_id: rest[0], timeout_s: total_s })
94
+ const stop = core.stopHint(st)
94
95
  // A Harness goal goes on after the run's own round: so does this wait, until the goal
95
96
  // stops (or the next report, with --until turn), with every round since the run began.
96
97
  if (core.goalGoesOn(st)) {
@@ -113,6 +114,10 @@ async function main() {
113
114
  // The hint points at evidence: it goes with it (files changed, checks run, other runs in the folder).
114
115
  ...(st.evidence ? { evidence: st.evidence } : {}),
115
116
  ...(st.state === 'completed' && !st.decided_for_user ? { hint: core.endedHint(st) } : {}),
117
+ // Stopped by the user, cancelled, or cut off by a Harness restart: say so, or the client
118
+ // takes it for a failure in the work and sends it again.
119
+ ...(st.harness_down ? { harness_down: true, hint: core.HARNESS_DOWN_HINT.replace('call dsh_wait again with the same run_id', 'run this same wait command again') } : {}),
120
+ ...(stop ? { ...(st.turn_end ? { turn_end: st.turn_end } : {}), ...stop } : {}),
116
121
  ...(st.needs_input ? { needs_input: st.needs_input, question: st.question, ...(st.question ? { relay: core.relayText(st.question) } : {}), hint: st.question ? 'The run asks the user something. If the user allowed you to decide, choose by your own judgment and answer with dsh_answer (or dsh-baton answer), passing reason, and tell the user what you chose and why. Otherwise show the user relay, written in the user\'s language, with its first line filled in with your own pick and a reason from your own judgment of the question and the task (harness_suggested is only the asker\'s pick, never a reason by itself), and ask the user first. Then wait again.' : 'The run waits for a person in the Harness UI. Tell the user, then wait again.' } : {}) })
117
122
  process.exitCode = st.state === 'completed' ? 0 : st.needs_input ? 3 : 1
118
123
  return
package/src/core.mjs CHANGED
@@ -147,7 +147,8 @@ function hostCallAt(endpoint, method, params = {}, { timeoutMs = 15000 } = {}) {
147
147
  msg.error ? finish(new Error(String(msg.error))) : finish(null, msg.result)
148
148
  })
149
149
  conn.on('error', (e) => finish(Object.assign(e, { hostDown: true })))
150
- conn.on('close', () => finish(new Error(`dsh-baton-host closed the connection before answering ${method}`)))
150
+ // Harness quitting closes the connection mid-call: marked so a wait can wait for it to come back.
151
+ conn.on('close', () => finish(Object.assign(new Error(`dsh-baton-host closed the connection before answering ${method}`), { hostClosed: true })))
151
152
  })
152
153
  }
153
154
 
@@ -623,12 +624,33 @@ const NEEDS_INPUT_HINT = (st) => st.question
623
624
  ? `The run stopped to ask the user ${st.question.count} question(s) (in question). Unless the user let you decide, show the user relay, written in the user's language, with its first line filled in with your own pick and reason (not the asker's pick and not its option description repeated), then ask which they want. ${ASKED} After answering, wait for the run again.`
624
625
  : 'The run waits for something only a person can do in the Harness UI (an approval, for example). Tell the user, then wait for the run again.'
625
626
 
627
+ // A run the user stopped in the Harness UI: its turn ended aborted, by the user. With the usual
628
+ // hint, Codex took that for a failure and sent the task again each time the user stopped it
629
+ // (Windows, 2026-11-08). The UI stop leaves background jobs of the session running; dsh_cancel
630
+ // stops them too.
631
+ export const stoppedByUser = (st) => st?.state === 'failed' && st.turn_end?.kind === 'aborted' && st.turn_end?.reason?.kind === 'user'
632
+ export const USER_STOPPED_HINT = 'The user stopped this run in Harness (stopped_by_user). It is not a failure to repair: do not send it again, continue it or send fixes. Tell the user it was stopped and what evidence shows it got done, then ask what to do next. Background jobs it started may still be running in that session; stop them with dsh_cancel and its session_id only if the user asks.'
633
+ // Ended by something other than the work: Harness restarting under the run, or a cancel (dsh_cancel,
634
+ // or in Harness: the user stopping the session drops its queued messages, or removing one). Sending
635
+ // again on one's own would override the user here too.
636
+ export const INTERRUPTED_HINT = 'Harness restarted or quit while this run was going, and its turn was stopped (interrupted). It is not a failure in the work: do not send it again or continue it on your own. Tell the user Harness was restarted and what evidence shows the run got done, then ask whether to run it again or continue in the same session.'
637
+ export const CANCELLED_HINT = 'This run was cancelled before it finished: with dsh_cancel, or in Harness, where the user stopped the session or removed the queued message. If you did not cancel it yourself, the user did: do not send it again. Tell the user it was cancelled and ask what to do next.'
638
+ // The hint for a run that did not end by its own work, or null for one that did.
639
+ export function stopHint(st) {
640
+ if (stoppedByUser(st)) return { stopped_by_user: true, hint: USER_STOPPED_HINT }
641
+ if (st?.state === 'interrupted') return { hint: INTERRUPTED_HINT }
642
+ if (st?.state === 'cancelled') return { hint: CANCELLED_HINT }
643
+ return null
644
+ }
645
+
626
646
  // The result of a run that ended, kept short: the client already knows what it sent, and a
627
647
  // completed run's event log is only the work behind final_text (dsh_status still has it).
628
648
  // A run that did not complete keeps its last events, to show where it stopped.
629
649
  function ended({ recent_events, message_preview, backend, ...st }) {
630
650
  const out = { ...st, ...(st.state !== 'completed' && recent_events?.length ? { recent_events: recent_events.slice(-5) } : {}) }
631
651
  if (outOfBalance(st)) return { ...out, hint: QUOTA_HINT }
652
+ const stop = stopHint(st)
653
+ if (stop) return { ...out, ...stop }
632
654
  // decided_for_user first, so a client that skims to final_text still meets it.
633
655
  return st.decided_for_user
634
656
  ? { hint: `${DECIDED_HINT} ${endedHint(st)}`, decided_for_user: st.decided_for_user, ...out }
@@ -757,6 +779,10 @@ export async function followThrough(params, { total_s, chunk_s = 300, onChunk, s
757
779
  }
758
780
  }
759
781
 
782
+ // How often a wait looks for Harness again while it is not running.
783
+ const HOST_RETRY_MS = Number(process.env.DSH_BATON_HOST_RETRY_MS) || 2000
784
+ export const HARNESS_DOWN_HINT = 'DeepSeek Harness is not running (closed, or restarting), so the run cannot be followed right now; Harness ends a running turn when it quits. Do not send the task again. Tell the user Harness is closed and to open it; then call dsh_wait again with the same run_id: it reports how the run ended (interrupted, if Harness quit while it ran).'
785
+
760
786
  export const api = {
761
787
  async workspaces() {
762
788
  if (!(await useHost())) throw new Error('Listing workspaces needs the dsh-baton-host plugin running inside DeepSeek Harness.')
@@ -828,8 +854,20 @@ export const api = {
828
854
  return changed ? { ...st, changed_files: changed } : st
829
855
  },
830
856
  async wait({ run_id, timeout_s = 300 }) {
831
- if (isHostRun(run_id)) return withEvidence(shaped(tag('host', await hostCall('wait', { run_id, timeout_s }, { timeoutMs: (timeout_s + 15) * 1000 }))))
832
- return withEvidence(tag('headless', await waitRun(run_id, { timeout_s })))
857
+ if (!isHostRun(run_id)) return withEvidence(tag('headless', await waitRun(run_id, { timeout_s })))
858
+ // Harness quit or restarting: wait for it to come back within the same call (the plugin then
859
+ // reports the run as interrupted) instead of failing, which had Codex retry every few seconds.
860
+ const deadline = Date.now() + timeout_s * 1000
861
+ for (;;) {
862
+ const left = Math.max(1, Math.ceil((deadline - Date.now()) / 1000))
863
+ try {
864
+ return withEvidence(shaped(tag('host', await hostCall('wait', { run_id, timeout_s: left }, { timeoutMs: (left + 15) * 1000 }))))
865
+ } catch (error) {
866
+ if (!(error?.hostDown || error?.hostClosed)) throw error
867
+ if (Date.now() + HOST_RETRY_MS >= deadline) return { run_id, state: 'unknown', timed_out: true, harness_down: true, backend: 'host' }
868
+ await new Promise((r) => setTimeout(r, HOST_RETRY_MS))
869
+ }
870
+ }
833
871
  },
834
872
  async cancel({ run_id, session_id }) {
835
873
  if (run_id && !isHostRun(run_id)) return tag('headless', cancelRun(run_id))
package/src/mcp.mjs CHANGED
@@ -122,6 +122,7 @@ async function waitInChunks({ run_id }, progress, signal, meta) {
122
122
  // Never spin: a chunk that came back early still takes a moment before the next one.
123
123
  if (Date.now() - t0 < 500) await new Promise((r) => setTimeout(r, 500))
124
124
  if (Date.now() >= deadline) {
125
+ if (st.harness_down) return { run_id, state: st.state, timed_out: true, harness_down: true, hint: core.HARNESS_DOWN_HINT }
125
126
  return {
126
127
  run_id, state: st.state, timed_out: true,
127
128
  ...(st.waiting_on ? { waiting_on: st.waiting_on } : {}),
@@ -182,6 +183,7 @@ async function waitAnyInChunks(run_ids, progress, signal, meta) {
182
183
  if (Date.now() - t0 < 500) await new Promise((r) => setTimeout(r, 500))
183
184
  if (Date.now() >= deadline) {
184
185
  const asking = running.filter((r) => r.state === 'waiting').map((r) => r.run_id)
186
+ if ([...answers.values()].some((st) => st.harness_down)) return { run_ids, timed_out: true, harness_down: true, still_running: running, hint: `${core.HARNESS_DOWN_HINT.replace('the same run_id', 'the same run_ids')}` }
185
187
  return {
186
188
  run_ids, timed_out: true, still_running: running,
187
189
  hint: `None of these runs has ended yet. Call dsh_wait again with the same run_ids, with no progress check or progress note in between (work the user asked you to do meanwhile is fine).${asking.length ? ` ${asking.join(', ')} still wait${asking.length === 1 ? 's' : ''} for a person (see dsh_status for the question): answer with dsh_answer if you may, or tell the user.` : ''}`,
@@ -200,12 +202,15 @@ function anyResult({ done, running }) {
200
202
  : res
201
203
  })
202
204
  const ids = running.map((r) => r.run_id)
205
+ // Stopped by the user, cancelled or cut off by a Harness restart: not work to send back.
206
+ const stopped = ended.filter((r) => r.stopped_by_user || r.state === 'interrupted' || r.state === 'cancelled').map((r) => r.run_id)
207
+ const leave = stopped.length ? ` ${stopped.join(', ')} did not end by ${stopped.length === 1 ? 'its' : 'their'} own work (stopped by the user, cancelled or cut off by a Harness restart, see ${stopped.length === 1 ? 'its' : 'each'} hint): send ${stopped.length === 1 ? 'it' : 'them'} nothing more on your own and leave ${stopped.length === 1 ? 'it' : 'them'} out of the next wait.` : ''
203
208
  const next = ids.length
204
209
  ? `Then call dsh_wait again with run_ids: the ones still running (${ids.join(', ')}) plus the run_id of every fix you sent, so whichever ends next comes back first. Do not wait for one run on its own while others are going.`
205
210
  : 'None of these runs is still going.'
206
211
  return {
207
212
  ended, still_running: running,
208
- hint: `${ended.length} of the runs ${ended.length === 1 ? 'has' : 'have'} ended or stopped to ask (ended; each has its own hint). Deal with ${ended.length === 1 ? 'it' : 'them'} now: review against what you asked, and send any fixes with dsh_send to the same session_id right away, or answer a question. ${next}`,
213
+ hint: `${ended.length} of the runs ${ended.length === 1 ? 'has' : 'have'} ended or stopped to ask (ended; each has its own hint). Deal with ${ended.length === 1 ? 'it' : 'them'} now: review against what you asked, and send any fixes with dsh_send to the same session_id right away, or answer a question.${leave} ${next}`,
209
214
  }
210
215
  }
211
216