release: stabilize shell handles for v0.8.0

Bumps the workspace/npm wrapper to 0.8.0 and fixes completed background shell jobs retaining live process handles, which could cause Too many open files, checkpoint save failures, shell spawn failures, and lag around send/close/Esc. Also includes Windows REPL bootstrap timeout hardening and Cargo/TUNA mirror install docs.
This commit is contained in:
Hunter Bown
2026-04-30 21:34:00 -05:00
committed by GitHub
parent 12d79ec774
commit 3f24759966
18 changed files with 159 additions and 68 deletions
+25 -1
View File
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.8.0] - 2026-05-01
### Fixed
- **Shell FD leak / post-send lag** — completed background shell jobs now release
their process, stdin, stdout, and stderr handles as soon as completion is
observed, while keeping the job record inspectable. This prevents long-running
TUI sessions from hitting `Too many open files (os error 24)`, which could
make checkpoint saves fail and cause shell spawning, message send, close, and
Esc/cancel paths to lag or fail.
- **Windows REPL runtime CI startup** — Windows gets a longer Python bootstrap
readiness timeout for the REPL runtime tests, matching GitHub runner startup
contention without weakening bootstrap failures on other platforms.
### Added
- **China / mirror-friendly Cargo install docs** — README now documents
installing through the TUNA Cargo mirror and direct release assets for users
with slow GitHub/npm access.
### Tests
- Added a regression test proving completed background shell jobs drop their
live process handles after `exec_shell_wait`.
- Re-ran the focused shell cancellation and Python REPL runtime slices.
## [0.7.9] - 2026-05-02
### Fixed
@@ -726,7 +749,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Hooks system and config profiles
- Example skills and launch assets
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.7.9...HEAD
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.0...HEAD
[0.8.0]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.7.9...v0.8.0
[0.7.9]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.7.8...v0.7.9
[0.7.8]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.7.7...v0.7.8
[0.7.7]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.7.6...v0.7.7
Generated
+14 -14
View File
@@ -1011,7 +1011,7 @@ dependencies = [
[[package]]
name = "deepseek-agent"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"deepseek-config",
"serde",
@@ -1019,7 +1019,7 @@ dependencies = [
[[package]]
name = "deepseek-app-server"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"anyhow",
"axum",
@@ -1042,7 +1042,7 @@ dependencies = [
[[package]]
name = "deepseek-config"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"anyhow",
"deepseek-secrets",
@@ -1055,7 +1055,7 @@ dependencies = [
[[package]]
name = "deepseek-core"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"anyhow",
"chrono",
@@ -1074,7 +1074,7 @@ dependencies = [
[[package]]
name = "deepseek-execpolicy"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"anyhow",
"deepseek-protocol",
@@ -1083,7 +1083,7 @@ dependencies = [
[[package]]
name = "deepseek-hooks"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1097,7 +1097,7 @@ dependencies = [
[[package]]
name = "deepseek-mcp"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"anyhow",
"deepseek-protocol",
@@ -1107,7 +1107,7 @@ dependencies = [
[[package]]
name = "deepseek-protocol"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"serde",
"serde_json",
@@ -1115,7 +1115,7 @@ dependencies = [
[[package]]
name = "deepseek-secrets"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"dirs",
"keyring",
@@ -1128,7 +1128,7 @@ dependencies = [
[[package]]
name = "deepseek-state"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"anyhow",
"chrono",
@@ -1140,7 +1140,7 @@ dependencies = [
[[package]]
name = "deepseek-tools"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1153,7 +1153,7 @@ dependencies = [
[[package]]
name = "deepseek-tui"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"anyhow",
"arboard",
@@ -1213,7 +1213,7 @@ dependencies = [
[[package]]
name = "deepseek-tui-cli"
version = "0.7.9"
version = "0.8.0"
dependencies = [
"anyhow",
"chrono",
@@ -1236,7 +1236,7 @@ dependencies = [
[[package]]
name = "deepseek-tui-core"
version = "0.7.9"
version = "0.8.0"
[[package]]
name = "deranged"
+1 -1
View File
@@ -19,7 +19,7 @@ default-members = ["crates/cli", "crates/app-server", "crates/tui"]
resolver = "2"
[workspace.package]
version = "0.7.9"
version = "0.8.0"
edition = "2024"
license = "MIT"
repository = "https://github.com/Hmbown/DeepSeek-TUI"
+45 -18
View File
@@ -43,6 +43,34 @@ npm install -g deepseek-tui
deepseek
```
### China / mirror-friendly install
If GitHub or npm downloads are slow from mainland China, install the Rust
crates through a Cargo registry mirror:
```toml
# ~/.cargo/config.toml
[source.crates-io]
replace-with = "tuna"
[source.tuna]
registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"
```
Then install the two shipped binaries:
```bash
cargo install deepseek-tui-cli --locked
cargo install deepseek-tui --locked
deepseek --version
```
You can also download prebuilt binaries directly from the
[GitHub Releases](https://github.com/Hmbown/DeepSeek-TUI/releases) page when
GitHub release assets are reachable. TUNA, rsproxy, Tencent COS, or Aliyun OSS
mirrors can also be used with `DEEPSEEK_TUI_RELEASE_BASE_URL` when a mirrored
release-asset directory is available.
On first launch you'll be prompted for your [DeepSeek API key](https://platform.deepseek.com/api_keys). You can also set it ahead of time:
```bash
@@ -88,33 +116,32 @@ cargo install --path crates/cli --bin deepseek --locked
---
## What's new in v0.7.9
## What's new in v0.8.0
### ⚡ Post-turn responsiveness
### ⚡ Shell stability and post-send responsiveness
The checkpoint-restart cycle boundary now runs *before* `TurnComplete`
is emitted to the UI, so the terminal is immediately responsive after
every completed turn. The "↻ context refreshing…" status chip stays
visible during the cycle wait instead of blocking input.
Completed background shell jobs now release their live process and pipe
handles as soon as completion is observed, while keeping the job record
inspectable. This prevents long-running sessions from hitting `Too many
open files (os error 24)`, which could make checkpoint saves fail and
cause shell spawning, message send, close, and Esc/cancel paths to lag
or fail.
### ⌨️ Enter during streaming now queues
### 🪟 Windows REPL runtime CI hardening
When the model is actively streaming text and you press Enter, your
message is now parked on the queue for dispatch after the turn finishes
— no more corrupted mid-turn steering. A status line shows "Queued
follow-up: … (N queued)" so you know what's waiting.
Windows gets a longer Python bootstrap readiness timeout for the REPL
runtime tests, matching GitHub runner startup contention without
weakening bootstrap failures on other platforms.
### 🛡️ Esc during fanout is robust
### 🌏 Cargo mirror install docs
Canceling a fanout (swarm/sub-agent spawn) with Esc no longer leaves
the transcript in a broken state when the engine sends a delayed
`TurnComplete`. Both finalization paths are now idempotent — no
double `[interrupted]` prefixes, no stale tool cards.
The README now includes a TUNA Cargo mirror setup and direct release
asset guidance for users with slow GitHub/npm access.
### 🧪 Test hardening
New regression tests lock in the Esc-during-fanout idempotency
contract and the streaming vs. steer dispatch decision.
New regression coverage proves completed background shell jobs drop
their live process handles after `exec_shell_wait`.
Full changelog: [CHANGELOG.md](CHANGELOG.md).
+1 -1
View File
@@ -7,5 +7,5 @@ repository.workspace = true
description = "Model/provider registry and fallback strategy for DeepSeek workspace architecture"
[dependencies]
deepseek-config = { path = "../config", version = "0.7.9" }
deepseek-config = { path = "../config", version = "0.8.0" }
serde.workspace = true
+9 -9
View File
@@ -10,15 +10,15 @@ description = "Codex-style app-server transport for DeepSeek workspace architect
anyhow.workspace = true
axum.workspace = true
clap.workspace = true
deepseek-agent = { path = "../agent", version = "0.7.9" }
deepseek-config = { path = "../config", version = "0.7.9" }
deepseek-core = { path = "../core", version = "0.7.9" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.9" }
deepseek-hooks = { path = "../hooks", version = "0.7.9" }
deepseek-mcp = { path = "../mcp", version = "0.7.9" }
deepseek-protocol = { path = "../protocol", version = "0.7.9" }
deepseek-state = { path = "../state", version = "0.7.9" }
deepseek-tools = { path = "../tools", version = "0.7.9" }
deepseek-agent = { path = "../agent", version = "0.8.0" }
deepseek-config = { path = "../config", version = "0.8.0" }
deepseek-core = { path = "../core", version = "0.8.0" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.0" }
deepseek-hooks = { path = "../hooks", version = "0.8.0" }
deepseek-mcp = { path = "../mcp", version = "0.8.0" }
deepseek-protocol = { path = "../protocol", version = "0.8.0" }
deepseek-state = { path = "../state", version = "0.8.0" }
deepseek-tools = { path = "../tools", version = "0.8.0" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
+7 -7
View File
@@ -14,13 +14,13 @@ path = "src/main.rs"
anyhow.workspace = true
clap.workspace = true
clap_complete.workspace = true
deepseek-agent = { path = "../agent", version = "0.7.9" }
deepseek-app-server = { path = "../app-server", version = "0.7.9" }
deepseek-config = { path = "../config", version = "0.7.9" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.9" }
deepseek-mcp = { path = "../mcp", version = "0.7.9" }
deepseek-secrets = { path = "../secrets", version = "0.7.9" }
deepseek-state = { path = "../state", version = "0.7.9" }
deepseek-agent = { path = "../agent", version = "0.8.0" }
deepseek-app-server = { path = "../app-server", version = "0.8.0" }
deepseek-config = { path = "../config", version = "0.8.0" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.0" }
deepseek-mcp = { path = "../mcp", version = "0.8.0" }
deepseek-secrets = { path = "../secrets", version = "0.8.0" }
deepseek-state = { path = "../state", version = "0.8.0" }
chrono.workspace = true
dirs.workspace = true
serde.workspace = true
+1 -1
View File
@@ -8,7 +8,7 @@ description = "Config schema and precedence model for DeepSeek workspace archite
[dependencies]
anyhow.workspace = true
deepseek-secrets = { path = "../secrets", version = "0.7.9" }
deepseek-secrets = { path = "../secrets", version = "0.8.0" }
dirs.workspace = true
serde.workspace = true
serde_json.workspace = true
+8 -8
View File
@@ -9,14 +9,14 @@ description = "Core runtime boundaries for DeepSeek workspace architecture"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
deepseek-agent = { path = "../agent", version = "0.7.9" }
deepseek-config = { path = "../config", version = "0.7.9" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.7.9" }
deepseek-hooks = { path = "../hooks", version = "0.7.9" }
deepseek-mcp = { path = "../mcp", version = "0.7.9" }
deepseek-protocol = { path = "../protocol", version = "0.7.9" }
deepseek-state = { path = "../state", version = "0.7.9" }
deepseek-tools = { path = "../tools", version = "0.7.9" }
deepseek-agent = { path = "../agent", version = "0.8.0" }
deepseek-config = { path = "../config", version = "0.8.0" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.0" }
deepseek-hooks = { path = "../hooks", version = "0.8.0" }
deepseek-mcp = { path = "../mcp", version = "0.8.0" }
deepseek-protocol = { path = "../protocol", version = "0.8.0" }
deepseek-state = { path = "../state", version = "0.8.0" }
deepseek-tools = { path = "../tools", version = "0.8.0" }
serde_json.workspace = true
tokio.workspace = true
uuid.workspace = true
+1 -1
View File
@@ -8,5 +8,5 @@ description = "Execution policy and approval model parity for DeepSeek workspace
[dependencies]
anyhow.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.7.9" }
deepseek-protocol = { path = "../protocol", version = "0.8.0" }
serde.workspace = true
+1 -1
View File
@@ -10,7 +10,7 @@ description = "Hook dispatch and notifications parity for DeepSeek workspace arc
anyhow.workspace = true
async-trait.workspace = true
chrono.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.7.9" }
deepseek-protocol = { path = "../protocol", version = "0.8.0" }
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
+1 -1
View File
@@ -8,6 +8,6 @@ description = "MCP server lifecycle and tool proxy compatibility for DeepSeek wo
[dependencies]
anyhow.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.7.9" }
deepseek-protocol = { path = "../protocol", version = "0.8.0" }
serde.workspace = true
serde_json.workspace = true
+1 -1
View File
@@ -9,7 +9,7 @@ description = "Tool invocation lifecycle, schema validation, and scheduler paral
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.7.9" }
deepseek-protocol = { path = "../protocol", version = "0.8.0" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
+2 -2
View File
@@ -13,8 +13,8 @@ path = "src/main.rs"
[dependencies]
anyhow = "1.0.100"
arboard = "3.4"
deepseek-secrets = { path = "../secrets", version = "0.7.9" }
deepseek-tools = { path = "../tools", version = "0.7.9" }
deepseek-secrets = { path = "../secrets", version = "0.8.0" }
deepseek-tools = { path = "../tools", version = "0.8.0" }
async-stream = "0.3.6"
async-trait = "0.1"
bytes = "1.11.0"
+3
View File
@@ -122,7 +122,10 @@ pub trait RpcDispatcher: Send + Sync {
const DEFAULT_STDOUT_LIMIT: usize = 8_192;
const ROUND_TIMEOUT: Duration = Duration::from_secs(180);
#[cfg(not(windows))]
const SPAWN_READY_TIMEOUT: Duration = Duration::from_secs(10);
#[cfg(windows)]
const SPAWN_READY_TIMEOUT: Duration = Duration::from_secs(30);
// ---------------------------------------------------------------------------
// PythonRuntime
+2
View File
@@ -292,6 +292,8 @@ impl BackgroundShell {
if let Some(handle) = self.stderr_thread.take() {
let _ = handle.join();
}
self.stdin = None;
self.child = None;
}
fn write_stdin(&mut self, input: &str, close: bool) -> Result<()> {
+35
View File
@@ -447,6 +447,41 @@ async fn test_exec_shell_wait_cancel_leaves_background_process_running() {
assert_eq!(killed.status, ShellStatus::Killed);
}
#[tokio::test]
async fn test_completed_background_shell_releases_process_handles() {
let tmp = tempdir().expect("tempdir");
let ctx = ToolContext::new(tmp.path());
let shell_manager = ctx.shell_manager.clone();
let started = shell_manager
.lock()
.expect("shell manager lock")
.execute(&echo_command("done"), None, 600_000, true)
.expect("execute");
let task_id = started.task_id.expect("task id");
let result = ShellWaitTool::new("exec_shell_wait")
.execute(
json!({
"task_id": task_id.clone(),
"wait": true,
"timeout_ms": 5_000
}),
&ctx,
)
.await
.expect("wait");
assert!(result.success);
let mut manager = shell_manager.lock().expect("shell manager lock");
let shell = manager.processes.get_mut(&task_id).expect("tracked shell");
shell.poll();
assert_eq!(shell.status, ShellStatus::Completed);
assert!(shell.stdin.is_none());
assert!(shell.child.is_none());
assert!(shell.stdout_thread.is_none());
assert!(shell.stderr_thread.is_none());
}
#[tokio::test]
async fn test_exec_shell_cancel_tool_kills_background_process() {
let tmp = tempdir().expect("tempdir");
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "deepseek-tui",
"version": "0.7.9",
"deepseekBinaryVersion": "0.7.9",
"version": "0.8.0",
"deepseekBinaryVersion": "0.8.0",
"description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.",
"author": "Hmbown",
"license": "MIT",