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:
+25
-1
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user