Skip to content

Commit 9752dbf

Browse files
committed
fix: harden managed runtime lifecycle and terminal history
Preserve Claude startup, resume, transcript acknowledgements and network settings. Keep daemon takeover and forced process-tree cleanup independent of graceful stop, with suspended Windows Job registration and retryable child ownership. Retain repainted and long terminal history across backward pages. Fix sidebar ordering, group menus, mobile layout, composer suggestions and modal navigation. Add regression coverage and Windows CI checks for suspended launch and Job cleanup. Align formatting, lint diagnostics and cross-platform test fixtures.
1 parent 4eb6cf1 commit 9752dbf

139 files changed

Lines changed: 6889 additions & 1402 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,18 @@ jobs:
146146
manager_windows_tests::npm_style_batch_actor_survives_utf8_message_delivery
147147
-- --test-threads=1
148148
149+
- name: Verify Windows suspended process launch
150+
timeout-minutes: 2
151+
run: >-
152+
cargo test --package cccc-windows-process --lib --locked
153+
-- --test-threads=1
154+
155+
- name: Verify Windows owned Job cleanup
156+
timeout-minutes: 2
157+
run: >-
158+
cargo test --package cccc-pair-runtime --lib --locked
159+
process_tree::windows_tests:: -- --test-threads=1
160+
149161
- name: Verify Windows console UTF-8 encoding lifecycle
150162
timeout-minutes: 10
151163
run: >-

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ graphify-out
6969
AGENTS.md
7070
.graphifyignore
7171
.claude/skills
72+
/.claude/worktrees/
7273
.shared/
7374
.agents/
7475
.codex/

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,25 @@ The format follows [Keep a Changelog](https://keepachangelog.com/), and versions
77
## [Unreleased]
88

99
### Added
10+
1011
- **Claude Code now provides the same visible, precisely observed session to Group Actors and Voice Analyst.** CCCC observes authoritative turns and tool results from Claude Agent View and attaches the native writable Claude TUI to that exact session; ordinary stop/start resumes the validated provider conversation.
1112

1213
### Changed
1314
- **Claude Code uses one strict managed-session path instead of separate Hook and `claude -p` implementations.** Runtime Profiles retain supported model, effort, tools, plugins, settings, and private environment while CCCC owns session topology, MCP identity, YOLO policy, cancellation, and resume. Unsupported wrappers, prompt tails, transport flags, and legacy receipts fail explicitly instead of falling back to a divergent session.
1415
- **Actors no longer expose PTY versus Headless as a configuration choice.** CCCC derives the Runtime surface automatically: CLI Actors retain their native writable terminal, while Codex, Claude Code, Grok Build, and OpenCode pair it with structured lifecycle observation on the same session. Incoming Actor messages are handed to that native terminal without CCCC waiting for provider idleness or choosing queue-versus-steer semantics.
1516

1617
### Fixed
18+
- **Forced launcher exit terminates owned process trees independently of normal shutdown.** PTY, managed providers, DeepSeek and Web-owned daemon launches register OS resources at spawn; the second interrupt or normal-shutdown deadline terminates those resources without protocol/session locks or another cleanup timeout. Unix exit polling revokes ownership before reaping to avoid reused PID/PGID targets.
19+
- **Group action menus no longer activate mouse sorting.** Menu mouse-down events stay inside the trigger while row dragging remains available.
20+
- **Long terminal history no longer loses newer lines when older pages are loaded.** Scrolled-off lines have a separate bounded history buffer with explicit truncation reporting.
21+
- **Windows managed processes join their Job before executing.** Standard launches remain suspended through Job assignment, preserving detached launch flags and cleaning up failed starts.
22+
- **Failed managed-process stops retain retryable ownership.** Child handles remain owned until confirmed exit and reaping, including signal and wait failures.
23+
- **Sidebar group reordering updates immediately.** The rendered list now subscribes to order changes, preventing drag snap-back and subsequent moves based on stale row positions.
24+
- **Claude resume recovers stale transcript paths after worktree moves.** Missing published paths are resolved to a unique retained transcript for the same session only during initial recovery; history offsets and active-file identity checks remain intact, and missing-history errors identify the path and session.
25+
- **Daemon takeover validates the actual CLI subcommand before targeting a PID.** Global options no longer cause one-shot commands to be mistaken for daemon hosts.
26+
- **Terminal history preserves ANSI state across backward pages and remains keyboard accessible.** Rendering preserves inferred frames before repainting within a fixed cumulative range. Short pages require explicit loading, errors pause automatic loading, and the nested dialog traps focus and restores its opener.
27+
- **Managed Claude sessions retain host proxy and CA settings.** HTTP/HTTPS/ALL/NO proxy aliases and custom CA paths survive the cleared launcher environment and are included in the private settings used for session respawn. Explicit network overrides, including empty proxy values, take precedence.
28+
- **Managed Claude and Codex sessions recover before their first completed turn.** Claude can locate its exact validated transcript before Agent View publishes `linkScanPath`, preserving the resume read boundary. New Codex threads materialize metadata and pass a same-thread resume check before CCCC exposes the native terminal; startup does not inject a model prompt.
1729
- **Managed terminal startup no longer accepts truncated first messages.** Actor and Voice Analyst native input waits for the TUI's input mode before writing, without injecting startup work or waiting for the current model turn to finish.
1830
- **Claude sessions stop and resume reliably across empty starts and observer failures.** Verified empty sessions retain their conversation ID without requiring nonexistent history; observer failures use confirmed background-task shutdown, and normal closure releases event readers. Transcript replacement checks now use file handles supported by stable Rust on Windows.
1931
- **Native Windows Actors now start and receive messages reliably.** Runtime launch resolves executables in the configured PATHEXT order without restoring excluded extensions or selecting extensionless npm shell shims, routes `.cmd`/`.bat` through the Windows command processor, preserves backslashes in `actor --command`, and uses the last stable `portable-pty` ConPTY behavior. A Windows-only regression test delivers UTF-8 text through an npm-style batch shim and verifies that the Actor stays alive.

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ members = [
1111
"crates/cccc-notebooklm",
1212
"crates/cccc-web",
1313
"crates/cccc-windows-console",
14+
"crates/cccc-windows-process",
1415
"crates/cccc-cli",
1516
]
1617
exclude = [

crates/cccc-cli/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ cccc-web = { package = "cccc-pair-web", version = "=0.4.37", path = "../cccc-web
3030
clap.workspace = true
3131
fs2.workspace = true
3232
reqwest.workspace = true
33+
rustls.workspace = true
3334
semver.workspace = true
3435
serde_json.workspace = true
3536
shell-words.workspace = true
@@ -39,6 +40,9 @@ tempfile.workspace = true
3940
[target.'cfg(target_os = "linux")'.dependencies]
4041
openssl-sys = { version = "0.9", features = ["vendored"], optional = true }
4142

43+
[target.'cfg(unix)'.dependencies]
44+
nix.workspace = true
45+
4246
[target.'cfg(windows)'.dependencies]
4347
cccc-windows-console = { path = "../cccc-windows-console" }
4448

crates/cccc-cli/src/confirm.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//! Asking the operator before stopping something that is already running.
2+
//!
3+
//! Every "should I stop the process that is already here?" decision shares one
4+
//! safety rule: without a terminal to ask, the answer is no. A launcher started
5+
//! by an MCP bridge, a service manager, or CI has nobody to consult, and
6+
//! guessing there would terminate processes on behalf of an operator who never
7+
//! saw the question. Keeping the rule in one place is what keeps the two call
8+
//! sites -- the Web instance lock and the daemon lock -- from drifting apart.
9+
10+
use anyhow::Result;
11+
use std::io::{self, BufRead, IsTerminal, Write};
12+
13+
/// Ask a yes/no question, defaulting to no.
14+
///
15+
/// Returns `false` without prompting when stdin is not a terminal.
16+
pub(crate) fn ask(question: &str) -> Result<bool> {
17+
if !io::stdin().is_terminal() {
18+
return Ok(false);
19+
}
20+
ask_with(question, &mut io::stdin().lock(), &mut io::stderr().lock())
21+
}
22+
23+
pub(crate) fn ask_with(
24+
question: &str,
25+
input: &mut impl BufRead,
26+
output: &mut impl Write,
27+
) -> Result<bool> {
28+
write!(output, "{question} [y/N] ")?;
29+
output.flush()?;
30+
31+
let mut answer = String::new();
32+
input.read_line(&mut answer)?;
33+
Ok(matches!(
34+
answer.trim().to_ascii_lowercase().as_str(),
35+
"y" | "yes"
36+
))
37+
}
38+
39+
#[cfg(test)]
40+
mod tests {
41+
use super::ask_with;
42+
43+
#[test]
44+
fn accepts_only_explicit_yes() {
45+
let mut output = Vec::new();
46+
assert!(ask_with("Stop it?", &mut "yes\n".as_bytes(), &mut output).expect("yes"));
47+
assert!(ask_with("Stop it?", &mut "y\n".as_bytes(), &mut output).expect("y"));
48+
assert!(!ask_with("Stop it?", &mut "\n".as_bytes(), &mut output).expect("default no"));
49+
assert!(!ask_with("Stop it?", &mut "n\n".as_bytes(), &mut output).expect("no"));
50+
// Anything that is not an explicit yes must not be read as consent.
51+
assert!(!ask_with("Stop it?", &mut "sure\n".as_bytes(), &mut output).expect("not yes"));
52+
}
53+
54+
#[test]
55+
fn eof_is_not_consent() {
56+
let mut output = Vec::new();
57+
assert!(!ask_with("Stop it?", &mut "".as_bytes(), &mut output).expect("eof"));
58+
}
59+
}

0 commit comments

Comments
 (0)