Skip to content

Commit 0004236

Browse files
committed
Probe shell supervisor show status
1 parent c4cb2e4 commit 0004236

4 files changed

Lines changed: 165 additions & 38 deletions

File tree

docs/runtime.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1353,7 +1353,8 @@ rendered from the durable `.dscode/shell-jobs` table, so a supervisor client can
13531353
inspect persisted shell jobs without separately calling the model tool.
13541354
`exec_shell_supervisor_status cwd=<path>` inspects that manifest/socket state,
13551355
reports absent/stale/ready status and supported method names, probes socket
1356-
protocol health with a bounded `health` request, and never prints
1356+
protocol health with a bounded `health` request, probes `show` for protocol
1357+
job-inventory parity when the daemon is healthy, and never prints
13571358
`control_token_hash`. Unsupported PTY methods return structured `unsupported`
13581359
responses until native supervisor-owned PTY sessions land.
13591360
Local file-backed TUI sessions surface the same read-only protocol check through

docs/superpowers/plans/2026-05-10-deepseek-tui-parity.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,8 @@ Landed first slice:
251251
PTY methods until supervisor-owned PTYs land; `deepseek agents service`
252252
and packaged systemd/launchd templates include that shell-supervisor service;
253253
`exec_shell_supervisor_status` now probes socket health before reporting a
254-
daemon as ready;
254+
daemon as ready and also reads healthy daemon `show` inventory into the
255+
status summary;
255256
foreground `exec_shell timeout_ms` / `detach_after_ms` now uses the durable
256257
background job table and returns `meta.backgrounded=true` plus a `task_id`
257258
when the command is still running, approximating DeepSeek-TUI's
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# DeepSeek-TUI parity: shell supervisor status show probe
2+
3+
Status: implemented
4+
Date: 2026-05-14
5+
6+
## Gap
7+
8+
The shell supervisor daemon can answer `show` with durable job inventory, but
9+
`exec_shell_supervisor_status` only probed `health`. Operator-facing status
10+
therefore proved the socket was responsive without proving the job-center
11+
protocol path worked.
12+
13+
## Implementation
14+
15+
- `exec_shell_supervisor_status` keeps the bounded `health` request as the
16+
readiness signal.
17+
- When `health` returns `ok`, it opens a second bounded protocol request for
18+
`show`.
19+
- The status summary now includes `protocol_show` and a
20+
`protocol_job_inventory` block populated from `job_inventory` when available.
21+
- If `show` fails or returns an unexpected response, the failure is reported in
22+
`protocol_show` without changing the native PTY boundary.
23+
24+
## Verification
25+
26+
- `cargo test exec_shell_supervisor_status_probes_protocol_health_and_show --lib`
27+
- `cargo test shell_supervisor --lib`
28+
- `cargo check`
29+
- `cargo fmt --check`
30+
- `cargo test --lib -- --test-threads=1`
31+
- `git diff --check`
32+
33+
## Remaining Gap
34+
35+
This proves the read-only daemon job-center path. It does not implement native
36+
supervisor-owned PTY process start, attach, stdin, resize, replay, wait, or
37+
cancel.

src/tools/exec_shell.rs

Lines changed: 124 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1836,6 +1836,8 @@ fn render_shell_supervisor_status(cwd: &str) -> AppResult<String> {
18361836
.unwrap_or(default_socket);
18371837
let socket_kind = shell_supervisor_socket_kind(&socket_path);
18381838
let protocol_health = shell_supervisor_protocol_health(&socket_path, socket_kind == "socket");
1839+
let (protocol_show, protocol_job_inventory) =
1840+
shell_supervisor_protocol_show(&socket_path, socket_kind == "socket", &protocol_health);
18391841
let supervisor_alive = manifest
18401842
.as_ref()
18411843
.and_then(|manifest| manifest.supervisor_pid)
@@ -1847,7 +1849,7 @@ fn render_shell_supervisor_status(cwd: &str) -> AppResult<String> {
18471849
&protocol_health,
18481850
);
18491851
Ok(format!(
1850-
"kind: deepseek.exec_shell.supervisor_status.v1\nstatus: {status}\nplatform: {}\ncwd: {}\nstate_dir: {}\nmanifest: {}\nmanifest_exists: {}\nmanifest_kind: {}\nsocket: {}\nsocket_kind: {socket_kind}\nprotocol_health: {protocol_health}\nsupervisor_pid: {}\nsupervisor_alive: {}\nsupervisor_epoch: {}\nprotocol: {}\nmethods: {}\nunsupported_methods: {}\nactive_jobs: {}\nstarted_at: {}\nupdated_at: {}\nnote: this is the shell supervisor protocol/status skeleton; native PTY ownership, live attach, and TIOCSWINSZ resize are not implemented until a real supervisor process writes this state.\n",
1852+
"kind: deepseek.exec_shell.supervisor_status.v1\nstatus: {status}\nplatform: {}\ncwd: {}\nstate_dir: {}\nmanifest: {}\nmanifest_exists: {}\nmanifest_kind: {}\nsocket: {}\nsocket_kind: {socket_kind}\nprotocol_health: {protocol_health}\nprotocol_show: {protocol_show}\nsupervisor_pid: {}\nsupervisor_alive: {}\nsupervisor_epoch: {}\nprotocol: {}\nmethods: {}\nunsupported_methods: {}\nactive_jobs: {}\nstarted_at: {}\nupdated_at: {}\nprotocol_job_inventory:\n{}\nnote: this is the shell supervisor protocol/status skeleton; native PTY ownership, live attach, and TIOCSWINSZ resize are not implemented until a real supervisor process writes this state.\n",
18511853
shell_supervisor_platform_label(),
18521854
cwd,
18531855
state_dir.display(),
@@ -1889,7 +1891,8 @@ fn render_shell_supervisor_status(cwd: &str) -> AppResult<String> {
18891891
manifest
18901892
.as_ref()
18911893
.and_then(|manifest| manifest.updated_at.as_deref())
1892-
)
1894+
),
1895+
protocol_job_inventory.unwrap_or_else(|| "not_checked".to_string())
18931896
)
18941897
.trim_end()
18951898
.to_string())
@@ -1936,49 +1939,112 @@ fn shell_supervisor_protocol_health(socket_path: &Path, socket_ready: bool) -> S
19361939

19371940
#[cfg(unix)]
19381941
fn shell_supervisor_protocol_health_unix(socket_path: &Path) -> AppResult<String> {
1942+
let root = shell_supervisor_protocol_request_unix(socket_path, "health")?;
1943+
if root.get("method").and_then(json_as_string) == Some("health")
1944+
&& root.get("status").and_then(json_as_string) == Some("ok")
1945+
{
1946+
Ok("ok".to_string())
1947+
} else {
1948+
Ok(format!(
1949+
"unexpected_response: {}",
1950+
shell_compact_error_label(&json_value_to_string(&JsonValue::Object(root)))
1951+
))
1952+
}
1953+
}
1954+
1955+
fn shell_supervisor_protocol_show(
1956+
socket_path: &Path,
1957+
socket_ready: bool,
1958+
protocol_health: &str,
1959+
) -> (String, Option<String>) {
1960+
if !socket_ready {
1961+
return ("not_checked".to_string(), None);
1962+
}
1963+
if protocol_health != "ok" {
1964+
return ("not_checked".to_string(), None);
1965+
}
1966+
#[cfg(unix)]
1967+
{
1968+
match shell_supervisor_protocol_show_unix(socket_path) {
1969+
Ok((label, inventory)) => (label, inventory),
1970+
Err(error) => (
1971+
format!("error: {}", shell_compact_error_label(&error.to_string())),
1972+
None,
1973+
),
1974+
}
1975+
}
1976+
#[cfg(not(unix))]
1977+
{
1978+
let _ = socket_path;
1979+
("unsupported".to_string(), None)
1980+
}
1981+
}
1982+
1983+
#[cfg(unix)]
1984+
fn shell_supervisor_protocol_show_unix(socket_path: &Path) -> AppResult<(String, Option<String>)> {
1985+
let root = shell_supervisor_protocol_request_unix(socket_path, "show")?;
1986+
if root.get("method").and_then(json_as_string) == Some("show")
1987+
&& root.get("status").and_then(json_as_string) == Some("ok")
1988+
{
1989+
let inventory = root
1990+
.get("job_inventory")
1991+
.and_then(json_as_string)
1992+
.map(str::to_string);
1993+
if let Some(error) = root.get("job_inventory_error").and_then(json_as_string) {
1994+
return Ok((
1995+
format!("inventory_error: {}", shell_compact_error_label(error)),
1996+
inventory,
1997+
));
1998+
}
1999+
return Ok(("ok".to_string(), inventory));
2000+
}
2001+
Ok((
2002+
format!(
2003+
"unexpected_response: {}",
2004+
shell_compact_error_label(&json_value_to_string(&JsonValue::Object(root)))
2005+
),
2006+
None,
2007+
))
2008+
}
2009+
2010+
#[cfg(unix)]
2011+
fn shell_supervisor_protocol_request_unix(
2012+
socket_path: &Path,
2013+
method: &str,
2014+
) -> AppResult<BTreeMap<String, JsonValue>> {
19392015
use std::io::{BufRead, BufReader, ErrorKind};
19402016
use std::os::unix::net::UnixStream;
19412017

19422018
let mut stream = UnixStream::connect(socket_path)
1943-
.map_err(|error| app_error(format!("health connect failed: {error}")))?;
2019+
.map_err(|error| app_error(format!("{method} connect failed: {error}")))?;
19442020
stream
19452021
.set_read_timeout(Some(Duration::from_millis(250)))
1946-
.map_err(|error| app_error(format!("health read timeout setup failed: {error}")))?;
2022+
.map_err(|error| app_error(format!("{method} read timeout setup failed: {error}")))?;
19472023
stream
19482024
.set_write_timeout(Some(Duration::from_millis(250)))
1949-
.map_err(|error| app_error(format!("health write timeout setup failed: {error}")))?;
2025+
.map_err(|error| app_error(format!("{method} write timeout setup failed: {error}")))?;
19502026
stream
1951-
.write_all(b"{\"method\":\"health\"}\n")
1952-
.map_err(|error| app_error(format!("health request write failed: {error}")))?;
2027+
.write_all(format!("{{\"method\":\"{method}\"}}\n").as_bytes())
2028+
.map_err(|error| app_error(format!("{method} request write failed: {error}")))?;
19532029

19542030
let mut response = String::new();
19552031
let mut reader = BufReader::new(stream);
19562032
match reader.read_line(&mut response) {
1957-
Ok(0) => return Err(app_error("health response was empty")),
2033+
Ok(0) => return Err(app_error(format!("{method} response was empty"))),
19582034
Ok(_) => {}
19592035
Err(error) if matches!(error.kind(), ErrorKind::TimedOut | ErrorKind::WouldBlock) => {
1960-
return Err(app_error("health response timed out"));
2036+
return Err(app_error(format!("{method} response timed out")));
19612037
}
1962-
Err(error) => return Err(app_error(format!("health response read failed: {error}"))),
2038+
Err(error) => return Err(app_error(format!("{method} response read failed: {error}"))),
19632039
}
19642040

1965-
let root = parse_root_object(response.trim()).map_err(|error| {
2041+
parse_root_object(response.trim()).map_err(|error| {
19662042
app_error(format!(
1967-
"health response was not valid JSON: {}; response={}",
2043+
"{method} response was not valid JSON: {}; response={}",
19682044
error,
19692045
shell_compact_error_label(response.trim())
19702046
))
1971-
})?;
1972-
if root.get("method").and_then(json_as_string) == Some("health")
1973-
&& root.get("status").and_then(json_as_string) == Some("ok")
1974-
{
1975-
Ok("ok".to_string())
1976-
} else {
1977-
Ok(format!(
1978-
"unexpected_response: {}",
1979-
shell_compact_error_label(response.trim())
1980-
))
1981-
}
2047+
})
19822048
}
19832049

19842050
fn shell_compact_error_label(value: &str) -> String {
@@ -3407,7 +3473,7 @@ mod tests {
34073473

34083474
#[cfg(unix)]
34093475
#[test]
3410-
fn exec_shell_supervisor_status_probes_protocol_health() {
3476+
fn exec_shell_supervisor_status_probes_protocol_health_and_show() {
34113477
use std::io::{BufRead, BufReader, Write};
34123478
use std::os::unix::net::UnixListener;
34133479

@@ -3433,19 +3499,26 @@ mod tests {
34333499
.unwrap();
34343500

34353501
let handle = std::thread::spawn(move || {
3436-
let (mut stream, _) = listener.accept().unwrap();
3437-
let mut request = String::new();
3438-
{
3439-
let mut reader = BufReader::new(&mut stream);
3440-
reader.read_line(&mut request).unwrap();
3502+
for (expected_method, response) in [
3503+
(
3504+
"health",
3505+
br#"{"kind":"deepseek.exec_shell.supervisor.response.v1","method":"health","status":"ok"}"#.as_slice(),
3506+
),
3507+
(
3508+
"show",
3509+
br#"{"kind":"deepseek.exec_shell.supervisor.response.v1","method":"show","status":"ok","job_inventory":"No background shell jobs."}"#.as_slice(),
3510+
),
3511+
] {
3512+
let (mut stream, _) = listener.accept().unwrap();
3513+
let mut request = String::new();
3514+
{
3515+
let mut reader = BufReader::new(&mut stream);
3516+
reader.read_line(&mut request).unwrap();
3517+
}
3518+
assert!(request.contains(&format!(r#""method":"{expected_method}""#)));
3519+
stream.write_all(response).unwrap();
3520+
stream.write_all(b"\n").unwrap();
34413521
}
3442-
assert!(request.contains(r#""method":"health""#));
3443-
stream
3444-
.write_all(
3445-
br#"{"kind":"deepseek.exec_shell.supervisor.response.v1","method":"health","status":"ok"}"#,
3446-
)
3447-
.unwrap();
3448-
stream.write_all(b"\n").unwrap();
34493522
});
34503523

34513524
let status = ExecShellSupervisorStatusTool
@@ -3463,6 +3536,21 @@ mod tests {
34633536
"{}",
34643537
status.summary
34653538
);
3539+
assert!(
3540+
status.summary.contains("protocol_show: ok"),
3541+
"{}",
3542+
status.summary
3543+
);
3544+
assert!(
3545+
status.summary.contains("protocol_job_inventory:"),
3546+
"{}",
3547+
status.summary
3548+
);
3549+
assert!(
3550+
status.summary.contains("No background shell jobs."),
3551+
"{}",
3552+
status.summary
3553+
);
34663554

34673555
let _ = fs::remove_dir_all(root);
34683556
}

0 commit comments

Comments
 (0)