From 3f24759966a08ac79bac88869b526e6d8ef18b78 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Thu, 30 Apr 2026 21:34:00 -0500 Subject: [PATCH] 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. --- CHANGELOG.md | 26 +++++++++++- Cargo.lock | 28 ++++++------- Cargo.toml | 2 +- README.md | 63 ++++++++++++++++++++--------- crates/agent/Cargo.toml | 2 +- crates/app-server/Cargo.toml | 18 ++++----- crates/cli/Cargo.toml | 14 +++---- crates/config/Cargo.toml | 2 +- crates/core/Cargo.toml | 16 ++++---- crates/execpolicy/Cargo.toml | 2 +- crates/hooks/Cargo.toml | 2 +- crates/mcp/Cargo.toml | 2 +- crates/tools/Cargo.toml | 2 +- crates/tui/Cargo.toml | 4 +- crates/tui/src/repl/runtime.rs | 3 ++ crates/tui/src/tools/shell.rs | 2 + crates/tui/src/tools/shell/tests.rs | 35 ++++++++++++++++ npm/deepseek-tui/package.json | 4 +- 18 files changed, 159 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13e5f263..c41aba48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 5d8bdf56..e56ae84f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 75eba577..c4161420 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index cba6fab7..acb6ca0a 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index e3d94214..04fafe31 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -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 diff --git a/crates/app-server/Cargo.toml b/crates/app-server/Cargo.toml index f7f189b5..bf7fb97d 100644 --- a/crates/app-server/Cargo.toml +++ b/crates/app-server/Cargo.toml @@ -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 diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index bc552784..16834c0b 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -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 diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 4e5aa9d1..c53aed8a 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -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 diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 8719e4a1..30c8c0bd 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -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 diff --git a/crates/execpolicy/Cargo.toml b/crates/execpolicy/Cargo.toml index 510ff00f..7687d82a 100644 --- a/crates/execpolicy/Cargo.toml +++ b/crates/execpolicy/Cargo.toml @@ -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 diff --git a/crates/hooks/Cargo.toml b/crates/hooks/Cargo.toml index e21c16a9..c638d08b 100644 --- a/crates/hooks/Cargo.toml +++ b/crates/hooks/Cargo.toml @@ -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 diff --git a/crates/mcp/Cargo.toml b/crates/mcp/Cargo.toml index b27640d0..d0cb326c 100644 --- a/crates/mcp/Cargo.toml +++ b/crates/mcp/Cargo.toml @@ -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 diff --git a/crates/tools/Cargo.toml b/crates/tools/Cargo.toml index 1c752474..ae1f9b13 100644 --- a/crates/tools/Cargo.toml +++ b/crates/tools/Cargo.toml @@ -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 diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 4637e14b..634a68b6 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -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" diff --git a/crates/tui/src/repl/runtime.rs b/crates/tui/src/repl/runtime.rs index 84324e65..626a0e7f 100644 --- a/crates/tui/src/repl/runtime.rs +++ b/crates/tui/src/repl/runtime.rs @@ -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 diff --git a/crates/tui/src/tools/shell.rs b/crates/tui/src/tools/shell.rs index 88472a97..dfba4e30 100644 --- a/crates/tui/src/tools/shell.rs +++ b/crates/tui/src/tools/shell.rs @@ -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<()> { diff --git a/crates/tui/src/tools/shell/tests.rs b/crates/tui/src/tools/shell/tests.rs index f6ddf983..740e5cd7 100644 --- a/crates/tui/src/tools/shell/tests.rs +++ b/crates/tui/src/tools/shell/tests.rs @@ -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"); diff --git a/npm/deepseek-tui/package.json b/npm/deepseek-tui/package.json index 74b8abbe..820afc8f 100644 --- a/npm/deepseek-tui/package.json +++ b/npm/deepseek-tui/package.json @@ -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",