fix: clean up OpenCode streams and allow external working directories

Completed OpenCode requests left their event streams open across repeated and
parallel conversations. Each request now closes its own subscription on exit
without cancelling peer sessions, and cancellation preserves its original cause.

Direct-model Execute commands no longer restrict cwd to the workspace. Absolute
paths, parent-relative paths, and symlinks to external directories are accepted.
The workspace remains the default and relative-path base; Ask stays read-only,
and local execution binding, directory validation, and process cleanup remain.

Add real OpenCode lifecycle and real Shell tool-loop regressions, plus matching
feature documentation. Focused checks: 182 passed, 1 skipped; typecheck and lint
passed. Full shared-worktree suite: 3542 passed, 57 skipped, 1 unrelated sidebar
heading failure, reproduced separately and documented.

Release note: 修复 OpenCode 连续及并行对话结束后事件连接未释放的问题;直连模型 Execute 命令现在可直接指定工作区外的绝对路径、相对路径或符号链接目录,Ask 只读边界保持不变。
This commit is contained in:
mesalogo
2026-09-06 19:53:42 +08:00
parent 7003141e56
commit b03c25685c
15 changed files with 394 additions and 100 deletions
@@ -78,15 +78,14 @@ validating
| 输入 | 结果 |
| --- | --- |
| 未提供 | 当前工作区根目录 |
| `.` 或合法相对子目录 | 解析到工作区内目录 |
| 绝对路径 | 拒绝 |
| `..` 越界、符号链接越界 | 拒绝 |
| `.` 或相对路径 | 以当前工作区为基准解析,可指向工作区外目录 |
| 绝对路径 | 使用指定目录 |
| `..` 或指向工作区外的符号链接 | 解析到实际目标目录,按当前用户权限执行 |
| 不存在或不是目录 | 拒绝 |
| 当前工作区身份在验证期间变化 | 拒绝本次调用 |
首版进程工具比 Execute 的 OS 权限范围更窄地固定工作目录输入,只是为了保持请求与项目
一致;命令本身仍以当前用户权限运行,可以显式访问该账号有权访问的其他绝对路径。该规则
不是 Runtime OS 沙箱,也不能宣称文件系统 containment。
工作区提供默认目录和相对路径基准,不是 Execute 命令的文件系统边界。单次命令选择其他
工作目录不会修改父请求绑定的项目或工作区,也不会影响后续未指定 `cwd` 的命令。
## 6. Shell 选择
+5 -3
View File
@@ -65,7 +65,8 @@ Subagent 使用父请求的项目、执行空间、工作模式、直连模型
- Ask 父请求只能创建 Ask Subagent。
- Execute 父请求创建 Execute Subagent。
- 子级不能切换项目、目录、Runtime、模型连接或工作模式。
- 子级不能切换父请求绑定的项目、工作区、Runtime、模型连接或工作模式;Execute 命令的
单次工作目录选择遵循 FR-3。
- 子级不再看到 `subagent_delegate`,首版最大委派深度固定为 1。
- 子任务不是新的顶层 Task 或 Conversation,结果先返回父模型。
@@ -79,7 +80,7 @@ Ask 禁止命令和写入。Execute 是用户对当前执行空间账号的完
### 4.1 包含
- 本机 Windows、macOS、Linux 的前台 Shell 命令执行。
- 当前工作区默认工作目录及合法子目录选择。
- 当前工作区默认工作目录,以及当前用户可访问的本机工作目录选择。
- Exit Code、标准输出、标准错误、Shell、目录、耗时和截断状态。
- 命令超时、用户取消、进程树停止和应用退出清理。
- 新进程使用当前本机工具环境提供的 Node、Python 和 PATH。
@@ -118,7 +119,8 @@ Ask 禁止命令和写入。Execute 是用户对当前执行空间账号的完
### FR-3 工作目录和执行位置
- 默认目录是父请求当前工作区。
- 可选工作目录必须解析为当前工作区本身或其子目录。
- 可选工作目录支持绝对路径、相对工作区路径及指向工作区外目录的路径或符号链接,
不额外限制在工作区内;目标必须是实际存在且当前用户可访问的目录。
- 本机执行只能发生在当前桌面执行空间。
- 当前请求绑定 SSH 执行空间且没有远端进程后端时不注册工具,绝不在桌面目录回退执行。
@@ -84,6 +84,26 @@
GoodBuddy 托管 Node/Python 已可用。
- 远端直连模型进程执行不在首版,后续需要独立增加 Agent 进程协议和真机验证。
## 2026-09-06 命令工作目录限制修复
- FR-3 / US-A3:移除命令工作目录的工作区范围限制。绝对路径、`..` 和指向工作区外
的符号链接均按本机文件系统解析,工作区只作为默认目录和相对路径基准。
- 保留目录存在性、本机执行空间绑定、Ask 拒绝命令、取消、超时和进程树回收。
本次没有改变工作区文件读写工具或远端 Agent 路径。
- `direct-model-process-service.test.ts` 覆盖绝对路径、相对路径、外部目录、符号链接、
不存在的目录和普通文件;当前 Windows 环境通过。
- `model-runtime.test.ts` 新增两个生产工具链回归:模型协议响应提供绝对或相对外部
`cwd`,经 `ModelAgentRuntime → ModelToolProvider → LocalDirectModelProcessService`
启动真实 PowerShell,在专用工作区外测试目录复制文件并读回验证。模型响应为本机
测试替身,没有外部模型调用;不将此记录为真实模型或 macOS/Linux 真机验收。
- 最终聚焦验证:`opencode-runtime`、`opencode-runtime-lifecycle`、
`direct-model-process-service`、`model-tool-provider`、`model-runtime` 五个测试文件
`182 passed, 1 skipped`;`npm run typecheck` 和 `npm run lint` 通过。
- 本次共享工作区的全量 `npm test` 为 `3542 passed, 57 skipped, 1 failed`。唯一失败是
`App.test.tsx` 的 `opens the global assistant sidebar and switches work tabs`,
仍期望“项目工作区”标题,单独复测同样失败;该侧栏处于其他会话的修改范围,本次未改动。
因此本次全量验证不能记录为全部通过。
## 2026-09-01 至 2026-09-02 实施与验证证据
- 新增 `direct-model-process-service.ts`:PowerShell/Bash/Sh 探测、工作区目录校验、96 KiB
@@ -89,7 +89,8 @@ const processExecuteInputSchema = z.object({
}).strict()
```
- `cwd` 只接受相对工作区路径。
- `cwd` 接受绝对路径或相对工作区路径,省略时使用工作区根目录;按本机路径语义解析
`..` 和符号链接,不限制目标位于工作区内。工具 schema 向模型明确这一规则。
- 首版不接受 `env`、`stdin`、`background`、`pty`、`shell` 或任意 executable 参数。
- Shell 选择由执行后端决定,防止模型绕过平台契约启动另一套受管接口。
- 命令字符串仍可以调用当前账号本来有权运行的程序;Execute 不增加命令白名单。
@@ -114,8 +115,8 @@ type ProcessExecuteResult = {
}
```
`cwd` 返回工作区相对显示值,不把不必要的绝对用户路径送入模型上下文。Shell 在模型结果
中只返回稳定标签;绝对可执行路径只进入 Main 诊断状态。
`cwd` 在工作区内返回相对显示值,根目录为 `.`;工作区外返回规范化绝对路径,准确说明
命令实际执行位置。Shell 在模型结果中只返回稳定标签;绝对可执行路径只进入 Main 诊断状态。
### 4.3 输出边界
@@ -293,7 +294,7 @@ DeepSeek Harness 的 Main 工具代理必须使用仅包含分配 MCP 和 Web
- `failed`:启动或契约失败。
- `cancelled`:父请求取消。
结果摘要包含 Shell、相对目录、退出码、耗时和截断标记。完整有界 stdout/stderr 进入工具
结果摘要包含 Shell、工作目录、退出码、耗时和截断标记。完整有界 stdout/stderr 进入工具
输出字段,不写应用诊断日志。
### 9.2 Subagent 事件
@@ -351,7 +352,7 @@ type SubagentActor =
- 工具可见性矩阵和 Runtime target 过滤。
- Ask 伪造进程调用拒绝。
- Windows PowerShell、POSIX Bash/Sh 选择和参数。
- cwd 默认、合法子目录、越界、符号链接越界和不存在。
- cwd 默认、相对子目录、绝对路径、工作区外目录、指向外部的符号链接和不存在的目录。
- exit 0、非零退出、spawn 失败、超时、取消和输出截断。
- 环境筛选、本机工具 PATH 优先且模型凭据不进入进程。
- 子级模式、模型、项目、执行空间和能力继承。
@@ -28,7 +28,7 @@ Job、Subjob 和 Run 继续是内部对象,不创建树形 UI、独立路由
```text
编程能力
进程执行
使用当前工作区和当前用户权限运行 PowerShell/Bash 命令。
使用当前用户权限运行 PowerShell、Bash 或 Sh 命令,默认在工作区执行,也可指定其他工作目录。
编程 Subagent
将聚焦任务委派给同一模型连接;继承当前工作模式和工作区。
```
@@ -54,7 +54,7 @@ PowerShell · packages/app · npm test
```
- 命令只显示单行有界摘要,完整参数进入可展开详情。
- 目录显示工作区相对路径;工作区根显示 `.`。
- 工作区内目录显示相对路径,工作区根显示 `.`;工作区外目录显示规范化绝对路径。
- 不在摘要中显示完整环境或凭据。
### 3.2 状态
@@ -42,12 +42,13 @@ Shell、目录、退出码、输出和耗时。
Given 当前项目位于 macOS 或 Linux 本机,When 模型运行同一工具,Then GoodBuddy 使用
Bash,Bash 不可用时使用 Sh,工具契约保持不变且实际 Shell 明确可见。
### US-A3 使用工作区子目录
### US-A3 选择命令工作目录
关联:FR-3。
Given 项目包含多个包,When 模型传入工作区内相对目录,Then 命令在该目录执行;When
路径越出工作区、不是目录或不存在,Then 命令不启动并返回准确错误。
Given 当前请求为本机 Execute,When 模型传入绝对路径或相对工作区路径,Then 命令在
解析后的目录执行,包括工作区外目录和符号链接目标,And 后续省略 `cwd` 时仍使用原工作区。
When 目标不是目录或不存在,Then 命令不启动并返回准确错误。
### US-A4 根据非零退出继续修正
@@ -239,6 +239,11 @@ Execute 直接启动已签名 Runtime:
解析为子 Agent 事件。本地 OpenCode SDK 与远端 ACP 增量工具事件复用同一转换;ACP
首帧缺少参数时可以先显示普通工具活动,后续参数确认其为 Task 后必须替换为子 Agent
状态卡,并持久化任务说明、终态与输出。
- 本机 OpenCode SDK 路径允许不同会话并行,同一会话仍按请求顺序执行。每个请求独立拥有
SSE 事件订阅,并在正常完成、错误、取消或消费方结束迭代时主动关闭自己的响应流,
不关闭其他会话的订阅或共享 Server。取消导致事件迭代结束时保留原取消原因,不改报
“事件流意外结束”。这一清理规则属于本机 SDK 路径,不改变远端 Agent 持有 ACP Prompt
的断线继续执行语义。
- 同一 detached Agent 存活时,短暂 SSH 断线依次执行 `controller/resume`、`runtime/resumeAcpChannel` 和 `runtime/replayAcpChannel`,从 Main 已确认的 cursor 后只重放 Agent 到 Main 的已记录输出。重复 frame 会被确认但不会再次交给上层。若上一代连接只留下没有活动请求的 detached binding,完成精确 controller takeover 后会先有界停止并核对遗留 Runtime process,再用新 channel epoch 重新打开同一 binding 并恢复已有 ACP session;其他 controller、未证明 takeover 或仍有活动请求的 binding 仍被拒绝。
- Main 到 Runtime 的 ACP 输入、模型请求、工具请求和 blob 不自动重放。
- 无法确认外部 Provider 是否已处理的模型调用保持结果未知,避免重复计费或重复副作用。