@hzxwonder/dsh-mcp 0.3.0 → 0.4.0

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.
@@ -38,6 +38,17 @@ Project and user restrictions take precedence over this skill.
38
38
  13. Choose `read-only` by default.
39
39
  14. Choose `workspace-write` only when the task requires edits.
40
40
 
41
+ Select `thinking_effort` explicitly for every call, including session continuation.
42
+ Use only `off`, `low`, `high`, or `max`.
43
+ Choose `low` for mechanical tasks.
44
+ Choose `high` for bounded tasks that require reasoning.
45
+ Choose `off` only after a deliberate decision to disable thinking.
46
+ Choose `max` only when the task justifies its additional cost.
47
+ Do not silently supply a missing value.
48
+ Do not use aliases such as `medium` or `xhigh`.
49
+ This parameter sets DSH `agent-default-model.config.reasoningEffort`, not prompt text.
50
+ Do not rely on the previous session's effort.
51
+
41
52
  The client may prefix the tool name with its server name.
42
53
  Do not change the Codex provider or silently substitute a direct API call.
43
54
  DSH cannot see the main conversation.
@@ -45,6 +56,9 @@ Do not include keys, tokens, or login details in the task.
45
56
 
46
57
  The user skill and MCP initialization instructions apply across projects.
47
58
  Global setup does not require a per-project installation or `AGENTS.md` section.
59
+ The installer can upgrade an owned, unchanged previous skill template.
60
+ It preserves customized skills and files without a previous-template reference.
61
+ Back up those files and resolve conflicts manually before replacement.
48
62
  Existing project, user, or global rules can prevent automatic delegation.
49
63
  Do not infer `project_root` from the server's working directory, HOME, installation path, or session history.
50
64
  Do not use HOME or `/` as a default grant.
@@ -72,6 +86,7 @@ Example tool arguments:
72
86
  ```json
73
87
  {
74
88
  "project_root": "/absolute/authorized/project",
89
+ "thinking_effort": "high",
75
90
  "task": "Add boundary tests for parsePort. Change only tests/port.test.ts. Use the existing test framework. Preserve production code and user changes. Cover zero, 65535, negative values, 65536, and non-integers. Run npm test -- tests/port.test.ts. Do not commit or delegate. Stop if a decision remains unresolved. Report questions in the final answer. Report changes, exact check commands, and results. Identify checks that you did not run.",
76
91
  "cwd": ".",
77
92
  "permission_mode": "workspace-write",
@@ -81,18 +96,30 @@ Example tool arguments:
81
96
 
82
97
  ## Review the result
83
98
 
84
- 1. Inspect `status`, `exit_code`, `reason`, `answer`, and `diagnostics`.
85
- 2. Inspect `changed_files` and `change_warning`.
86
- 3. Review actual edits, new files, and existing user changes.
87
- 4. Check the result against each acceptance criterion.
88
- 5. Run applicable acceptance checks when the assignment permits them.
89
- 6. Report worker changes separately from your own review and checks.
99
+ 1. Read `answer` and check that `output_source` is `session`.
100
+ 2. Read `session_id`, `session_file`, `assistant_seq`, and `turn`.
101
+ 3. Inspect `status`, `exit_code`, `reason`, `terminal_reason`, `session_output_error`, `diagnostics`, and `thinking_effort`.
102
+ 4. Inspect `changed_files` and `change_warning`.
103
+ 5. Review actual edits, new files, and existing user changes.
104
+ 6. Check the result against each acceptance criterion.
105
+ 7. Run applicable acceptance checks when the assignment permits them.
106
+ 8. Report worker changes separately from your own review and checks.
90
107
 
91
108
  `completed` means that the Harness turn completed, not that the task passed acceptance.
92
109
  A successful tool call does not prove that internal tests passed.
93
110
  Do not claim that you ran a check merely because the worker reports it.
94
111
  Respect existing check evidence unless changes or new findings invalidate it.
95
112
 
113
+ After the worker stops, MCP reads its persisted session and checks the session ID and saved physical working directory.
114
+ The answer is the last committed assistant text in the latest durable turn.
115
+ Hidden reasoning, tool calls, and tool results are not the final answer.
116
+ If that turn has no assistant text, `answer` is empty and `assistant_seq` is `null`.
117
+ Never substitute an earlier turn's successful answer.
118
+ Missing, corrupt, unreadable, or mismatched session data causes an explicit failure.
119
+ Do not substitute stdout when the durable read fails.
120
+ Do not accept work from `completed` or stdout alone.
121
+ Do not expose hidden reasoning in your report.
122
+
96
123
  Git reports omit ignored files and complete internal submodule diffs.
97
124
  They cannot attribute concurrent writes.
98
125
  A non-Git workspace can return `null` instead of a file list.
@@ -111,11 +138,13 @@ The `never` approval policy denies approval requests instead of approving them.
111
138
  4. Send a new call with the returned `session_id`.
112
139
  5. Supply the same authorized `project_root` and preserve exactly the same actual `cwd`.
113
140
  6. Supply explicit continuation instructions and the required permission mode.
141
+ 7. Select and supply `thinking_effort` again, even when the session already has an effort setting.
114
142
 
115
143
  Wait for the previous call to return before continuation.
116
144
  The server waits for process termination before it returns.
117
145
  This sequence resumes a session but does not provide real-time dialogue.
118
146
  Do not assume a session can resume in a different project.
147
+ There is no separate tool for reading arbitrary sessions.
119
148
 
120
149
  On failure, inspect partial changes before another call.
121
150
  Preserve user changes.
package/README.md CHANGED
@@ -1,48 +1,18 @@
1
1
  # dsh-mcp
2
2
 
3
- 让 Codex 保持当前 GPT 模型、Provider 和账号登录,通过 MCP 把边界明确的任务交给 **DeepSeek Harness + DeepSeek Flash**。Codex 主代理负责拆分任务、审查修改和最终验收。
3
+ 让 Codex 保持当前 GPT 模型、Provider 和账号登录,通过 MCP 把边界明确的任务交给 **DeepSeek Harness + DeepSeek Flash**。Codex 主代理负责选择思考强度、读取工作器结果、审查修改和最终验收。
4
4
 
5
- 本项目使用官方 `headless --json` 接口,不依赖 DSH Web 内部接口。当前固定使用 `@deepseek-ai/dsh@0.2.2-rc.1`,模型为 `deepseek-flash`。DSH 仍处于 developer preview,升级时需要重新检查兼容性。
5
+ ## 快速开始:npx 安装一次
6
6
 
7
- ## 运行方式
7
+ 需要 macOS 或 Linux、Node.js **24.9.0 或更高版本**、npm,以及支持 MCP 的 Codex 客户端。准备好 [DeepSeek API Key](https://platform.deepseek.com/api_keys),或兼容 Messages API 的服务凭据。
8
8
 
9
- ```text
10
- 用户电脑上的 Codex
11
- ↓ MCP stdio
12
- 用户电脑上的 dsh-mcp
13
- ↓ 启动本地子进程
14
- 用户电脑上的 DeepSeek Harness
15
- ├─ 读写本次授权的项目
16
- └─ 请求 DeepSeek 官方 API 或自定义 Base URL
17
- ```
18
-
19
- 安装 MCP 时,npm 会自动下载完整的 Harness 依赖。不需要用户另行全局安装 `dsh`,也不会安装桌面 App 或下载模型权重。
20
-
21
- 你使用自己的 DeepSeek API Key,模型费用由对应 API 服务收取。本项目只提供软件,不提供远程任务执行服务。若使用自定义 Base URL,该服务会收到你的密钥和任务数据。
22
-
23
- 依赖的磁盘占用较大。开发环境的 `node_modules` 曾约为 922 MB,具体占用因平台、依赖布局和缓存而异。
24
-
25
- ## 环境要求
26
-
27
- - macOS 或 Linux。
28
- - Node.js **24.9.0 或更高版本**。
29
- - npm。
30
- - 支持 MCP 的 Codex 客户端。
31
- - [DeepSeek API Key](https://platform.deepseek.com/api_keys),或兼容 Messages API 的服务凭据。
32
-
33
- ChatGPT 账号登录、订阅额度和 DSH GUI 登录不能替代 API Key。
34
-
35
- ## 快速安装
36
-
37
- ### npm 安装
38
-
39
- 包页面:[npm · @hzxwonder/dsh-mcp](https://www.npmjs.com/package/@hzxwonder/dsh-mcp)。以下命令固定使用 `0.3.0`,避免自动升级改变执行行为。
9
+ 安装命令固定使用 `0.4.0`。此版本要求每次显式选择思考强度,并从持久化 session 返回最终结果。
40
10
 
41
11
  ```sh
42
- npx --yes --package @hzxwonder/dsh-mcp@0.3.0 dsh-mcp setup
12
+ npx --yes --package @hzxwonder/dsh-mcp@0.4.0 dsh-mcp setup
43
13
  ```
44
14
 
45
- 安装器会打开本机浏览器表单,只填写两个配置项:
15
+ 安装器打开本机浏览器表单,只填写两个配置项:
46
16
 
47
17
  | 配置项 | 说明 |
48
18
  | --- | --- |
@@ -51,200 +21,188 @@ npx --yes --package @hzxwonder/dsh-mcp@0.3.0 dsh-mcp setup
51
21
 
52
22
  保存后,在 Codex 中新建会话,确认工具列表出现 `delegate_to_deepseek`。客户端可能为工具名添加服务器前缀。
53
23
 
54
- **只安装一次,不需要选择或绑定项目。** 切换项目后不需要重新安装。每次委派时,由 Codex 根据当前上下文传入项目的绝对路径 `project_root`。
24
+ **全局安装一次,不选择或绑定项目,也不需要逐项目配置。** 每次委派时,Codex 必须传入当前授权项目的绝对路径 `project_root`,并主动选择 `thinking_effort`。
55
25
 
56
26
  没有图形浏览器时,使用终端输入:
57
27
 
58
28
  ```sh
59
- npx --yes --package @hzxwonder/dsh-mcp@0.3.0 dsh-mcp setup --terminal
29
+ npx --yes --package @hzxwonder/dsh-mcp@0.4.0 dsh-mcp setup --terminal
60
30
  ```
61
31
 
62
- 终端不会回显 API Key。不要把真实密钥写进命令参数、URL、任务文本、skill 或项目配置。
32
+ 终端不会回显 API Key。不要把真实密钥写进命令参数、URL、任务文本、skill 或项目配置。ChatGPT 登录、订阅额度和 DSH GUI 登录不能替代 API Key。
63
33
 
64
- ### 从源码安装
34
+ ### Base URL 必须支持 Messages
65
35
 
66
- ```sh
67
- git clone https://github.com/hzxwonder/dsh-mcp.git
68
- cd dsh-mcp
69
- npm ci
70
- npm run setup
71
- ```
36
+ DSH 使用 **Messages API**,请求路径为 `/v1/messages`。默认根地址是 `https://api.deepseek.com/anthropic`。只支持 OpenAI Chat Completions 的地址不能直接使用。
72
37
 
73
- 终端方式:
38
+ 自定义地址不能包含用户名、密码、查询参数或片段。请只使用可信服务:该服务会收到你的密钥和任务数据。模型费用由对应 API 服务收取。
74
39
 
75
- ```sh
76
- npm run setup -- --terminal
77
- ```
40
+ ## 让 Codex 委派任务
78
41
 
79
- 自定义私有状态目录:
42
+ 安装器安装用户级 `deepseek-worker` skill,MCP 初始化时也提供委派说明,不修改项目的 `AGENTS.md`。可以直接描述需求:
80
43
 
81
- ```sh
82
- npm run setup -- --home /absolute/path/to/private/dsh-home
83
- ```
44
+ > 为端口解析模块补充边界测试。只修改现有测试文件,不改变生产代码。请按任务选择 DeepSeek 思考强度,读取会话结果后审查 diff 并运行相关测试。
84
45
 
85
- 安装器会复用已经保存的密钥,不会覆盖它。需要更换密钥时,编辑私有 `.env` 文件,再启动新的 MCP 会话。
46
+ 需要明确指定时:
86
47
 
87
- ### Base URL 的协议要求
48
+ > $deepseek-worker 把这个独立测试任务交给 DeepSeek。你选择合适的思考强度,并负责读取结果、审查实际修改和最终验收。
88
49
 
89
- DSH 使用 **Messages API**,请求路径为 `/v1/messages`。默认根地址是:
50
+ 适合委派边界清晰的简单实现、独立测试、文档和机械修改。极小任务应直接完成。不委派架构、安全、鉴权、支付、数据库迁移、根因不明的问题或涉及秘密的任务。
90
51
 
91
- ```text
92
- https://api.deepseek.com/anthropic
93
- ```
52
+ 规则指导模型选择,**不保证每次调用**,也不取消 Codex 的批准提示。项目和用户规则仍然优先。
94
53
 
95
- 只支持 OpenAI Chat Completions 的代理地址不能直接使用。自定义地址必须支持 Messages 协议,并且不能包含用户名、密码、查询参数或片段。请只使用可信的 API 服务。
54
+ ### Codex 必须主动选择思考强度
96
55
 
97
- ## 安装器修改哪些文件
56
+ 每次调用(包括用 `session_id` 续接)都必须显式传入 `thinking_effort`,没有默认值:
98
57
 
99
- | 文件 | 用途 |
58
+ | 值 | 选择依据 |
100
59
  | --- | --- |
101
- | `~/.codex/config.toml` | 注册用户级 MCP 服务器 `dsh`。 |
102
- | `~/.agents/skills/deepseek-worker/SKILL.md` | 安装用户级委派 skill。 |
103
- | `~/.dsh-mcp/.env` | 保存 API Key。 |
104
- | `~/.dsh-mcp/settings.json` | 保存 Base URL。 |
105
-
106
- 如果设置了 `CODEX_HOME`,Codex 配置写入 `$CODEX_HOME/config.toml`。`--home` 只改变 DSH 的私有状态目录。
107
-
108
- 私有目录权限为 `0700`,密钥和设置文件权限为 `0600`。MCP 配置中不包含密钥或固定项目路径。
109
-
110
- DSH 不允许在 `.env` 中设置网络路由变量。**不要把 `DEEPSEEK_BASE_URL` 写进私有 `.env`。** 桥接器会读取 `settings.json`,再通过进程环境把地址传给 DSH。
111
-
112
- 安装器保留其他模型、Provider、MCP 和登录配置,不修改任何 `AGENTS.md`,也不向项目中写配置或 skill。
113
-
114
- 已有同名但不归安装器管理的 `dsh` 配置,或已有不同内容的同名 skill 时,安装器会报告冲突,不会覆盖。
115
-
116
- npx 安装可能把服务路径记录为 npm 缓存中的绝对路径。清理缓存、移动安装目录或更换 Node 可执行文件后,需要重新运行 `setup`。
117
-
118
- ## 如何让 Codex 自动委派
119
-
120
- 安装器会安装用户级 `deepseek-worker` skill。MCP 初始化时也会提供委派说明,不需要为每个项目追加 `AGENTS.md`。
121
-
122
- 普通任务可以直接描述需求。例如:
123
-
124
- > 为端口解析模块补充边界测试。只修改现有测试文件,不改变生产代码。完成后审查 diff 并运行相关测试。
125
-
126
- Codex 会根据规则判断是否值得委派:
60
+ | `low` | 机械修改、简单文档或明确的样板任务。 |
61
+ | `high` | 边界明确,但需要推理的实现或测试任务。 |
62
+ | `off` | 明确决定不使用思考时才选择。 |
63
+ | `max` | 额外推理成本有明确理由时才选择。 |
127
64
 
128
- - 适合:边界清晰的简单实现、独立测试、文档、样板代码和机械修改。
129
- - 不适合:架构、安全、鉴权、支付、数据库迁移、根因不明的问题,以及涉及秘密的任务。
130
- - 极小任务直接完成,避免委派成本超过实现成本。
65
+ Codex 不得静默补齐缺失值。只接受 `off`、`low`、`high`、`max`,不接受 `medium` 或 `xhigh` 等别名。这是真实的 DSH `agent-default-model.config.reasoningEffort` 配置,不是任务中的提示词。续接时也必须重新选择,不依赖旧会话的设置。
131
66
 
132
- 这些规则指导模型选择,**不保证每次调用**,也不会取消 Codex 的工具批准提示。现有项目或用户规则可能限制委派。
133
-
134
- 需要明确指定时,可以使用:
135
-
136
- > $deepseek-worker 把这个独立测试任务交给 DeepSeek。你负责审查实际修改和最终验收。
137
-
138
- ## 项目路径由谁提供
139
-
140
- 安装和启动 MCP 时不配置项目根目录。每次调用必须由 Codex 提供绝对路径:
67
+ ### 最小调用示例
141
68
 
142
69
  ```json
143
70
  {
144
71
  "project_root": "/absolute/path/to/current-project",
145
- "task": "补充指定模块的边界测试。只修改对应测试文件,复用现有测试框架,不提交 Git。报告精确验证命令和结果。",
72
+ "thinking_effort": "high",
73
+ "task": "补充指定模块的边界测试。只修改对应测试文件,复用现有测试框架,不提交 Git,不再委派。遇到未决问题停止并报告。报告精确验证命令和结果。",
146
74
  "cwd": ".",
147
75
  "permission_mode": "workspace-write",
148
76
  "timeout_seconds": 300
149
77
  }
150
78
  ```
151
79
 
152
- Codex 从当前项目上下文或用户的明确授权中取得路径。服务不会从自身工作目录、HOME、安装目录或之前的调用猜测路径。
80
+ Codex 从当前项目上下文或用户明确授权中取得路径。服务不会从自身工作目录、HOME、安装目录或上次调用猜测项目。缺少路径、相对路径、目录不存在或不是目录时,调用失败,不启动工作器。
153
81
 
154
- 缺少路径、使用相对路径、目录不存在或路径不是目录时,调用会失败,不会启动工作器。
82
+ `project_root` 是调用方对本次目录的授权,**不是服务端独立核实过的 Codex 工作区 ACL**。不要默认授权 HOME 或 `/`。
155
83
 
156
- `project_root` 是调用方对本次任务的目录授权,**不是服务端独立核实过的 Codex 工作区 ACL**。不要默认授权 HOME 或 `/`。
84
+ ## 工作器结束后,读取什么结果
157
85
 
158
- ## 工具参数与返回值
86
+ `delegate_to_deepseek` 同步执行一个任务。DSH 进程停止后,MCP 读取对应的持久化会话,核对会话 ID 和保存的实际工作目录。`answer` 来自**当前最新持久化回合中最后一条已提交的 assistant 文本**,不是 stdout 的替代摘要。
159
87
 
160
- `delegate_to_deepseek` 同步执行一个任务。一个服务器实例最多同时运行一个任务,包括不同项目的任务。重叠调用会立即报错,不会在后台排队。
88
+ 结果放在 `structuredContent` 和文本 JSON 中。Codex 必须先读取以下字段,再审查实际修改:
161
89
 
162
- | 参数 | 默认值 | 说明 |
163
- | --- | --- | --- |
164
- | `project_root` | 必填 | 本次授权的项目绝对路径。 |
165
- | `task` | 必填 | 完整目标、上下文、允许修改的文件、限制和验收标准,最多 65536 字符。 |
166
- | `cwd` | `.` | 相对 `project_root` 的目录,或其中的绝对目录。 |
167
- | `permission_mode` | `read-only` | 确需编辑时显式选择 `workspace-write`。 |
168
- | `session_id` | 无 | 续接已有 DSH 会话。 |
169
- | `timeout_seconds` | `300` | 整数,范围 1–900 秒。 |
90
+ - `answer`、`output_source: "session"`:会话中的最终文本及来源。
91
+ - `session_id`、`session_file`、`assistant_seq`、`turn`:对应会话和文本的位置元数据。
92
+ - `status`、`exit_code`、`signal`、`reason`、`diagnostics`:进程状态、终止原因和诊断。
93
+ - `terminal_reason`、`session_output_error`:持久化回合的结束原因和读取失败说明。
94
+ - `project_root`、`cwd`、`model`、`thinking_effort`、`permission_mode`:实际执行设置。
95
+ - `changed_files`、`git_root`、`change_warning`:Git 变化报告及限制。
170
96
 
171
- 服务会解析真实路径,拒绝 `cwd` 通过 `..` 或符号链接逃出本次根目录。客户端工具超时应长于工作器超时和清理时间,安装器设为 960 秒。
97
+ 隐藏思考、工具调用和工具结果都不是最终答案。若最新回合没有已提交的 assistant 文本,`answer` 为空且 `assistant_seq` 为 `null`,不能取上一回合的成功答案。读取的回合必须与本次进程的 `turn_start` 一致:续接若在新回合开始前失败,不会返回旧文本。会话缺失、校验失败、不可读或身份/目录不匹配会明确报错,不回退到 stdout。
172
98
 
173
- 结果同时放入 `structuredContent` 和文本 JSON,包含:
99
+ 读取使用 DSH 官方的只读存储接口,支持压缩会话。官方接口可能忽略未完成的物理尾帧,只返回已验证的持久化前缀。读取需要重放会话历史,大会话的读取成本会增加。
174
100
 
175
- - `status`、`exit_code`、`signal`:执行状态和进程结果。
176
- - `session_id`、`answer`、`reason`:会话、最终答案和终止原因。
177
- - `project_root`、`cwd`、`model`、`permission_mode`:实际执行设置。
178
- - `changed_files`、`git_root`、`change_warning`:Git 文件变化和报告限制。
179
- - `diagnostics`:最后 32 KiB 标准错误输出。
101
+ 只有进程退出码为 0、DSH 最终原因为 `completed`,且成功读取的对应回合也已持久化完成,服务才报告完成。**这只表示代理回合结束,不代表任务通过验收。** Codex 必须检查 `answer`、`reason`、`terminal_reason`、`session_output_error` 和 `diagnostics`,再审查实际 diff 并执行验收。工作器声称运行了检查,不等于主代理亲自运行了检查。
102
+
103
+ ### 未决问题与续接
104
+
105
+ 当前调用不实时转发中间事件,不支持途中的双向问答、批准响应或单独浏览任意会话的工具。
106
+
107
+ 1. DeepSeek 停止任务,在最终文本中报告未决问题。
108
+ 2. Codex 读取结果,在已有授权内决策,必要时询问用户。
109
+ 3. 上次调用返回后,Codex 用返回的 `session_id`、相同 `project_root` 和相同实际 `cwd` 续接。
110
+ 4. 新调用仍须显式选择 `thinking_effort`,并提供完整续接指令和权限。
111
+
112
+ 这是多轮会话续接,不是实时对话。`--approval-policy never` 拒绝需要人工批准的操作,不是自动批准。
113
+
114
+ ## 本地依赖与配置
115
+
116
+ npm 自动下载**真实、完整的本地 Harness 依赖**。不需要另行全局安装 `dsh`,不会安装桌面 App 或下载模型权重。npm 主流程不需要克隆源码,也不要求 Git 作为安装步骤。任务本身需要的工具仍须在本机可用。
117
+
118
+ ```text
119
+ 本机 Codex → MCP stdio → 本机 dsh-mcp → 本机 DSH 子进程 → 配置的 API 服务
120
+ ```
180
121
 
181
- 只有进程退出码为 0,且 DSH 的最终原因是 `completed`,服务才会报告完成。**这只表示代理回合结束,不代表通过任务验收。** 主代理仍需审查实际文件和验证结果。
122
+ 本项目不提供远程执行服务。依赖占用较大,具体磁盘占用取决于平台、依赖布局和缓存。
182
123
 
183
- ## DeepSeek 需要 Codex 决策时
124
+ 固定使用 `@deepseek-ai/dsh@0.2.2-rc.1` 和 `deepseek-flash`。DSH 仍处于 developer preview,升级时需要检查兼容性。使用官方 `headless --json` 和持久化/查询库,不依赖 DSH Web 内部接口。
184
125
 
185
- 当前调用只返回最终结果,不实时转发 DSH 的中间事件,也不支持调用途中的双向问答或批准响应。
126
+ | 文件 | 用途 |
127
+ | --- | --- |
128
+ | `~/.codex/config.toml` | 注册用户级 MCP 服务器 `dsh`。 |
129
+ | `~/.agents/skills/deepseek-worker/SKILL.md` | 安装用户级委派 skill。 |
130
+ | `~/.dsh-mcp/.env` | 保存 API Key。 |
131
+ | `~/.dsh-mcp/settings.json` | 保存 Base URL。 |
186
132
 
187
- 协作流程为:
133
+ 设置 `CODEX_HOME` 时,Codex 配置写入 `$CODEX_HOME/config.toml`。`--home` 只改变 DSH 私有状态目录。私有目录权限为 `0700`,密钥和设置文件为 `0600`。MCP 配置不包含密钥或固定项目路径。
188
134
 
189
- 1. DeepSeek 停止任务,在最终答案中说明待决问题。
190
- 2. Codex 在已有授权内决策,必要时向用户询问。
191
- 3. Codex 用返回的 `session_id`、相同 `project_root` 和相同实际 `cwd` 发起续接。
135
+ **不要把 `DEEPSEEK_BASE_URL` 写进私有 `.env`。** DSH 不允许在那里设置网络路由变量。桥接器从 `settings.json` 读取地址,再通过进程环境传入。
192
136
 
193
- 这是多轮会话续接,不是实时对话。`--approval-policy never` 会拒绝需要人工批准的操作,不是自动批准。
137
+ 安装器复用已保存的密钥,不覆盖它。更换密钥时,编辑私有 `.env` 后启动新的 MCP 会话。安装器保留其他模型、Provider、MCP 和登录配置,不向项目写入配置或 skill。
194
138
 
195
- ## 安全边界与限制
139
+ ### 更新与冲突保护
196
140
 
197
- - 工作区限制不是读取隔离。DSH 仍能读取其他可访问文件,shell 工具也不是恶意代码容器。
198
- - `workspace-write` 可以写入 `cwd`、必要的 DSH 状态和平台定义的临时目录。敏感任务应使用容器或独立账户。
199
- - 任务中的文件名单是行为约束,不是强制文件级 ACL。服务不会自动撤销越界编辑。
200
- - 同一工作区不能有并发写入者,包括主代理、编辑器、其他代理和其他 MCP 实例。需要并发时,先准备独立 worktree。
201
- - Git 变化报告比较任务前后的状态和内容指纹,不包含忽略文件,也不提供完整的子模块内部 diff。它不能归因外部并发修改。
202
- - `changed_files: null` 表示报告不可用,不表示没有修改。始终检查 `change_warning` 和实际 diff。
203
- - 超时、取消、客户端断开或进程终止时,服务清理 POSIX 进程组。自行脱离该组的后代进程不在保证内。
204
- - 服务会等待工作器退出,但退出不是回滚。失败或中断后可能留下部分修改。
205
- - NDJSON 标准输出超过 8 MiB 时,服务终止任务并报告 `output_limit`,不会截断后冒报成功。
206
- - DSH 会持久化会话和任务内容。请按敏感数据保护私有目录与诊断输出,不要在任务中放入秘密。
207
- - 服务不自动提交、发布、合并、回滚或创建 worktree,也不提供 full-access 参数。
141
+ 更新已安装配置仍使用同一条全局 setup 命令,不需要逐项目操作:
208
142
 
209
- ## 从旧的项目绑定配置升级
143
+ ```sh
144
+ npx --yes --package @hzxwonder/dsh-mcp@0.4.0 dsh-mcp setup
145
+ ```
210
146
 
211
- 旧项目中的 `.codex/config.toml` 可能覆盖新全局配置。只移除其中旧的受管理 `dsh` 区块,保留其他配置。
147
+ 安装器可以更新归它管理且未被修改的旧 skill 模板。若 skill 已定制,或缺少可核对的旧模板引用,安装器保留现有文件并报告冲突。先备份,再手动合并或处理冲突,不要直接覆盖定制内容。同名但不归安装器管理的 `dsh` 配置也不会被覆盖。
212
148
 
213
- 同时检查旧项目 skill 和 `AGENTS.md` 中固定安装目录的委派规则,只移除过时的受管理部分。安装器不会扫描或批量修改所有仓库。
149
+ npx 安装可能把服务路径记录为 npm 缓存中的绝对路径。清理缓存、移动安装目录或更换 Node 后,应重新运行 setup。
214
150
 
215
- 已有手动配置的全局 `dsh` 服务器时,需要先处理同名冲突,再运行安装器。
151
+ 旧项目的 `.codex/config.toml` 可能覆盖新全局配置。只移除旧的受管理 `dsh` 区块,保留其他内容。对旧项目 skill 或 `AGENTS.md` 中的固定路径规则也只移除过时受管理部分。安装器不扫描或批量修改所有仓库。
216
152
 
217
- ## 分发状态
153
+ ## 参数参考
218
154
 
219
- npm 用于下载程序包,MCP Registry 用于发布发现与安装元数据。两者都不负责在云端执行本机任务。
155
+ | 参数 | 默认值 | 说明 |
156
+ | --- | --- | --- |
157
+ | `project_root` | 必填 | 本次授权的项目绝对路径。 |
158
+ | `thinking_effort` | 必填 | `off`、`low`、`high` 或 `max`。续接也必填。 |
159
+ | `task` | 必填 | 完整目标、上下文、允许修改的文件、限制和验收标准,最多 65536 字符。 |
160
+ | `cwd` | `.` | 根目录内的相对目录或绝对目录。 |
161
+ | `permission_mode` | `read-only` | 确需编辑时显式选择 `workspace-write`。 |
162
+ | `session_id` | 无 | 续接对应 DSH 会话。 |
163
+ | `timeout_seconds` | `300` | 整数,范围 1–900 秒。 |
220
164
 
221
- 官方 MCP Registry 条目尚未登记。npm 发布不会自动让包进入各个客户端的商店。
165
+ 服务解析真实路径,拒绝 `cwd` 通过 `..` 或符号链接逃出本次根目录。一个服务器实例最多运行一个任务,包括跨项目任务。重叠调用立即报错,不后台排队。客户端超时应长于工作器超时和清理时间,安装器设为 960 秒。
222
166
 
223
- 当前仓库提供 `server.json`,包含两个 Provider 环境配置项。支持该元数据的客户端可以生成输入界面,但不能据此保证 Codex 原生商店会自动显示相同填写框。
167
+ ## 安全边界
224
168
 
225
- 注册信息中的 `isSecret` 是给客户端的处理提示,不等于所有客户端都会加密保存配置。推荐使用安装器的私有设置文件,或客户端提供的受保护凭据存储。
169
+ - 工作区限制不是读取隔离。DSH 可以读取其他可访问文件,shell 工具不是恶意代码容器。
170
+ - `workspace-write` 可以写入 `cwd`、必要 DSH 状态和平台定义的临时目录。敏感任务应使用容器或独立账户。
171
+ - 任务文件名单是行为约束,不是强制文件级 ACL。服务不自动撤销越界编辑。
172
+ - 同一工作区不能有并发写入者,包括主代理、编辑器、其他代理和 MCP 实例。并行任务应先准备独立 worktree,不并发修改共享 Git 元数据。
173
+ - Git 报告比较前后状态和内容指纹,不包含忽略文件或完整子模块内部 diff,也不能归因外部并发修改。
174
+ - `changed_files: null` 表示报告不可用,不表示没有修改。检查 `change_warning` 和实际 diff。
175
+ - 超时、取消、客户端断开或终止时,服务清理 POSIX 进程组。自行脱离该组的后代不在保证内。
176
+ - 服务等待工作器退出,但不回滚。失败或中断后先检查部分修改,不自动 reset、clean 或重试覆盖用户修改。
177
+ - NDJSON stdout 超过 8 MiB 时,服务终止任务并报告 `output_limit`,不会截断后冒报成功。
178
+ - DSH 持久化会话和任务内容。保护私有目录与诊断输出,不在任务中放秘密,也不公开隐藏思考。
179
+ - 工作器不得递归委派。服务不自动提交、发布、合并、回滚或创建 worktree,不提供 full-access 参数。
226
180
 
227
- 手动配置示例见 [examples/codex.toml](examples/codex.toml)。不要把真实密钥提交到项目 TOML。
181
+ ## 高级:源码安装与开发
228
182
 
229
- ## 验证
183
+ 普通 npm 用户不需要此流程。需要修改桥接器源码时:
230
184
 
231
185
  ```sh
232
- npm test
186
+ git clone https://github.com/hzxwonder/dsh-mcp.git
187
+ cd dsh-mcp
188
+ npm ci
189
+ npm run setup
233
190
  ```
234
191
 
235
- 现有检查覆盖全局配置保留、凭据权限、重复安装、冲突保护、表单安全、路径校验、跨项目执行、超时与进程清理。
192
+ 终端安装使用 `npm run setup -- --terminal`。自定义私有目录使用 `npm run setup -- --home /absolute/path/to/private/dsh-home`。
193
+
194
+ 开发检查命令为 `npm test`,当前 44 项检查全部通过。另用真实 DSH 和本地 Messages 协议桩核对了四档 effort 的实际请求、同会话续接和压缩 session 最终文本读取;从已发布的 `0.3.0` 配置升级也已验证。真实 DeepSeek 在线推理、Codex App 加载和模型自动选择工具仍需在实际环境中检查。
236
195
 
237
- 使用真实 DSH 与本地 Messages SSE 协议桩验证了文件写入、只读拒写、会话续接和私有设置加载。同一个全局 MCP 连接成功修改了两个独立项目。缺失或相对项目路径不会发起模型请求。
196
+ ## 分发与参考
238
197
 
239
- 浏览器和终端安装验证使用隔离的临时用户配置,没有修改真实用户的全局配置。
198
+ 包页面:[npm · @hzxwonder/dsh-mcp](https://www.npmjs.com/package/@hzxwonder/dsh-mcp)。普通安装用户不需要登录 npm。
240
199
 
241
- 本地协议验证不等于真实 DeepSeek 在线生成验证。Codex App 内加载和模型自动选择工具仍需在实际客户端中检查。
200
+ 官方 MCP Registry 条目尚未登记。npm 发布不自动进入客户端商店。`server.json` 提供两个 Provider 输入项,但不保证 Codex 原生商店显示相同表单。`isSecret` 只是客户端处理提示,不保证加密存储。
242
201
 
243
- ## 官方参考
202
+ 手动配置见 [examples/codex.toml](examples/codex.toml)。优先使用安装器私有文件或客户端受保护凭据存储,不把真实密钥提交到 TOML。
244
203
 
245
204
  - [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
246
205
  - [DSH Headless 凭据和 NDJSON 接口](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/bundle/headless/README.md)
247
206
  - [Codex MCP 配置](https://developers.openai.com/codex/mcp/)
248
207
  - [Codex skills](https://developers.openai.com/codex/skills/)
249
208
  - [MCP Registry 发布说明](https://modelcontextprotocol.io/registry/quickstart)
250
- - [npm 发布说明](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages)
package/config/routing.md CHANGED
@@ -1,9 +1,9 @@
1
1
  ## DeepSeek task delegation
2
2
 
3
- Preserve the current GPT model, provider, and account. Delegate only bounded, low-risk tasks. Review actual changes before acceptance.
4
-
5
- Every delegate_to_deepseek call requires an absolute project_root from Codex context or explicit user authorization.
6
- Never infer this path from the server or installation. Use read-only unless the task requires edits. Follow project and user restrictions first.
3
+ Keep the current GPT model, provider, and account.
4
+ Every delegate_to_deepseek call needs the current authorized absolute project_root and explicit thinking_effort: off, low, high, or max.
5
+ Include both on resume. Never infer the root from the server or installation. Never default effort.
6
+ Use read-only unless the task needs edits. Read the session answer, reason, and diagnostics before review. Follow user and project restrictions.
7
7
 
8
8
  - Use the installed `deepseek-worker` skill for eligible tasks.
9
9
  - Delegate worthwhile, bounded, low-risk tasks by default when the MCP tool is available.
@@ -13,6 +13,12 @@ Never infer this path from the server or installation. Use read-only unless the
13
13
  - Use the active project root unless the user explicitly authorizes a different project directory.
14
14
  - Do not infer the root from the server directory, home directory, installation, or previous calls.
15
15
  - Do not delegate when the current project root is unknown.
16
+ - Select `thinking_effort` consciously for every call, including continuation.
17
+ - Use `low` for mechanical tasks and `high` for bounded reasoning.
18
+ - Use `off` only to disable thinking deliberately.
19
+ - Use `max` only when the task justifies its additional cost.
20
+ - Never silently supply missing effort or use aliases such as `medium` or `xhigh`.
21
+ - This parameter sets DSH `agent-default-model.config.reasoningEffort`, not prompt text.
16
22
  - Each `project_root` grants directory access for that call only.
17
23
  - The server does not independently check this grant against a Codex workspace ACL.
18
24
  - This grant is not a read isolation boundary.
@@ -27,8 +33,35 @@ Never infer this path from the server or installation. Use read-only unless the
27
33
  - Require the worker to stop and report unresolved questions in its final answer.
28
34
  - Do not promise live dialogue or automatic approval responses during a call.
29
35
  - Resolve questions before a new call with the returned `session_id`, the same `project_root`, and the same `cwd`.
36
+ - Select `thinking_effort` again when you resume. Do not depend on the old session setting.
37
+ - After the worker stops, MCP reads the persisted session and checks its ID and saved physical working directory.
38
+ - Read `answer`, `output_source: "session"`, `session_id`, `session_file`, `assistant_seq`, and `turn`.
39
+ - Read `reason`, `terminal_reason`, `session_output_error`, and `diagnostics` before reviewing actual changes.
40
+ - The answer is the last committed assistant text in the latest durable turn.
41
+ - Hidden reasoning, tool calls, and tool results are not final answers.
42
+ - An empty latest turn returns empty text and a null assistant sequence, never an earlier successful answer.
43
+ - Session read failures are explicit. Do not substitute stdout or old answers.
44
+ - The persisted turn must match this invocation's turn_start event. Never return a previous turn after a failed resume.
45
+ - Do not promise a separate arbitrary-session reader.
30
46
  - Review actual changes and check evidence before acceptance.
31
47
  - Treat `completed` as Harness turn completion, not task acceptance.
32
48
  - Report unverified results and partial changes explicitly.
33
49
 
34
50
  These instructions guide delegation decisions but cannot guarantee automatic tool invocation.
51
+
52
+ Example arguments:
53
+
54
+ ```json
55
+ {
56
+ "project_root": "/absolute/authorized/project",
57
+ "thinking_effort": "low",
58
+ "task": "Correct the specified documentation typo. Preserve user changes. Do not commit or delegate. Report the changed file and any checks.",
59
+ "cwd": ".",
60
+ "permission_mode": "workspace-write"
61
+ }
62
+ ```
63
+
64
+ Global setup needs no project configuration.
65
+ The installer can upgrade an owned, unchanged previous skill template.
66
+ It preserves customized skills and skills without a previous-template reference.
67
+ Back up those files and resolve conflicts manually before replacement.
@@ -2,6 +2,7 @@
2
2
  config:
3
3
  provider: deepseek-official
4
4
  model: deepseek-flash
5
+ reasoningEffort: !!js process.env.DSH_MCP_THINKING_EFFORT
5
6
  - id: settings
6
7
  disabled: true
7
8
  - id: config-editor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzxwonder/dsh-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "mcpName": "io.github.hzxwonder/dsh-mcp",
5
5
  "type": "module",
6
6
  "description": "Delegate bounded Codex tasks to DeepSeek Harness over MCP stdio.",
@@ -35,7 +35,11 @@
35
35
  "dsh": "dsh"
36
36
  },
37
37
  "dependencies": {
38
+ "@deepseek-ai/cordis": "4.1.0",
38
39
  "@deepseek-ai/dsh": "0.2.2-rc.1",
40
+ "@deepseek-ai/dsh-session": "0.2.2-rc.1",
41
+ "@deepseek-ai/dsh-session-format-catalog": "0.2.2-rc.1",
42
+ "@deepseek-ai/dsh-session-persistence-jsonl": "0.2.2-rc.1",
39
43
  "@modelcontextprotocol/sdk": "1.32.2",
40
44
  "smol-toml": "1.9.1",
41
45
  "zod": "^4.0.0"
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.hzxwonder/dsh-mcp",
4
4
  "title": "DeepSeek Harness for Codex",
5
5
  "description": "Delegate bounded tasks to local DeepSeek Harness with an explicit project_root in every tool call.",
6
- "version": "0.3.0",
6
+ "version": "0.4.0",
7
7
  "repository": {
8
8
  "url": "https://github.com/hzxwonder/dsh-mcp",
9
9
  "source": "github"
@@ -14,7 +14,7 @@
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "@hzxwonder/dsh-mcp",
17
- "version": "0.3.0",
17
+ "version": "0.4.0",
18
18
  "runtimeHint": "npx",
19
19
  "transport": {
20
20
  "type": "stdio"
package/src/server.js CHANGED
@@ -38,6 +38,7 @@ server.registerTool('delegate_to_deepseek', {
38
38
  inputSchema: {
39
39
  project_root: z.string().min(1).describe('Required absolute project directory from Codex context or explicit user authorization. This is the caller directory grant, not an independently verified workspace ACL.'),
40
40
  task: z.string().trim().min(1).max(65536).describe('Self-contained task, allowed files, constraints, acceptance criteria and requested verification.'),
41
+ thinking_effort: z.enum(['off', 'low', 'high', 'max']).describe('Required DSH reasoning effort. Choose explicitly for every call and resume. Use low for mechanical tasks and high for bounded reasoning.'),
41
42
  cwd: z.string().min(1).default('.').describe('Directory relative to project_root, or an absolute directory within project_root.'),
42
43
  permission_mode: z.enum(['read-only', 'workspace-write']).default('read-only'),
43
44
  session_id: z.string().min(1).max(256).optional().describe('Previously returned DSH session ID for scoped revision in the same cwd.'),
@@ -0,0 +1,72 @@
1
+ import { realpath } from 'node:fs/promises';
2
+ import { isAbsolute, join } from 'node:path';
3
+ import { Context } from '@deepseek-ai/cordis';
4
+ import { Session } from '@deepseek-ai/dsh-session';
5
+ import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl';
6
+ import { currentSessionMessageProjections } from '@deepseek-ai/dsh-session-format-catalog/message-projections';
7
+
8
+ export async function readSessionOutput({ home, sessionId, cwd }) {
9
+ if (typeof sessionId !== 'string' || !sessionId) throw new Error('A persisted DSH session ID is required');
10
+ if (typeof cwd !== 'string' || !isAbsolute(cwd)) throw new Error('The session cwd must be an absolute path');
11
+ const physicalCwd = await realpath(cwd);
12
+ const ctx = new Context();
13
+ try {
14
+ await ctx.plugin(JsonlSessionPersistence, { root: join(home, 'sessions') }).await();
15
+ const persistence = ctx.sessionPersistence;
16
+ const snapshot = await persistence.stat(sessionId);
17
+ if (!snapshot?.location?.path) throw new Error('Persisted session log not found');
18
+ const handle = await persistence.open(sessionId, 'read');
19
+ let read;
20
+ try {
21
+ if (handle.header.id !== sessionId || snapshot.header.id !== sessionId) {
22
+ throw new Error('Persisted session header ID does not match the requested session');
23
+ }
24
+ if (!handle.header.cwd || await realpath(handle.header.cwd) !== physicalCwd
25
+ || snapshot.header.cwd !== handle.header.cwd) {
26
+ throw new Error('Persisted session cwd does not match the requested physical cwd');
27
+ }
28
+ read = await handle.read();
29
+ } finally {
30
+ await handle.close();
31
+ }
32
+
33
+ // Restore only in memory: the storage backend owns decoding and migrations.
34
+ const session = Session.fromRestore(sessionId, read.events, handle.header,
35
+ handle.inheritedEventCount, read.eventState, currentSessionMessageProjections);
36
+ const events = read.events;
37
+ let start = events.length - 1;
38
+ while (start >= 0 && events[start].type !== 'turn/start') start--;
39
+ if (start < 0) throw new Error('Persisted session has no durable turn');
40
+ const turn = events[start].data.turn;
41
+ const current = new Set(session.surface.nodes);
42
+ let text = '';
43
+ let assistantSeq = null;
44
+ let terminalReason = null;
45
+ for (let index = events.length - 1; index > start; index--) {
46
+ const event = events[index];
47
+ if (event.type === 'turn/end' && event.data.turn === turn && terminalReason === null) {
48
+ terminalReason = event.data.reason;
49
+ }
50
+ if (event.type !== 'assistant/message' || event.data.turn !== turn
51
+ || assistantSeq !== null || !current.has(event.seq)) continue;
52
+ const message = session.deriveEventMessage(event);
53
+ const visibleText = message?.content.filter((block) => block.type === 'text').map((block) => block.text).join('') ?? '';
54
+ if (visibleText) {
55
+ text = visibleText;
56
+ assistantSeq = event.seq;
57
+ }
58
+ }
59
+ return {
60
+ session_id: sessionId,
61
+ text,
62
+ session_file: snapshot.location.path,
63
+ assistant_seq: assistantSeq,
64
+ turn,
65
+ terminal_reason: terminalReason,
66
+ };
67
+ } catch (error) {
68
+ throw new Error(`Cannot read persisted DSH session "${sessionId}": ${error.message}`, { cause: error });
69
+ } finally {
70
+ await ctx.fiber.dispose();
71
+ }
72
+ }
package/src/setup.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { constants } from 'node:fs';
2
2
  import { chmod, lstat, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
- import { dirname, join, resolve } from 'node:path';
4
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
- import { parseArgs, parseEnv } from 'node:util';
6
+ import { isDeepStrictEqual, parseArgs, parseEnv } from 'node:util';
7
7
  import { createInterface } from 'node:readline';
8
8
  import { Writable } from 'node:stream';
9
9
  import { randomUUID } from 'node:crypto';
@@ -39,10 +39,13 @@ function managedBlock(text, begin, end, body) {
39
39
  throw new Error(`Repair the incomplete or duplicate managed block: ${begin}`);
40
40
  }
41
41
  const block = `${begin}\n${body.trimEnd()}\n${end}\n`;
42
- if (!starts.length) return text + (text && !text.endsWith('\n') ? '\n' : '') + (text ? '\n' : '') + block;
42
+ if (!starts.length) {
43
+ return { text: text + (text && !text.endsWith('\n') ? '\n' : '') + (text ? '\n' : '') + block, previous: null };
44
+ }
45
+ const previous = text.slice(starts[0].index + starts[0][0].length, ends[0].index);
43
46
  let after = ends[0].index + ends[0][0].length;
44
47
  if (text[after] === '\n') after++;
45
- return text.slice(0, starts[0].index) + block + text.slice(after);
48
+ return { text: text.slice(0, starts[0].index) + block + text.slice(after), previous };
46
49
  }
47
50
 
48
51
  function parseConfig(text) {
@@ -50,6 +53,28 @@ function parseConfig(text) {
50
53
  catch { throw new Error('Codex config.toml is not valid TOML. Repair this file before setup.'); }
51
54
  }
52
55
 
56
+ async function matchesPreviousBundledSkill(server, skillText) {
57
+ const script = server?.args?.[0];
58
+ if (typeof script !== 'string' || !isAbsolute(script)) return false;
59
+ const previousRoot = dirname(dirname(script));
60
+ if (script !== join(previousRoot, 'src', 'server.js')) return false;
61
+ try {
62
+ const [currentPackage, previousPackage, previousSkill, scriptInfo] = await Promise.all([
63
+ readExisting(join(packageRoot, 'package.json')),
64
+ readExisting(join(previousRoot, 'package.json')),
65
+ readExisting(join(previousRoot, '.agents', 'skills', 'deepseek-worker', 'SKILL.md')),
66
+ inspect(script),
67
+ ]);
68
+ const name = JSON.parse(currentPackage.text).name;
69
+ return typeof name === 'string' && name.length > 0
70
+ && JSON.parse(previousPackage.text).name === name
71
+ && scriptInfo?.isFile() === true
72
+ && previousSkill.mode !== null && previousSkill.text === skillText;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
53
78
  async function atomicWrite(path, text, mode) {
54
79
  const temp = join(dirname(path), `.dsh-mcp-${randomUUID()}.tmp`);
55
80
  try {
@@ -82,18 +107,22 @@ export async function installCodex({
82
107
  readFile(join(packageRoot, '.agents', 'skills', 'deepseek-worker', 'SKILL.md'), 'utf8'),
83
108
  ]);
84
109
  const parsed = parseConfig(config.text);
85
- if (parsed.mcp_servers?.dsh && !/^# BEGIN dsh-mcp\r?$/m.test(config.text)) {
86
- throw new Error('Codex already defines mcp_servers.dsh. Preserve or rename that server before setup.');
87
- }
88
- if (skill.mode !== null && skill.text !== bundledSkill) {
89
- throw new Error('A different deepseek-worker skill already exists. Preserve or rename that skill before setup.');
90
- }
91
110
  const args = [join(packageRoot, 'src', 'server.js'), '--dsh-home', home];
92
111
  const entry = [
93
112
  '[mcp_servers.dsh]', `command = ${JSON.stringify(process.execPath)}`, `args = ${JSON.stringify(args)}`,
94
113
  'startup_timeout_sec = 20', 'tool_timeout_sec = 960',
95
114
  ].join('\n');
96
- const nextConfig = managedBlock(config.text, '# BEGIN dsh-mcp', '# END dsh-mcp', entry);
115
+ const { text: nextConfig, previous } = managedBlock(config.text, '# BEGIN dsh-mcp', '# END dsh-mcp', entry);
116
+ const previousServer = previous === null ? undefined : parseConfig(previous).mcp_servers?.dsh;
117
+ const ownsServer = parsed.mcp_servers?.dsh && previousServer
118
+ && isDeepStrictEqual(parsed.mcp_servers.dsh, previousServer);
119
+ if (parsed.mcp_servers?.dsh && !ownsServer) {
120
+ throw new Error('Codex already defines mcp_servers.dsh outside this installer’s managed block. Preserve or rename that server before setup.');
121
+ }
122
+ if (skill.mode !== null && skill.text !== bundledSkill
123
+ && !(ownsServer && await matchesPreviousBundledSkill(previousServer, skill.text))) {
124
+ throw new Error('A different deepseek-worker skill already exists and cannot be safely upgraded. Back up or rename that skill before setup; existing files were not changed.');
125
+ }
97
126
  parseConfig(nextConfig);
98
127
  const existingKey = parseEnv(credentials.text).DEEPSEEK_API_KEY;
99
128
  const providerSettings = parseProviderSettings(provider.text);
@@ -115,7 +144,7 @@ export async function installCodex({
115
144
  if (nextProvider !== provider.text) await atomicWrite(providerPath, nextProvider, 0o600);
116
145
  else await chmod(providerPath, 0o600);
117
146
  if (nextConfig !== config.text) await atomicWrite(configPath, nextConfig, config.mode ?? 0o644);
118
- if (skill.mode === null) await atomicWrite(skillPath, bundledSkill, 0o644);
147
+ if (skill.text !== bundledSkill || skill.mode === null) await atomicWrite(skillPath, bundledSkill, skill.mode ?? 0o644);
119
148
  return { home, configPath, skillPath };
120
149
  }
121
150
 
package/src/worker.js CHANGED
@@ -5,11 +5,13 @@ import { createReadStream } from 'node:fs';
5
5
  import { lstat, readlink, realpath, stat } from 'node:fs/promises';
6
6
  import { isAbsolute, relative, resolve, sep } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { readSessionOutput } from './session-output.js';
8
9
 
9
10
  const exec = promisify(execFile);
10
11
  const dsh = fileURLToPath(new URL('./lib/bin.js', import.meta.resolve('@deepseek-ai/dsh/package.json')));
11
12
  const patch = fileURLToPath(new URL('../config/worker.patch.yml', import.meta.url));
12
13
  const MAX_OUTPUT = 8 * 1024 * 1024;
14
+ const THINKING_EFFORTS = new Set(['off', 'low', 'high', 'max']);
13
15
 
14
16
  // Wait for termination before releasing the worker; kill the process group, not just dsh.
15
17
  export function runProcess(command, { cwd, env, input, timeoutMs, signal }) {
@@ -69,6 +71,7 @@ export function parseRun(stdout, exitCode) {
69
71
  let sessionId = null;
70
72
  let final = null;
71
73
  let error = null;
74
+ let turn = null;
72
75
  for (const line of stdout.split('\n')) {
73
76
  if (!line.trim()) continue;
74
77
  let event;
@@ -76,12 +79,13 @@ export function parseRun(stdout, exitCode) {
76
79
  catch { throw new Error('Invalid DSH NDJSON output; check the pinned Harness version'); }
77
80
  if (event?.type === 'session') sessionId = event.sessionId;
78
81
  if (event?.type === 'error') error = event;
82
+ if (event?.type === 'status' && event.phase === 'turn_start') turn = event.turn;
79
83
  if (event?.type === 'final') final = event;
80
84
  }
81
85
  return {
82
86
  status: exitCode === 0 && final?.reason?.kind === 'completed' ? 'completed' : 'failed',
83
87
  session_id: final?.sessionId ?? sessionId,
84
- answer: typeof final?.text === 'string' ? final.text : '',
88
+ turn,
85
89
  reason: final?.reason ?? error ?? null,
86
90
  };
87
91
  }
@@ -119,10 +123,10 @@ async function gitSnapshot(cwd) {
119
123
  return { root, files };
120
124
  }
121
125
 
122
- export function createWorker({ home, command = [process.execPath, dsh] }) {
126
+ export function createWorker({ home, command = [process.execPath, dsh], readSession = readSessionOutput }) {
123
127
  let busy = false;
124
128
  return {
125
- async delegate({ project_root, task, cwd = '.', permission_mode = 'read-only', session_id, timeout_seconds = 300 }, { signal } = {}) {
129
+ async delegate({ project_root, task, thinking_effort, cwd = '.', permission_mode = 'read-only', session_id, timeout_seconds = 300 }, { signal } = {}) {
126
130
  if (busy) throw new Error('DeepSeek worker is busy; serialize tasks across all projects');
127
131
  busy = true;
128
132
  let before;
@@ -138,6 +142,9 @@ export function createWorker({ home, command = [process.execPath, dsh] }) {
138
142
  throw new Error('cwd is outside project_root');
139
143
  }
140
144
  if (!(await stat(directory)).isDirectory()) throw new Error('cwd must be a directory');
145
+ if (!THINKING_EFFORTS.has(thinking_effort)) {
146
+ throw new Error('thinking_effort is required and must be off, low, high, or max');
147
+ }
141
148
  before = await gitSnapshot(directory);
142
149
  const args = ['headless', '--patch', patch, '--json', '--permission-mode', permission_mode, '--approval-policy', 'never'];
143
150
  if (session_id) args.push('--session-id', session_id);
@@ -154,12 +161,26 @@ export function createWorker({ home, command = [process.execPath, dsh] }) {
154
161
  ].join('\n');
155
162
  const result = await runProcess([...command, ...args], {
156
163
  cwd: directory,
157
- env: { ...process.env, DSH_HOME: home, DSH_PERMISSION_MODE: permission_mode },
164
+ env: { ...process.env, DSH_HOME: home, DSH_PERMISSION_MODE: permission_mode, DSH_MCP_THINKING_EFFORT: thinking_effort },
158
165
  input: prompt, timeoutMs: timeout_seconds * 1000, signal,
159
166
  });
160
167
  let run;
161
168
  try { run = parseRun(result.stdout, result.exit_code); }
162
- catch (error) { run = { status: 'failed', session_id: null, answer: '', reason: error.message }; }
169
+ catch (error) { run = { status: 'failed', session_id: null, turn: null, reason: error.message }; }
170
+ let output = null;
171
+ let sessionOutputError = null;
172
+ try {
173
+ if (!run.session_id) throw new Error('The run returned no session ID for its persisted output');
174
+ const persisted = await readSession({ home, sessionId: run.session_id, cwd: directory });
175
+ if (run.turn === null || persisted.turn !== run.turn) {
176
+ throw new Error('The persisted latest turn does not match this run; no current answer is available');
177
+ }
178
+ output = persisted;
179
+ if (run.status === 'completed' && output.terminal_reason?.kind !== 'completed') {
180
+ sessionOutputError = 'The session turn did not complete durably';
181
+ }
182
+ } catch (error) { sessionOutputError = error.message; }
183
+ if (sessionOutputError && run.status === 'completed') run.status = 'failed';
163
184
  let changedFiles = null;
164
185
  let changeWarning = before ? null : 'Not a Git repository; changed_files is unavailable.';
165
186
  try {
@@ -172,6 +193,13 @@ export function createWorker({ home, command = [process.execPath, dsh] }) {
172
193
  return {
173
194
  ...run,
174
195
  status: result.failure ?? run.status,
196
+ answer: output?.text ?? '',
197
+ output_source: output ? 'session' : null,
198
+ session_file: output?.session_file ?? null,
199
+ assistant_seq: output?.assistant_seq ?? null,
200
+ terminal_reason: output?.terminal_reason ?? null,
201
+ session_output_error: sessionOutputError,
202
+ thinking_effort,
175
203
  project_root: root, cwd: directory, model: 'deepseek-flash', permission_mode,
176
204
  exit_code: result.exit_code, signal: result.signal,
177
205
  changed_files: changedFiles, git_root: before?.root ?? null,