@deepseek-ai/dsh-terminal-bash 0.1.1-rc.2 → 0.1.2-alpha.2
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.i18n.yaml +2 -2
- package/README.md +155 -15
- package/README.zh.md +157 -17
- package/lib/index.js +172 -31
- package/lib/types/config.d.ts +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/session.d.ts +22 -1
- package/package.json +22 -20
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/terminal/terminal-bash/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 779e7802f0e7e018d19a9757707e29601926d474
|
|
6
|
+
README.zh.md: 91c7af9fb62da79e09b1baa9ce7447b91d09448e
|
package/README.md
CHANGED
|
@@ -1,39 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The shipped shell backend for persistent terminal sessions: interactive bash or pwsh under the shared sandbox policy, with readiness detection and bounded line-oriented output."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-terminal-bash
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`dsh-terminal-bash` starts a persistent interactive shell under the deployment's sandbox policy: the session stays alive across tool calls, readiness for input is detected, and bounded line-oriented output is retained for reads. It provides the `shell` backend type and supports bash on POSIX and pwsh on Windows through a `shellDialect` setting. The same backend composes with local or remote execution worlds through the mounted subprocess provider. Full-screen terminal applications are outside its line-oriented contract.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Mount this backend when a composition needs persistent shell sessions — state such as cwd, exported variables, functions, or running interactive children must survive across tool calls. It is the default `shell` type: a composition that mounts `@deepseek-ai/dsh-terminal` without it has no sessions to open.
|
|
29
|
+
|
|
30
|
+
### When to choose it
|
|
31
|
+
|
|
32
|
+
Choose this backend when work needs an interactive shell or REPL whose state persists: stepping a debugger, exploring in a Python or Node REPL, or returning to a shell after interrupting a foreground command. Choose the one-shot bash tool for bounded commands that should start and end in one call. The bash dialect targets POSIX; the pwsh dialect targets Windows hosts where `dsh-pwsh-local` can resolve a pwsh executable.
|
|
33
|
+
|
|
34
|
+
### Composition
|
|
35
|
+
|
|
36
|
+
Mount the terminal service, a subprocess provider, the sandbox and policy services, this backend, and a tool package:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
- name: '@deepseek-ai/dsh-terminal'
|
|
40
|
+
- name: '@deepseek-ai/dsh-subprocess-local'
|
|
41
|
+
- name: '@deepseek-ai/dsh-sandbox-local'
|
|
42
|
+
- name: '@deepseek-ai/dsh-sandbox-policy'
|
|
43
|
+
- name: '@deepseek-ai/dsh-terminal-bash'
|
|
44
|
+
- name: '@deepseek-ai/dsh-tool-terminal'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`danger-full-access` starts the shell directly. Confined modes require a same-world `ctx.sandbox` provider: without one, the spawn fails before the shell starts.
|
|
48
|
+
|
|
49
|
+
### Configuration
|
|
50
|
+
|
|
51
|
+
| Field | Default | Meaning |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `backendType` | `shell` | Backend type registered on `ctx.terminals` |
|
|
54
|
+
| `shellDialect` | `bash` | Interactive shell stack: `bash` or `pwsh` |
|
|
55
|
+
| `shellPath` / `shellArgs` | per dialect | Shell executable and arguments; empty selects the dialect defaults |
|
|
56
|
+
| `maxReadBytes` | `262144` | Maximum UTF-8 bytes returned by one read or settled send |
|
|
57
|
+
| `timeoutMs` | `30000` | Absolute bound on one send wait |
|
|
58
|
+
| `disposeGraceMs` | `3000` | Grace before teardown escalates to `SIGKILL` |
|
|
59
|
+
|
|
60
|
+
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-terminal-bash) is the exhaustive source for every field, including the readiness timings (`pollIntervalMs`, `exactProbeAfterMs`, `idleSilenceMs`, `handoffGraceMs`), terminal size (`rows`, `cols`), and scrollback bounds (`scrollbackLines`, `scrollbackMaxBytes`).
|
|
61
|
+
|
|
62
|
+
### Shell dialects and readiness
|
|
63
|
+
|
|
64
|
+
Both dialects expose the same readiness contract, so consumers are dialect-agnostic. A send settles when the shell is ready again: after the controlled prompt is verified, after the foreground process group provably waits on stdin (Linux), after output silence (`inferred_idle`), or at the absolute `timeoutMs`. An `inferred_idle` or `timeout` result does not prove the foreground command exited.
|
|
65
|
+
|
|
66
|
+
### Sandboxing and safe operation
|
|
67
|
+
|
|
68
|
+
The shell runs under the effective sandbox boundary for its whole life. Changing the effective sandbox mode is rejected while the owner still has open sessions or a spawn in progress — wait for creation to settle and close the sessions first, so a terminal opened with wider access cannot survive a downgrade. The backend supplies only terminal-specific environment overrides; the subprocess provider applies its shared credential scrub.
|
|
69
|
+
|
|
70
|
+
### Observable outcomes and failures
|
|
71
|
+
|
|
72
|
+
An open returns the session id and a bounded startup message. Sends settle with one of the four wait reasons and a session status; `session_exit` means the top-level shell exited. Setup failures reject the open: a missing sandbox provider in a confined mode, a shell that exits during startup, a shell that fails to reach readiness before the startup timeout, or caller cancellation. Cleanup failures reject the close instead of claiming success.
|
|
73
|
+
|
|
74
|
+
-----
|
|
75
|
+
|
|
76
|
+
<a id="understand-the-implementation"></a>
|
|
77
|
+
## Understand the implementation
|
|
78
|
+
|
|
79
|
+
<details>
|
|
80
|
+
<summary>Implementation internals — click to expand</summary>
|
|
81
|
+
|
|
82
|
+
This section explains the design behind the backend and points at the code that realizes it; the observable behavior is covered in [Use this package](#use-this-package).
|
|
83
|
+
|
|
84
|
+
### Design concept
|
|
6
85
|
|
|
7
|
-
|
|
86
|
+
One backend serves both dialects: bash and pwsh share the same session machinery — sanitizer, bounded buffers, readiness polling, cancellation, and teardown — and differ only in argv, environment, and prompt installation. Bash receives a private marker through `PS1` plus `PROMPT_COMMAND`. Pwsh writes a prompt function, pins UTF-8 console encoding, and publishes startup only after the backend reports `stdin_read`; echoed setup text cannot publish the shell. A zero-scrollback `@xterm/headless` instance consumes raw PTY data and returns terminal-protocol replies through the same handle, while the line sanitizer remains the only output projection.
|
|
8
87
|
|
|
9
|
-
|
|
88
|
+
### Source map
|
|
10
89
|
|
|
11
|
-
|
|
90
|
+
| File | Role |
|
|
91
|
+
|---|---|
|
|
92
|
+
| [`src/index.ts`](src/index.ts) | Backend registration, sandbox-mode fence, argv and environment assembly, startup sequence |
|
|
93
|
+
| [`src/config.ts`](src/config.ts) | Dialect resolution, defaults, and validation of every timing field |
|
|
94
|
+
| [`src/session.ts`](src/session.ts) | `LocalPtySession`: send lifecycle, readiness polling, scrollback, signals, close |
|
|
95
|
+
| [`src/sanitize.ts`](src/sanitize.ts) | Streaming control-sequence sanitizer and line normalization |
|
|
12
96
|
|
|
13
|
-
Readiness
|
|
97
|
+
### Readiness model
|
|
14
98
|
|
|
15
|
-
|
|
99
|
+
Three bounded tiers settle a send: exact stdin-wait evidence from the subprocess provider (Linux only), the verified private prompt marker with an exact printable tail, and output silence (`inferred_idle`); an absolute timeout always bounds the wait. Pwsh startup uses one deadline across its complete setup loop, so an `inferred_idle` follow-up does not restart the bound. Evidence collected before the provider write is discarded at the write boundary, a stdin wait that predates the write is not post-write readiness, and unknown foreground state is never a positive exact-idle signal.
|
|
16
100
|
|
|
101
|
+
### Send cancellation and teardown
|
|
102
|
+
|
|
103
|
+
Cancellation marks queued input as canceled, then signals the current foreground process group with a real `SIGINT` after any in-flight provider write settles; it never emulates interruption by writing `\x03`. Closing stops readiness polling, terminates the provider-owned process tree, awaits quiescence, and settles the active send as `session_exit`.
|
|
104
|
+
|
|
105
|
+
### Sandbox-mode fence
|
|
106
|
+
|
|
107
|
+
A write that would change the effective sandbox mode is rejected before the `sandbox/mode` event commits while that owner has an open session or a spawn in progress. The fence is attached to the exact owner and outlives a provider reload that retains existing sessions.
|
|
108
|
+
|
|
109
|
+
</details>
|
|
110
|
+
|
|
111
|
+
-----
|
|
112
|
+
|
|
113
|
+
<a id="further-exploration"></a>
|
|
114
|
+
## Further Exploration
|
|
115
|
+
|
|
116
|
+
Read these pages when the package-level contract is not enough. They move from the shared terminal model to the service, the tools, and the execution-world substrate.
|
|
117
|
+
|
|
118
|
+
- [Terminal subsystem reference](../../../docs/subsystems/terminal.md) — the service contract this backend implements and the generated `ctx.terminals` surface.
|
|
119
|
+
- [terminal service](../terminal/README.md) — backend registration, owner fencing, and cleanup semantics.
|
|
120
|
+
- [tool-terminal tools](../tool-terminal/README.md) — the model-facing tools that operate sessions.
|
|
121
|
+
- [Subprocess seam](../../../docs/subsystems/subprocess.md) — the terminal primitive that owns PTY allocation and process-tree cleanup.
|
|
122
|
+
- [Persistent PTY Agent Note](../../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.md) — the capability design and deferred boundaries.
|
|
123
|
+
- [Persistent pwsh Agent Note](../../../.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.md) — the Windows substrate and the pwsh dialect.
|
|
124
|
+
|
|
125
|
+
-----
|
|
126
|
+
|
|
127
|
+
<a id="model-experience"></a>
|
|
17
128
|
## Model Experience
|
|
18
129
|
|
|
19
|
-
###
|
|
130
|
+
### Indirect consumer
|
|
20
131
|
|
|
21
132
|
#### What the model sees
|
|
22
133
|
|
|
23
|
-
|
|
134
|
+
This package registers no prompt or tool. Through `@deepseek-ai/dsh-tool-terminal` or another PTY consumer, the model may receive bounded startup output, send deltas, scrollback pages, readiness reasons, and cleanup errors.
|
|
24
135
|
|
|
25
136
|
#### Token effect
|
|
26
137
|
|
|
27
|
-
|
|
138
|
+
Retained PTY scrollback is not placed in model history until a consumer returns bounded output.
|
|
28
139
|
|
|
29
140
|
#### KV Cache effect
|
|
30
141
|
|
|
31
|
-
|
|
142
|
+
No direct invalidation; consumer results remain append-only.
|
|
143
|
+
|
|
144
|
+
### Sandbox policy context
|
|
145
|
+
|
|
146
|
+
#### What the model sees
|
|
147
|
+
|
|
148
|
+
While this backend is composed, the `sandbox-policy` owner contributes the capability-neutral `sandbox:policy` runtime-context clause to prompts.
|
|
149
|
+
|
|
150
|
+
#### Token effect
|
|
151
|
+
|
|
152
|
+
The policy clause is present on requests while the backend is mounted.
|
|
153
|
+
|
|
154
|
+
#### KV Cache effect
|
|
155
|
+
|
|
156
|
+
A standing-policy change appends a superseding runtime-context snapshot after retained history.
|
|
32
157
|
|
|
33
158
|
## Known Limitations and Deferred Work
|
|
34
159
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
160
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
These limits define where the backend is a poor fit or needs special operational care. They are current package constraints, not a general shell comparison or a task backlog.
|
|
164
|
+
|
|
165
|
+
- **Line-oriented output only** — a headless xterm maintains control-sequence state only for terminal-protocol replies. Returned output remains normalized to lines, and full-screen alternate-buffer interaction is unsupported.
|
|
166
|
+
- **Readiness is heuristic without an exact tier** — exact stdin-wait detection depends on the mounted subprocess provider; providers that cannot prove it (macOS, Windows) settle on prompt-marker and silence/timeout readiness.
|
|
167
|
+
- **pwsh bootstrap in a constrained sandbox** — the prompt function and UTF-8 pin write through `[Console]::`, which the Windows ACL sandbox's read-only mode may deny. When that prevents marker readiness, startup rejects at `timeoutMs` instead of publishing an incomplete shell.
|
|
168
|
+
- **Cleanup guarantees belong to the provider** — process-tree teardown is the `SubprocessTerminalHandle` contract, not this backend's.
|
|
169
|
+
- **Sessions do not survive process exit** — a harness restart destroys every session.
|
|
170
|
+
|
|
171
|
+
<a id="dev-note"></a>
|
|
172
|
+
### Dev Note
|
|
173
|
+
|
|
174
|
+
<details>
|
|
175
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
176
|
+
|
|
177
|
+
None.
|
|
178
|
+
|
|
179
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,39 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "持久终端会话的随附 shell 后端:在共享沙箱策略下启动交互式 bash 或 pwsh,带就绪检测与有界逐行输出。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-terminal-bash
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`dsh-terminal-bash` 在部署的沙箱策略下启动持久交互式 shell:会话跨工具调用存活,检测 shell 何时可以接收输入,并保留有界的逐行输出供读取。它提供 `shell` 后端类型,并通过 `shellDialect` 设置在 POSIX 上支持 bash、在 Windows 上支持 pwsh。通过已挂载的子进程提供方,同一个后端既可以与本地执行世界组合,也可以与远程执行世界组合。全屏终端应用不在其逐行约定的范围内。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
当组合需要持久 shell 会话时挂载此后端——cwd、导出的变量、函数或正在运行的交互式子进程等状态必须跨工具调用存活。它是默认的 `shell` 类型:组合只挂载 `@deepseek-ai/dsh-terminal` 而不挂载它时,将没有任何会话可打开。
|
|
29
|
+
|
|
30
|
+
### 何时选择
|
|
31
|
+
|
|
32
|
+
当工作需要状态持续存在的交互式 shell 或 REPL 时选择此后端:逐步调试 gdb、在 Python 或 Node REPL 中探索,或中断前台命令后回到 shell。对于应当一次调用即开始并结束的有界命令,请选择单次 bash 工具。bash 方言面向 POSIX;pwsh 方言面向 `dsh-pwsh-local` 能解析出 pwsh 可执行文件的 Windows 主机。
|
|
33
|
+
|
|
34
|
+
### 组合方式
|
|
35
|
+
|
|
36
|
+
挂载终端服务、子进程提供方、沙箱与策略服务、此后端以及一个工具包:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
- name: '@deepseek-ai/dsh-terminal'
|
|
40
|
+
- name: '@deepseek-ai/dsh-subprocess-local'
|
|
41
|
+
- name: '@deepseek-ai/dsh-sandbox-local'
|
|
42
|
+
- name: '@deepseek-ai/dsh-sandbox-policy'
|
|
43
|
+
- name: '@deepseek-ai/dsh-terminal-bash'
|
|
44
|
+
- name: '@deepseek-ai/dsh-tool-terminal'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`danger-full-access` 直接启动 shell。受限模式要求同一执行世界中存在 `ctx.sandbox` 提供方:缺少时,spawn 会在 shell 启动前失败。
|
|
48
|
+
|
|
49
|
+
### 配置
|
|
50
|
+
|
|
51
|
+
| 字段 | 默认值 | 含义 |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `backendType` | `shell` | 注册到 `ctx.terminals` 的后端类型 |
|
|
54
|
+
| `shellDialect` | `bash` | 交互式 shell 栈:`bash` 或 `pwsh` |
|
|
55
|
+
| `shellPath` / `shellArgs` | 按方言 | shell 可执行文件与参数;为空时选择方言默认值 |
|
|
56
|
+
| `maxReadBytes` | `262144` | 一次读取或一次结算发送返回的最大 UTF-8 字节数 |
|
|
57
|
+
| `timeoutMs` | `30000` | 一次发送等待的绝对上限 |
|
|
58
|
+
| `disposeGraceMs` | `3000` | 清理升级到 `SIGKILL` 前的宽限时间 |
|
|
59
|
+
|
|
60
|
+
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-terminal-bash)是每个字段的穷尽式真源,包括就绪计时(`pollIntervalMs`、`exactProbeAfterMs`、`idleSilenceMs`、`handoffGraceMs`)、终端尺寸(`rows`、`cols`)与 scrollback 上限(`scrollbackLines`、`scrollbackMaxBytes`)。
|
|
61
|
+
|
|
62
|
+
### shell 方言与就绪
|
|
63
|
+
|
|
64
|
+
两种方言暴露相同的就绪约定,因此消费方与方言无关。当 shell 再次就绪时发送即结算:受控提示符被验证之后、前台进程组被证明在等待 stdin(Linux)之后、输出静默(`inferred_idle`)之后,或到达绝对 `timeoutMs`。`inferred_idle` 或 `timeout` 结果并不证明前台命令已退出。
|
|
65
|
+
|
|
66
|
+
### 沙箱与安全运行
|
|
67
|
+
|
|
68
|
+
shell 在整个生命周期内运行在有效的沙箱边界之下。当所有者仍有打开的会话或进行中的 spawn 时,改变有效沙箱模式会被拒绝——请先等待创建完成并关闭会话,避免以更宽权限打开的终端在权限降级后继续存在。后端只提供终端专属的环境覆盖;共享凭据清理由子进程提供方负责。
|
|
69
|
+
|
|
70
|
+
### 可观察结果与失败
|
|
71
|
+
|
|
72
|
+
打开会返回会话 id 与有界启动消息。发送以四种等待原因之一与一个会话状态结算;`session_exit` 表示顶层 shell 已退出。设置失败会拒绝打开:受限模式下缺少沙箱提供方、shell 在启动期间退出、shell 未能在启动超时前达到就绪,或调用方取消。清理失败会拒绝关闭,而不是声称成功。
|
|
73
|
+
|
|
74
|
+
-----
|
|
75
|
+
|
|
76
|
+
<a id="understand-the-implementation"></a>
|
|
77
|
+
## 理解实现
|
|
78
|
+
|
|
79
|
+
<details>
|
|
80
|
+
<summary>实现细节——点击展开</summary>
|
|
81
|
+
|
|
82
|
+
本节解释后端背后的设计并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中说明。
|
|
83
|
+
|
|
84
|
+
### 设计理念
|
|
6
85
|
|
|
7
|
-
|
|
86
|
+
一个后端服务两种方言:bash 与 pwsh 共享同一套会话机制——清理器、有界缓冲区、就绪轮询、取消与关闭——只在 argv、环境与提示符安装方式上不同。bash 通过 `PS1` 加 `PROMPT_COMMAND` 接收私有标记。pwsh 会写入提示符函数、钉住 UTF-8 控制台编码,并只在后端报告 `stdin_read` 后发布启动;回显的设置文本不能发布 shell。一个不保留 scrollback 的 `@xterm/headless` 实例会消费原始 PTY 数据,并通过同一句柄返回终端协议响应;逐行 sanitizer 仍是唯一输出投影。
|
|
8
87
|
|
|
9
|
-
|
|
88
|
+
### 源码地图
|
|
10
89
|
|
|
11
|
-
|
|
90
|
+
| 文件 | 职责 |
|
|
91
|
+
|---|---|
|
|
92
|
+
| [`src/index.ts`](src/index.ts) | 后端注册、沙箱模式限制、argv 与环境组装、启动序列 |
|
|
93
|
+
| [`src/config.ts`](src/config.ts) | 方言解析、默认值与每个计时字段的校验 |
|
|
94
|
+
| [`src/session.ts`](src/session.ts) | `LocalPtySession`:发送生命周期、就绪轮询、scrollback、信号、关闭 |
|
|
95
|
+
| [`src/sanitize.ts`](src/sanitize.ts) | 流式控制序列清理器与行规范化 |
|
|
12
96
|
|
|
13
|
-
|
|
97
|
+
### 就绪模型
|
|
14
98
|
|
|
15
|
-
|
|
99
|
+
三个有界档位结算一次发送:来自子进程提供方的精确 stdin 等待证据(仅 Linux)、带精确可打印尾部的受控私有提示符标记,以及输出静默(`inferred_idle`);绝对超时始终限制等待。pwsh 启动的完整设置循环共用一条 deadline,因此 `inferred_idle` 后续发送不会重新计时。提供方写入前收集的证据会在写入边界丢弃,早于写入的 stdin 等待不算写入后就绪,未知的前台状态绝不是精确空闲的正向信号。
|
|
16
100
|
|
|
101
|
+
### 发送取消与关闭
|
|
102
|
+
|
|
103
|
+
取消先把排队输入标记为已取消,再在任何在途的提供方写入结算后向当前前台进程组发送真正的 `SIGINT`;它绝不会通过写入 `\x03` 模拟中断。关闭会停止就绪轮询、终止提供方拥有的进程树、等待完全停稳,并把活跃发送结算为 `session_exit`。
|
|
104
|
+
|
|
105
|
+
### 沙箱模式限制
|
|
106
|
+
|
|
107
|
+
当所有者存在打开的会话或进行中的 spawn 时,凡是会改变有效沙箱模式的写入都会在 `sandbox/mode` 事件提交前被拒绝。该限制绑定到确切所有者,并在保留现有会话的提供方重新加载后依然有效。
|
|
108
|
+
|
|
109
|
+
</details>
|
|
110
|
+
|
|
111
|
+
-----
|
|
112
|
+
|
|
113
|
+
<a id="further-exploration"></a>
|
|
114
|
+
## 进一步探索
|
|
115
|
+
|
|
116
|
+
当包级约定不够用时阅读以下页面。它们从共享终端模型进入服务、工具与执行世界基底。
|
|
117
|
+
|
|
118
|
+
- [终端子系统参考](../../../docs/subsystems/terminal.zh.md)——此后端实现的服务器约定与生成的 `ctx.terminals` 接口面。
|
|
119
|
+
- [terminal 服务](../terminal/README.zh.md)——后端注册、所有者限制与清理语义。
|
|
120
|
+
- [tool-terminal 工具](../tool-terminal/README.zh.md)——操作会话的面向模型工具。
|
|
121
|
+
- [子进程 seam](../../../docs/subsystems/subprocess.zh.md)——负责 PTY 分配与进程树清理的终端原语。
|
|
122
|
+
- [持久 PTY Agent Note](../../../.agents/notes/implemented/feature/2026-07-16-persistent-pty-sessions.zh.md)——能力设计与暂缓边界。
|
|
123
|
+
- [持久 pwsh Agent Note](../../../.agents/notes/implemented/architecture/2026-08-11-pwsh-persistent-pty.zh.md)——Windows 基底与 pwsh 方言。
|
|
124
|
+
|
|
125
|
+
-----
|
|
126
|
+
|
|
127
|
+
<a id="model-experience"></a>
|
|
17
128
|
## 模型体验
|
|
18
129
|
|
|
19
|
-
###
|
|
130
|
+
### 间接消费方
|
|
131
|
+
|
|
132
|
+
#### 模型看到什么
|
|
133
|
+
|
|
134
|
+
此包不注册提示词或工具。模型通过 `@deepseek-ai/dsh-tool-terminal` 或其他 PTY 消费方可能收到有界的启动输出、发送增量、scrollback 页、就绪原因与清理错误。
|
|
135
|
+
|
|
136
|
+
#### Token 影响
|
|
137
|
+
|
|
138
|
+
在消费方返回有界输出之前,保留的 PTY scrollback 不会进入模型历史。
|
|
139
|
+
|
|
140
|
+
#### KV Cache 影响
|
|
20
141
|
|
|
21
|
-
|
|
142
|
+
不会直接失效;消费方结果保持仅追加。
|
|
22
143
|
|
|
23
|
-
|
|
144
|
+
### 沙箱策略上下文
|
|
145
|
+
|
|
146
|
+
#### 模型看到什么
|
|
147
|
+
|
|
148
|
+
组合此后端期间,`sandbox-policy` 归属方会向提示词贡献与具体能力无关的 `sandbox:policy` 运行时上下文子句。
|
|
24
149
|
|
|
25
150
|
#### Token 影响
|
|
26
151
|
|
|
27
|
-
|
|
152
|
+
后端挂载期间,请求中会包含该策略子句。
|
|
28
153
|
|
|
29
154
|
#### KV Cache 影响
|
|
30
155
|
|
|
31
|
-
|
|
156
|
+
常驻策略发生变化时,会在保留的历史之后追加一份取代先前状态的运行时上下文快照。
|
|
157
|
+
|
|
158
|
+
## 已知限制与延期工作
|
|
159
|
+
|
|
160
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
这些限制说明后端何时不合适或需要特别的运维注意。它们是当前包约束,不是通用 shell 对比或任务积压。
|
|
164
|
+
|
|
165
|
+
- **仅逐行输出**——headless xterm 只为终端协议响应维护控制序列状态。返回输出仍按行规范化;不支持全屏备用缓冲区交互。
|
|
166
|
+
- **没有精确档时,就绪是启发式的**——精确 stdin 等待检测取决于已挂载的子进程提供方;无法证明该状态的提供方(macOS、Windows)按提示符标记与静默/超时就绪结算。
|
|
167
|
+
- **受限沙箱中的 pwsh 引导**——提示符函数与 UTF-8 钉通过 `[Console]::` 写入,Windows ACL 沙箱的只读模式可能拒绝。若因此无法获得 marker 就绪,启动会在 `timeoutMs` 到期时拒绝,而不会发布不完整的 shell。
|
|
168
|
+
- **清理保证属于提供方**——进程树清理是 `SubprocessTerminalHandle` 的约定,而不是此后端的。
|
|
169
|
+
- **会话不随进程退出存活**——harness 重启会销毁所有会话。
|
|
170
|
+
|
|
171
|
+
<a id="dev-note"></a>
|
|
172
|
+
### 开发备注
|
|
173
|
+
|
|
174
|
+
<details>
|
|
175
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
32
176
|
|
|
33
|
-
|
|
177
|
+
无。
|
|
34
178
|
|
|
35
|
-
|
|
36
|
-
- 精确 stdin 等待检测取决于已挂载的进程管理提供方;无法证明该状态的提供方使用提示符标记和静默/超时就绪机制。Windows 正是这样的提供方:shell pid 是伪前台进程组,没有精确的 stdin-wait 档,因此无标记的子进程按静默上限结算。
|
|
37
|
-
- pwsh 引导(UTF-8 编码钉与 `prompt` 函数)通过 `[Console]::` 写入,Windows ACL 沙箱的只读模式(ConstrainedLanguage)可能拒绝它。shell 仍可通过受控可打印提示符和静默档结算,但无法使用 marker 就绪,非 ASCII 输出也可能沿用宿主代码页。
|
|
38
|
-
- 清理保证以 `SubprocessTerminalHandle` 的保证为准;提供方特定的缺口属于该实现的约定,而非这个 PTY 消费方。
|
|
39
|
-
- harness 进程退出后,会话无法继续存在。
|
|
179
|
+
</details>
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import { TerminalBackendCleanupError, TerminalError } from "@deepseek-ai/dsh-terminal";
|
|
2
|
-
import { effectiveSandboxMode } from "@deepseek-ai/dsh-sandbox-policy";
|
|
3
3
|
import { ENCODING_PREAMBLE, resolvePwshPath } from "@deepseek-ai/dsh-pwsh-local";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { Buffer } from "node:buffer";
|
|
@@ -231,7 +231,8 @@ function normalizeTerminalText(text) {
|
|
|
231
231
|
}
|
|
232
232
|
//#endregion
|
|
233
233
|
//#region lib/types/session.js
|
|
234
|
-
/** Persistent PTY session
|
|
234
|
+
/** Persistent PTY session with bounded output, readiness, and terminal-protocol replies. */
|
|
235
|
+
const { Terminal: HeadlessTerminal } = createRequire(import.meta.url)("@xterm/headless");
|
|
235
236
|
function utf8Tail(text, maxBytes) {
|
|
236
237
|
if (Buffer.byteLength(text) <= maxBytes) return {
|
|
237
238
|
text,
|
|
@@ -361,6 +362,9 @@ var LocalPtySession = class {
|
|
|
361
362
|
motd = "";
|
|
362
363
|
pid;
|
|
363
364
|
decoder = new TextDecoder();
|
|
365
|
+
/** Protocol state only; the sanitizer and bounded buffers own returned text. */
|
|
366
|
+
emulator;
|
|
367
|
+
emulatorData;
|
|
364
368
|
sanitizer;
|
|
365
369
|
scrollback;
|
|
366
370
|
outputEnded = Promise.withResolvers();
|
|
@@ -383,10 +387,34 @@ var LocalPtySession = class {
|
|
|
383
387
|
closing = false;
|
|
384
388
|
closePromise;
|
|
385
389
|
transportFailure;
|
|
390
|
+
emulatorWrites = Promise.resolve();
|
|
391
|
+
emulatorWriteDone;
|
|
392
|
+
emulatorBuffer = "";
|
|
393
|
+
emulatorWriting = false;
|
|
394
|
+
responseWrites = Promise.resolve();
|
|
395
|
+
pendingResponseWrites = 0;
|
|
396
|
+
emulatorClosed = false;
|
|
386
397
|
constructor(terminal, config) {
|
|
387
398
|
this.terminal = terminal;
|
|
388
399
|
this.config = config;
|
|
389
400
|
this.pid = terminal.pid;
|
|
401
|
+
this.emulator = new HeadlessTerminal({
|
|
402
|
+
cols: config.cols,
|
|
403
|
+
rows: config.rows,
|
|
404
|
+
scrollback: 0
|
|
405
|
+
});
|
|
406
|
+
this.emulatorData = this.emulator.onData((data) => {
|
|
407
|
+
this.pendingResponseWrites += 1;
|
|
408
|
+
const response = this.responseWrites.then(async () => {
|
|
409
|
+
await this.terminal.write(data);
|
|
410
|
+
});
|
|
411
|
+
this.responseWrites = response.then(() => {
|
|
412
|
+
this.finishResponseWrite();
|
|
413
|
+
}, (error) => {
|
|
414
|
+
this.finishResponseWrite();
|
|
415
|
+
if (!this.emulatorClosed && !this.closing) this.onTransportFailure(error);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
390
418
|
this.sanitizer = new TerminalSanitizer(config.maxReadBytes);
|
|
391
419
|
this.scrollback = new BoundedTextBuffer(config.scrollbackMaxBytes, config.scrollbackLines);
|
|
392
420
|
terminal.output.on("data", this.onTerminalData);
|
|
@@ -437,7 +465,7 @@ var LocalPtySession = class {
|
|
|
437
465
|
this.activeAbort = () => request.signal?.removeEventListener("abort", onAbort);
|
|
438
466
|
}
|
|
439
467
|
this.activeDeadlineTimer = setTimeout(() => {
|
|
440
|
-
if (this.active === operation) this.settleActive("timeout", this.activeWrite !== void 0 || this.interrupting === operation);
|
|
468
|
+
if (this.active === operation) this.settleActive("timeout", this.activeWrite !== void 0 || this.interrupting === operation || this.protocolWorkPending());
|
|
441
469
|
}, this.config.timeoutMs);
|
|
442
470
|
this.beginSend(operation, request);
|
|
443
471
|
return operation;
|
|
@@ -445,8 +473,13 @@ var LocalPtySession = class {
|
|
|
445
473
|
async beginSend(operation, request) {
|
|
446
474
|
let foreground;
|
|
447
475
|
try {
|
|
476
|
+
if (this.protocolWorkPending()) await this.drainTerminalProtocol();
|
|
477
|
+
const emulatorWrites = this.emulatorWrites;
|
|
478
|
+
const responseWrites = this.responseWrites;
|
|
448
479
|
foreground = await this.terminal.inspectForeground();
|
|
480
|
+
if (this.protocolStateChanged(emulatorWrites, responseWrites)) foreground = await this.inspectForegroundAfterProtocol();
|
|
449
481
|
} catch (error) {
|
|
482
|
+
if (this.protocolWorkPending()) await this.drainTerminalProtocol();
|
|
450
483
|
if (this.active === operation && !this.closing && this.interrupting !== operation) this.failActive(error);
|
|
451
484
|
return;
|
|
452
485
|
}
|
|
@@ -466,7 +499,7 @@ var LocalPtySession = class {
|
|
|
466
499
|
}
|
|
467
500
|
if (operation.cancelRequested) return;
|
|
468
501
|
if (this.active === operation && operation.settled) {
|
|
469
|
-
this.
|
|
502
|
+
this.releaseSettledActive();
|
|
470
503
|
return;
|
|
471
504
|
}
|
|
472
505
|
if (this.active === operation && !this.closing) {
|
|
@@ -474,7 +507,7 @@ var LocalPtySession = class {
|
|
|
474
507
|
this.schedulePoll(operation);
|
|
475
508
|
}
|
|
476
509
|
} catch (error) {
|
|
477
|
-
if (this.active === operation && !this.closing) if (operation.settled) this.
|
|
510
|
+
if (this.active === operation && !this.closing) if (operation.settled) this.releaseSettledActive();
|
|
478
511
|
else this.failActive(error);
|
|
479
512
|
}
|
|
480
513
|
}
|
|
@@ -534,14 +567,18 @@ var LocalPtySession = class {
|
|
|
534
567
|
}
|
|
535
568
|
onTerminalData = (chunk) => {
|
|
536
569
|
const bytes = typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
537
|
-
this.
|
|
570
|
+
const data = this.decoder.decode(bytes, { stream: true });
|
|
571
|
+
this.queueEmulatorData(data);
|
|
572
|
+
this.onData(data);
|
|
538
573
|
};
|
|
539
574
|
onTerminalEnd = () => {
|
|
540
575
|
this.onData(this.decoder.decode());
|
|
541
576
|
this.appendOutput(this.sanitizer.flush());
|
|
577
|
+
this.closeEmulator();
|
|
542
578
|
this.outputEnded.resolve();
|
|
543
579
|
};
|
|
544
580
|
onTerminalError = (error) => {
|
|
581
|
+
this.closeEmulator();
|
|
545
582
|
this.onTransportFailure(error);
|
|
546
583
|
this.outputEnded.resolve();
|
|
547
584
|
};
|
|
@@ -578,6 +615,7 @@ var LocalPtySession = class {
|
|
|
578
615
|
exitCode: null,
|
|
579
616
|
signal: null
|
|
580
617
|
};
|
|
618
|
+
this.closeEmulator();
|
|
581
619
|
this.failActive(failure);
|
|
582
620
|
this.terminal.terminate().catch(() => {});
|
|
583
621
|
}
|
|
@@ -603,7 +641,11 @@ var LocalPtySession = class {
|
|
|
603
641
|
this.settleActive("session_exit");
|
|
604
642
|
return;
|
|
605
643
|
}
|
|
606
|
-
|
|
644
|
+
if (this.protocolWorkPending()) await this.drainTerminalProtocol();
|
|
645
|
+
const emulatorWrites = this.emulatorWrites;
|
|
646
|
+
const responseWrites = this.responseWrites;
|
|
647
|
+
let foreground = await this.terminal.inspectForeground();
|
|
648
|
+
if (this.protocolStateChanged(emulatorWrites, responseWrites)) foreground = await this.inspectForegroundAfterProtocol();
|
|
607
649
|
if (this.active !== operation || this.closing || this.interrupting === operation) return;
|
|
608
650
|
const idleFor = Date.now() - this.lastOutputAt;
|
|
609
651
|
if (this.promptSeen && foreground !== void 0 && this.shellPgid === void 0) this.shellPgid = foreground.processGroupId;
|
|
@@ -621,6 +663,7 @@ var LocalPtySession = class {
|
|
|
621
663
|
const handoffGrace = this.promptSeen ? this.config.handoffGraceMs : 0;
|
|
622
664
|
if (startupHasOutput && idleFor >= this.config.idleSilenceMs + handoffGrace) this.settleActive("inferred_idle");
|
|
623
665
|
} catch (error) {
|
|
666
|
+
if (this.protocolWorkPending()) await this.drainTerminalProtocol();
|
|
624
667
|
if (this.active === operation && !this.closing && this.interrupting !== operation) this.failActive(error);
|
|
625
668
|
} finally {
|
|
626
669
|
this.polling = false;
|
|
@@ -628,6 +671,91 @@ var LocalPtySession = class {
|
|
|
628
671
|
if (active !== void 0 && this.pollingReady === active) this.schedulePoll(active);
|
|
629
672
|
}
|
|
630
673
|
}
|
|
674
|
+
/** Wait until generated replies reach the provider before another send can publish. */
|
|
675
|
+
async drainTerminalProtocol() {
|
|
676
|
+
for (;;) {
|
|
677
|
+
const emulatorWrites = this.emulatorWrites;
|
|
678
|
+
await emulatorWrites;
|
|
679
|
+
const responseWrites = this.responseWrites;
|
|
680
|
+
await responseWrites;
|
|
681
|
+
if (emulatorWrites === this.emulatorWrites && responseWrites === this.responseWrites && !this.protocolWorkPending()) return;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/** Sample foreground state only after protocol replies are quiet for the entire inspection. */
|
|
685
|
+
async inspectForegroundAfterProtocol() {
|
|
686
|
+
for (;;) {
|
|
687
|
+
if (this.protocolWorkPending()) await this.drainTerminalProtocol();
|
|
688
|
+
const emulatorWrites = this.emulatorWrites;
|
|
689
|
+
const responseWrites = this.responseWrites;
|
|
690
|
+
const foreground = await this.terminal.inspectForeground();
|
|
691
|
+
if (!this.protocolStateChanged(emulatorWrites, responseWrites)) return foreground;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
protocolStateChanged(emulatorWrites, responseWrites) {
|
|
695
|
+
return emulatorWrites !== this.emulatorWrites || responseWrites !== this.responseWrites || this.protocolWorkPending();
|
|
696
|
+
}
|
|
697
|
+
protocolWorkPending() {
|
|
698
|
+
return this.emulatorWriteDone !== void 0 || this.pendingResponseWrites > 0;
|
|
699
|
+
}
|
|
700
|
+
queueEmulatorData(data) {
|
|
701
|
+
if (this.emulatorClosed) return;
|
|
702
|
+
this.emulatorBuffer += data;
|
|
703
|
+
if (this.emulatorWriteDone === void 0) {
|
|
704
|
+
const idle = Promise.withResolvers();
|
|
705
|
+
this.emulatorWrites = idle.promise;
|
|
706
|
+
this.emulatorWriteDone = () => {
|
|
707
|
+
idle.resolve(void 0);
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
this.pumpEmulator();
|
|
711
|
+
}
|
|
712
|
+
pumpEmulator() {
|
|
713
|
+
if (this.emulatorWriting || this.emulatorClosed) return;
|
|
714
|
+
if (this.emulatorBuffer.length === 0) {
|
|
715
|
+
const done = this.emulatorWriteDone;
|
|
716
|
+
this.emulatorWriteDone = void 0;
|
|
717
|
+
done?.();
|
|
718
|
+
this.releaseSettledActive();
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const data = this.emulatorBuffer;
|
|
722
|
+
this.emulatorBuffer = "";
|
|
723
|
+
this.emulatorWriting = true;
|
|
724
|
+
try {
|
|
725
|
+
this.emulator.write(data, () => {
|
|
726
|
+
this.emulatorWriting = false;
|
|
727
|
+
this.pumpEmulator();
|
|
728
|
+
});
|
|
729
|
+
} catch (error) {
|
|
730
|
+
this.emulatorWriting = false;
|
|
731
|
+
this.emulatorBuffer = "";
|
|
732
|
+
const done = this.emulatorWriteDone;
|
|
733
|
+
this.emulatorWriteDone = void 0;
|
|
734
|
+
done?.();
|
|
735
|
+
this.releaseSettledActive();
|
|
736
|
+
if (!this.closing) this.onTransportFailure(error);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
finishResponseWrite() {
|
|
740
|
+
this.pendingResponseWrites -= 1;
|
|
741
|
+
this.releaseSettledActive();
|
|
742
|
+
}
|
|
743
|
+
releaseSettledActive() {
|
|
744
|
+
const operation = this.active;
|
|
745
|
+
if (operation === void 0 || !operation.settled || this.activeWrite !== void 0 || this.interrupting === operation || this.protocolWorkPending()) return;
|
|
746
|
+
this.clearActive();
|
|
747
|
+
}
|
|
748
|
+
closeEmulator() {
|
|
749
|
+
if (this.emulatorClosed) return;
|
|
750
|
+
this.emulatorClosed = true;
|
|
751
|
+
this.emulatorBuffer = "";
|
|
752
|
+
this.emulatorWriting = false;
|
|
753
|
+
const done = this.emulatorWriteDone;
|
|
754
|
+
this.emulatorWriteDone = void 0;
|
|
755
|
+
done?.();
|
|
756
|
+
this.emulatorData.dispose();
|
|
757
|
+
this.emulator.dispose();
|
|
758
|
+
}
|
|
631
759
|
settleActive(waitReason, retainOwnership = false) {
|
|
632
760
|
const operation = this.active;
|
|
633
761
|
if (operation === void 0) return;
|
|
@@ -681,7 +809,7 @@ var LocalPtySession = class {
|
|
|
681
809
|
} finally {
|
|
682
810
|
if (this.interrupting === operation) this.interrupting = void 0;
|
|
683
811
|
}
|
|
684
|
-
if (this.active === operation && operation.settled) this.
|
|
812
|
+
if (this.active === operation && operation.settled) this.releaseSettledActive();
|
|
685
813
|
else if (this.active === operation && !this.closing) {
|
|
686
814
|
this.pollingReady = operation;
|
|
687
815
|
this.schedulePoll(operation, 0);
|
|
@@ -689,6 +817,7 @@ var LocalPtySession = class {
|
|
|
689
817
|
}
|
|
690
818
|
async closeOnce(reason) {
|
|
691
819
|
this.stopPolling();
|
|
820
|
+
this.closeEmulator();
|
|
692
821
|
try {
|
|
693
822
|
await this.terminal.terminate();
|
|
694
823
|
} catch (error) {
|
|
@@ -711,10 +840,11 @@ var LocalPtySession = class {
|
|
|
711
840
|
*/
|
|
712
841
|
/** Cordis plugin name. */
|
|
713
842
|
const name = "terminal-bash";
|
|
714
|
-
/** Required services:
|
|
843
|
+
/** Required services: terminal registry, shared confinement policy, projection registry, and process substrate. */
|
|
715
844
|
const inject = [
|
|
716
845
|
"terminals",
|
|
717
846
|
"sandboxPolicy",
|
|
847
|
+
"sessionProjections",
|
|
718
848
|
"subprocess"
|
|
719
849
|
];
|
|
720
850
|
const sandboxModeFences = /* @__PURE__ */ new WeakMap();
|
|
@@ -723,18 +853,20 @@ function ensureSandboxModeFence(ctx, owner) {
|
|
|
723
853
|
if (existing !== void 0) {
|
|
724
854
|
existing.pty = ctx.terminals;
|
|
725
855
|
existing.sandboxPolicy = ctx.sandboxPolicy;
|
|
856
|
+
existing.sessionProjections = ctx.sessionProjections;
|
|
726
857
|
return;
|
|
727
858
|
}
|
|
728
859
|
const state = {
|
|
729
860
|
pty: ctx.terminals,
|
|
730
|
-
sandboxPolicy: ctx.sandboxPolicy
|
|
861
|
+
sandboxPolicy: ctx.sandboxPolicy,
|
|
862
|
+
sessionProjections: ctx.sessionProjections
|
|
731
863
|
};
|
|
732
864
|
sandboxModeFences.set(owner, state);
|
|
733
865
|
owner.ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
734
866
|
if (eventName !== "session/event") return;
|
|
735
867
|
const [session, event] = args;
|
|
736
868
|
if (session !== owner.session || event.type !== "sandbox/mode") return;
|
|
737
|
-
const currentMode =
|
|
869
|
+
const currentMode = state.sessionProjections.stateOf(session, "sandboxMode") ?? null ?? state.sandboxPolicy.defaultMode;
|
|
738
870
|
if (event.data.mode === currentMode || !state.pty.hasOwnerActivity(owner)) return;
|
|
739
871
|
throw new Error(`cannot change sandbox mode from "${currentMode}" to "${event.data.mode}" while persistent terminal sessions are open or being created; wait for creation to settle and close them first`);
|
|
740
872
|
}, { global: true });
|
|
@@ -776,7 +908,8 @@ function spawnArgv(ctx, config, policy) {
|
|
|
776
908
|
mode: policy.mode
|
|
777
909
|
}).argv;
|
|
778
910
|
}
|
|
779
|
-
async function startupSession(session, dialect, signal) {
|
|
911
|
+
async function startupSession(session, dialect, timeoutMs, signal) {
|
|
912
|
+
let startupOperation;
|
|
780
913
|
const start = async () => {
|
|
781
914
|
if (dialect === "bash") {
|
|
782
915
|
await session.initialize(signal);
|
|
@@ -785,36 +918,44 @@ async function startupSession(session, dialect, signal) {
|
|
|
785
918
|
let viewport = "";
|
|
786
919
|
for (;;) {
|
|
787
920
|
const first = viewport.length === 0;
|
|
788
|
-
|
|
921
|
+
startupOperation = session.startSend({
|
|
789
922
|
text: first ? ENCODING_PREAMBLE + PWSH_PROMPT_SETUP : "",
|
|
790
923
|
submit: first,
|
|
791
924
|
...signal !== void 0 ? { signal } : {}
|
|
792
|
-
})
|
|
925
|
+
});
|
|
926
|
+
const result = await startupOperation.done;
|
|
793
927
|
if (result.waitReason === "session_exit") throw new Error("PTY shell exited during startup");
|
|
794
928
|
if (result.waitReason === "timeout") throw new Error("PTY shell did not reach readiness before startup timeout");
|
|
795
929
|
viewport = result.viewport;
|
|
796
|
-
|
|
797
|
-
offset: 0,
|
|
798
|
-
count: 20
|
|
799
|
-
}).text;
|
|
800
|
-
if (viewport.includes("dsh> ") || scrollback.includes("dsh> ")) break;
|
|
930
|
+
if (result.waitReason === "stdin_read") break;
|
|
801
931
|
}
|
|
802
932
|
session.motd = viewport;
|
|
803
933
|
};
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
934
|
+
const races = [];
|
|
935
|
+
let onAbort;
|
|
936
|
+
if (signal !== void 0) {
|
|
937
|
+
const aborted = Promise.withResolvers();
|
|
938
|
+
onAbort = () => {
|
|
939
|
+
aborted.reject(signal.reason);
|
|
940
|
+
};
|
|
941
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
942
|
+
races.push(aborted.promise);
|
|
943
|
+
}
|
|
944
|
+
let deadlineTimer;
|
|
945
|
+
if (dialect === "pwsh") {
|
|
946
|
+
const deadline = Promise.withResolvers();
|
|
947
|
+
deadlineTimer = setTimeout(() => {
|
|
948
|
+
startupOperation?.cancel();
|
|
949
|
+
deadline.reject(/* @__PURE__ */ new Error("PTY shell did not reach readiness before startup timeout"));
|
|
950
|
+
}, timeoutMs);
|
|
951
|
+
races.push(deadline.promise);
|
|
807
952
|
}
|
|
808
|
-
const aborted = Promise.withResolvers();
|
|
809
|
-
const onAbort = () => {
|
|
810
|
-
aborted.reject(signal.reason);
|
|
811
|
-
};
|
|
812
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
813
953
|
try {
|
|
814
|
-
signal
|
|
815
|
-
await Promise.race([start(),
|
|
954
|
+
signal?.throwIfAborted();
|
|
955
|
+
await Promise.race([start(), ...races]);
|
|
816
956
|
} finally {
|
|
817
|
-
|
|
957
|
+
if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
|
|
958
|
+
if (signal !== void 0 && onAbort !== void 0) signal.removeEventListener("abort", onAbort);
|
|
818
959
|
}
|
|
819
960
|
}
|
|
820
961
|
/** Local shell backend registered under the configured type. */
|
|
@@ -848,7 +989,7 @@ var BashTerminalBackend = class {
|
|
|
848
989
|
});
|
|
849
990
|
const session = this.createSession(terminal, this.config);
|
|
850
991
|
try {
|
|
851
|
-
await startupSession(session, this.config.shellDialect, spec.signal);
|
|
992
|
+
await startupSession(session, this.config.shellDialect, this.config.timeoutMs, spec.signal);
|
|
852
993
|
return session;
|
|
853
994
|
} catch (error) {
|
|
854
995
|
try {
|
package/lib/types/config.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface Config {
|
|
|
33
33
|
* regain the foreground before `inferred_idle` settles; at least one `pollIntervalMs`.
|
|
34
34
|
*/
|
|
35
35
|
handoffGraceMs?: number;
|
|
36
|
-
/** Absolute send
|
|
36
|
+
/** Absolute bound for one send and the complete pwsh startup sequence. */
|
|
37
37
|
timeoutMs?: number;
|
|
38
38
|
/** Grace before teardown escalates to `SIGKILL`. */
|
|
39
39
|
disposeGraceMs?: number;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { Config } from './config.ts';
|
|
|
12
12
|
export type { Config as TerminalLocalConfig } from './config.ts';
|
|
13
13
|
/** Cordis plugin name. */
|
|
14
14
|
export declare const name = "terminal-bash";
|
|
15
|
-
/** Required services:
|
|
15
|
+
/** Required services: terminal registry, shared confinement policy, projection registry, and process substrate. */
|
|
16
16
|
export declare const inject: string[];
|
|
17
17
|
/**
|
|
18
18
|
* The pwsh prompt function that emits the shared OSC `133;D;` + BEL marker
|
package/lib/types/session.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Persistent PTY session
|
|
1
|
+
/** Persistent PTY session with bounded output, readiness, and terminal-protocol replies. */
|
|
2
2
|
import type { SubprocessTerminalHandle } from '@deepseek-ai/dsh-subprocess';
|
|
3
3
|
import type { TerminalBackendSession, TerminalReadRequest, TerminalReadResult, TerminalSendOperation, TerminalSendRequest, TerminalSessionStatus, TerminalSignal, TerminalSignalResult } from '@deepseek-ai/dsh-terminal';
|
|
4
4
|
import type { ResolvedConfig } from './config.ts';
|
|
@@ -9,6 +9,9 @@ export declare class LocalPtySession implements TerminalBackendSession {
|
|
|
9
9
|
motd: string;
|
|
10
10
|
readonly pid: number;
|
|
11
11
|
private readonly decoder;
|
|
12
|
+
/** Protocol state only; the sanitizer and bounded buffers own returned text. */
|
|
13
|
+
private readonly emulator;
|
|
14
|
+
private readonly emulatorData;
|
|
12
15
|
private readonly sanitizer;
|
|
13
16
|
private readonly scrollback;
|
|
14
17
|
private readonly outputEnded;
|
|
@@ -31,6 +34,13 @@ export declare class LocalPtySession implements TerminalBackendSession {
|
|
|
31
34
|
private closing;
|
|
32
35
|
private closePromise;
|
|
33
36
|
private transportFailure;
|
|
37
|
+
private emulatorWrites;
|
|
38
|
+
private emulatorWriteDone;
|
|
39
|
+
private emulatorBuffer;
|
|
40
|
+
private emulatorWriting;
|
|
41
|
+
private responseWrites;
|
|
42
|
+
private pendingResponseWrites;
|
|
43
|
+
private emulatorClosed;
|
|
34
44
|
constructor(terminal: SubprocessTerminalHandle, config: ResolvedConfig);
|
|
35
45
|
/**
|
|
36
46
|
* Capture startup output through the same readiness contract as later sends.
|
|
@@ -54,6 +64,17 @@ export declare class LocalPtySession implements TerminalBackendSession {
|
|
|
54
64
|
private appendOutput;
|
|
55
65
|
private schedulePoll;
|
|
56
66
|
private pollReadiness;
|
|
67
|
+
/** Wait until generated replies reach the provider before another send can publish. */
|
|
68
|
+
private drainTerminalProtocol;
|
|
69
|
+
/** Sample foreground state only after protocol replies are quiet for the entire inspection. */
|
|
70
|
+
private inspectForegroundAfterProtocol;
|
|
71
|
+
private protocolStateChanged;
|
|
72
|
+
private protocolWorkPending;
|
|
73
|
+
private queueEmulatorData;
|
|
74
|
+
private pumpEmulator;
|
|
75
|
+
private finishResponseWrite;
|
|
76
|
+
private releaseSettledActive;
|
|
77
|
+
private closeEmulator;
|
|
57
78
|
private settleActive;
|
|
58
79
|
private stopPolling;
|
|
59
80
|
private stopReadinessPolling;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-terminal-bash",
|
|
3
3
|
"description": "Persistent shell PTY backend over the DeepSeek Harness subprocess terminal primitive",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -32,28 +32,30 @@
|
|
|
32
32
|
],
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
36
|
-
"@deepseek-ai/dsh-
|
|
37
|
-
"@deepseek-ai/dsh-
|
|
38
|
-
"@deepseek-ai/dsh-sandbox": "^0.1.
|
|
39
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
40
|
-
"@deepseek-ai/dsh-
|
|
41
|
-
"@deepseek-ai/
|
|
42
|
-
"@deepseek-ai/
|
|
35
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
|
|
36
|
+
"@deepseek-ai/dsh-terminal": "^0.1.2-alpha.2",
|
|
37
|
+
"@deepseek-ai/dsh-sandbox": "^0.1.2-alpha.2",
|
|
38
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.1.2-alpha.2",
|
|
39
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
40
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
41
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2",
|
|
42
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
43
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
|
-
"@
|
|
46
|
-
"@deepseek-ai/
|
|
46
|
+
"@xterm/headless": "^6.0.0",
|
|
47
|
+
"@deepseek-ai/dsh-pwsh-local": "^0.1.2-alpha.2",
|
|
48
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
50
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
51
|
-
"@deepseek-ai/dsh-terminal": "^0.1.
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/dsh-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-subprocess": "^0.1.
|
|
56
|
-
"@deepseek-ai/
|
|
57
|
-
"@deepseek-ai/
|
|
51
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
|
|
53
|
+
"@deepseek-ai/dsh-terminal": "^0.1.2-alpha.2",
|
|
54
|
+
"@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
|
|
55
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.2-alpha.2",
|
|
56
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.1.2-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-subprocess-local": "^0.1.2-alpha.2",
|
|
58
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
59
|
+
"@deepseek-ai/dsh-sandbox": "^0.1.2-alpha.2"
|
|
58
60
|
}
|
|
59
61
|
}
|