@team-agent/installer 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +120 -9
- package/Cargo.toml +2 -2
- package/crates/team-agent/Cargo.toml +21 -0
- package/crates/team-agent/src/app_server_test_support.rs +258 -0
- package/crates/team-agent/src/cli/adapters.rs +32 -3
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +16 -0
- package/crates/team-agent/src/cli/diagnose.rs +14 -5
- package/crates/team-agent/src/cli/emit.rs +79 -1
- package/crates/team-agent/src/cli/mod.rs +190 -80
- package/crates/team-agent/src/cli/named_address.rs +111 -0
- package/crates/team-agent/src/cli/send.rs +98 -3
- package/crates/team-agent/src/cli/status_port.rs +44 -6
- package/crates/team-agent/src/cli/tests/named_address.rs +135 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
- package/crates/team-agent/src/cli/types.rs +17 -0
- package/crates/team-agent/src/codex_app_server.rs +549 -0
- package/crates/team-agent/src/conpty/backend.rs +822 -0
- package/crates/team-agent/src/conpty/mod.rs +32 -0
- package/crates/team-agent/src/coordinator/backoff.rs +132 -7
- package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
- package/crates/team-agent/src/coordinator/health.rs +97 -11
- package/crates/team-agent/src/coordinator/mod.rs +8 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
- package/crates/team-agent/src/diagnose/orphans.rs +29 -10
- package/crates/team-agent/src/leader/lease.rs +144 -7
- package/crates/team-agent/src/leader/owner_bind.rs +5 -2
- package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
- package/crates/team-agent/src/leader/start.rs +18 -5
- package/crates/team-agent/src/leader/tests/lease_api.rs +179 -0
- package/crates/team-agent/src/lib.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch.rs +207 -94
- package/crates/team-agent/src/lifecycle/lock.rs +40 -33
- package/crates/team-agent/src/lifecycle/restart/agent.rs +10 -18
- package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +63 -0
- package/crates/team-agent/src/mcp_server/wire.rs +6 -7
- package/crates/team-agent/src/messaging/delivery.rs +329 -9
- package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +78 -21
- package/crates/team-agent/src/messaging/tests/runtime.rs +237 -0
- package/crates/team-agent/src/packaging/tests.rs +41 -6
- package/crates/team-agent/src/packaging/types.rs +31 -3
- package/crates/team-agent/src/platform/argv.rs +324 -0
- package/crates/team-agent/src/platform/errors.rs +95 -0
- package/crates/team-agent/src/platform/file_lock.rs +418 -0
- package/crates/team-agent/src/platform/mod.rs +66 -0
- package/crates/team-agent/src/platform/process.rs +555 -0
- package/crates/team-agent/src/state/persist.rs +205 -25
- package/crates/team-agent/src/tmux_backend.rs +94 -13
- package/crates/team-agent/src/transport_factory.rs +751 -0
- package/package.json +4 -4
|
@@ -112,6 +112,10 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
112
112
|
"claim-leader" => cmd_claim_leader(&claim_leader_args(args, cwd)).map(emit_result),
|
|
113
113
|
// Real dispatch: `cmd_attach_leader` writes the `leader_receiver` binding.
|
|
114
114
|
"attach-leader" => cmd_attach_leader(&attach_leader_args(args, cwd)?).map(emit_result),
|
|
115
|
+
"attach-app-server-leader" => {
|
|
116
|
+
cmd_attach_app_server_leader(&attach_app_server_leader_args(args, cwd)?)
|
|
117
|
+
.map(emit_result)
|
|
118
|
+
}
|
|
115
119
|
"identity" => cmd_identity(&identity_args(args, cwd)).map(emit_result),
|
|
116
120
|
"approvals" => cmd_approvals(&approvals_args(args, cwd)).map(emit_result),
|
|
117
121
|
"inbox" => cmd_inbox(&inbox_args(args, cwd)?).map(emit_result),
|
|
@@ -167,6 +171,7 @@ const DISPATCH_COMMANDS: &[&str] = &[
|
|
|
167
171
|
"takeover",
|
|
168
172
|
"claim-leader",
|
|
169
173
|
"attach-leader",
|
|
174
|
+
"attach-app-server-leader",
|
|
170
175
|
"identity",
|
|
171
176
|
"approvals",
|
|
172
177
|
"inbox",
|
|
@@ -210,6 +215,23 @@ fn is_known_subcommand(command: &str) -> bool {
|
|
|
210
215
|
DISPATCH_COMMANDS.contains(&command) || SPEC_ONLY_HELP_COMMANDS.contains(&command)
|
|
211
216
|
}
|
|
212
217
|
|
|
218
|
+
/// Test-only public accessor for `command_help` — allows integration
|
|
219
|
+
/// tests to grep the help copy without depending on internal parser
|
|
220
|
+
/// machinery.
|
|
221
|
+
pub fn __test_command_help(command: Option<&str>) -> String {
|
|
222
|
+
command_help(command)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// Test-only public accessor for `quick_start_args` — allows
|
|
226
|
+
/// integration tests to exercise the parser without going through
|
|
227
|
+
/// stdio + the full `main` entrypoint.
|
|
228
|
+
pub fn __test_quick_start_args(
|
|
229
|
+
args: &[String],
|
|
230
|
+
cwd: &std::path::Path,
|
|
231
|
+
) -> Result<crate::cli::types::QuickStartArgs, crate::cli::CliError> {
|
|
232
|
+
quick_start_args(args, cwd)
|
|
233
|
+
}
|
|
234
|
+
|
|
213
235
|
fn command_help(command: Option<&str>) -> String {
|
|
214
236
|
match command {
|
|
215
237
|
None => {
|
|
@@ -222,7 +244,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
222
244
|
)
|
|
223
245
|
}
|
|
224
246
|
Some("init") => "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]".to_string(),
|
|
225
|
-
Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.".to_string(),
|
|
247
|
+
Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.\n\n--backend selects the worker transport (Phase 1d Batch 2): tmux (default on POSIX; unchanged behavior), conpty (Windows-native ConPTY worker transport; requires the shim binary and Windows host).".to_string(),
|
|
226
248
|
Some("start") => "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]".to_string(),
|
|
227
249
|
Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
|
|
228
250
|
Some("send") => "usage: team-agent send TARGET MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] [--to-name NAME] [--pane PANE] [--task TASK] [--sender SENDER] [--watch-result] [--requires-ack|--no-ack] [--no-wait] [--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]\n\nMVP: name-based cross-workspace addressing assumes trusted local caller; no auth gate.".to_string(),
|
|
@@ -247,6 +269,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
247
269
|
Some("takeover") => "usage: team-agent takeover [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
|
|
248
270
|
Some("claim-leader") => "usage: team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
|
|
249
271
|
Some("attach-leader") => "usage: team-agent attach-leader [--workspace WORKSPACE] [--team TEAM] [--pane PANE] [--provider PROVIDER] [--confirm] [--json]".to_string(),
|
|
272
|
+
Some("attach-app-server-leader") => "usage: team-agent attach-app-server-leader [--workspace WORKSPACE] [--team TEAM] --socket unix:///path.sock --thread-id THREAD_ID [--json]".to_string(),
|
|
250
273
|
Some("identity") => "usage: team-agent identity [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
251
274
|
Some("approvals") => "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
252
275
|
Some("inbox") => "usage: team-agent inbox AGENT [--workspace WORKSPACE] [--team TEAM] [--limit N] [--since CURSOR] [--json]".to_string(),
|
|
@@ -670,12 +693,18 @@ struct ParsedArgs {
|
|
|
670
693
|
pane: Option<String>,
|
|
671
694
|
to_name: Option<String>,
|
|
672
695
|
provider: Option<String>,
|
|
696
|
+
socket: Option<String>,
|
|
697
|
+
thread_id: Option<String>,
|
|
673
698
|
message_id: Option<String>,
|
|
674
699
|
content: Option<String>,
|
|
675
700
|
primary_error: Option<String>,
|
|
676
701
|
agent_id: Option<String>,
|
|
677
702
|
task_id: Option<String>,
|
|
678
703
|
result_json: Option<String>,
|
|
704
|
+
/// 0.5.x Phase 1d Batch 2: quick-start `--backend <tmux|conpty>`.
|
|
705
|
+
/// Raw string (validated at the quick-start builder); the factory
|
|
706
|
+
/// enforces literal semantics.
|
|
707
|
+
backend: Option<String>,
|
|
679
708
|
}
|
|
680
709
|
|
|
681
710
|
fn parse_args(args: &[String]) -> ParsedArgs {
|
|
@@ -717,6 +746,7 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
717
746
|
}
|
|
718
747
|
"--force" => parsed.force = true,
|
|
719
748
|
"--no-display" => parsed.no_display = true,
|
|
749
|
+
"--backend" => parsed.backend = next_arg(args, &mut i),
|
|
720
750
|
"--discard-session" => parsed.discard_session = true,
|
|
721
751
|
"--role-file" => parsed.role_file = next_arg(args, &mut i),
|
|
722
752
|
"--as" => parsed.as_agent = next_arg(args, &mut i),
|
|
@@ -753,6 +783,8 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
753
783
|
"--pane" => parsed.pane = next_arg(args, &mut i),
|
|
754
784
|
"--to-name" => parsed.to_name = next_arg(args, &mut i),
|
|
755
785
|
"--provider" => parsed.provider = next_arg(args, &mut i),
|
|
786
|
+
"--socket" => parsed.socket = next_arg(args, &mut i),
|
|
787
|
+
"--thread-id" => parsed.thread_id = next_arg(args, &mut i),
|
|
756
788
|
"--message-id" => parsed.message_id = next_arg(args, &mut i),
|
|
757
789
|
"--content" => parsed.content = next_arg(args, &mut i),
|
|
758
790
|
"--primary-error" => parsed.primary_error = next_arg(args, &mut i),
|
|
@@ -770,6 +802,12 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
770
802
|
other if other.starts_with("--provider=") => {
|
|
771
803
|
parsed.provider = Some(other.trim_start_matches("--provider=").to_string());
|
|
772
804
|
}
|
|
805
|
+
other if other.starts_with("--socket=") => {
|
|
806
|
+
parsed.socket = Some(other.trim_start_matches("--socket=").to_string());
|
|
807
|
+
}
|
|
808
|
+
other if other.starts_with("--thread-id=") => {
|
|
809
|
+
parsed.thread_id = Some(other.trim_start_matches("--thread-id=").to_string());
|
|
810
|
+
}
|
|
773
811
|
other if other.starts_with('-') => {}
|
|
774
812
|
other => parsed.positionals.push(other.to_string()),
|
|
775
813
|
}
|
|
@@ -833,6 +871,20 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
|
|
|
833
871
|
} else {
|
|
834
872
|
workspace.join(agents_dir)
|
|
835
873
|
};
|
|
874
|
+
// 0.5.x Phase 1d Batch 2: validate the `--backend` literal up-front
|
|
875
|
+
// so users get a fast, actionable error instead of a downstream
|
|
876
|
+
// factory refusal. Accept the same literals as the factory
|
|
877
|
+
// `RequestedTransportBackend::parse_literal`.
|
|
878
|
+
if let Some(literal) = parsed.backend.as_deref() {
|
|
879
|
+
let normalized = literal.trim().to_ascii_lowercase();
|
|
880
|
+
if normalized != "tmux" && normalized != "conpty" {
|
|
881
|
+
return Err(CliError::Usage(format!(
|
|
882
|
+
"--backend must be `tmux` or `conpty`, got {literal:?}. \
|
|
883
|
+
`pty` is not a supported literal in Phase 1d (design \
|
|
884
|
+
§Non-Goals + CR C-1 ②)."
|
|
885
|
+
)));
|
|
886
|
+
}
|
|
887
|
+
}
|
|
836
888
|
Ok(QuickStartArgs {
|
|
837
889
|
workspace,
|
|
838
890
|
agents_dir,
|
|
@@ -841,6 +893,7 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
|
|
|
841
893
|
yes: parsed.yes,
|
|
842
894
|
no_display: parsed.no_display,
|
|
843
895
|
json: parsed.json,
|
|
896
|
+
backend: parsed.backend,
|
|
844
897
|
})
|
|
845
898
|
}
|
|
846
899
|
|
|
@@ -1079,6 +1132,26 @@ fn attach_leader_args(args: &[String], cwd: &Path) -> Result<AttachLeaderArgs, C
|
|
|
1079
1132
|
})
|
|
1080
1133
|
}
|
|
1081
1134
|
|
|
1135
|
+
fn attach_app_server_leader_args(
|
|
1136
|
+
args: &[String],
|
|
1137
|
+
cwd: &Path,
|
|
1138
|
+
) -> Result<AttachAppServerLeaderArgs, CliError> {
|
|
1139
|
+
let parsed = parse_args(args);
|
|
1140
|
+
Ok(AttachAppServerLeaderArgs {
|
|
1141
|
+
workspace: workspace(&parsed, cwd),
|
|
1142
|
+
team: parsed.team,
|
|
1143
|
+
socket: parsed
|
|
1144
|
+
.socket
|
|
1145
|
+
.filter(|socket| !socket.is_empty())
|
|
1146
|
+
.ok_or_else(|| CliError::Usage("missing --socket".to_string()))?,
|
|
1147
|
+
thread_id: parsed
|
|
1148
|
+
.thread_id
|
|
1149
|
+
.filter(|thread_id| !thread_id.is_empty())
|
|
1150
|
+
.ok_or_else(|| CliError::Usage("missing --thread-id".to_string()))?,
|
|
1151
|
+
json: parsed.json,
|
|
1152
|
+
})
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1082
1155
|
fn identity_args(args: &[String], cwd: &Path) -> IdentityArgs {
|
|
1083
1156
|
let parsed = parse_args(args);
|
|
1084
1157
|
IdentityArgs {
|
|
@@ -1444,10 +1517,15 @@ fn peek_args(args: &[String], cwd: &Path) -> Result<PeekArgs, CliError> {
|
|
|
1444
1517
|
fn run_coordinator(args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
|
|
1445
1518
|
let parsed = parse_args(args);
|
|
1446
1519
|
let workspace = crate::coordinator::WorkspacePath::new(workspace(&parsed, cwd));
|
|
1520
|
+
// 0.5.x Windows portability Batch 9 F8: pass `--team` through
|
|
1521
|
+
// to the daemon so it doesn't have to derive from state at
|
|
1522
|
+
// boot time. `parse_args` already recognizes `--team`; we just
|
|
1523
|
+
// thread the value into `DaemonArgs::team_key`.
|
|
1447
1524
|
crate::coordinator::run_daemon(crate::coordinator::DaemonArgs {
|
|
1448
1525
|
workspace,
|
|
1449
1526
|
once: parsed.once,
|
|
1450
1527
|
tick_interval_sec: parsed.tick_interval,
|
|
1528
|
+
team_key: parsed.team.clone(),
|
|
1451
1529
|
})
|
|
1452
1530
|
.map(|()| ExitCode::Ok)
|
|
1453
1531
|
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
@@ -53,6 +53,7 @@ pub(crate) const SEND_REMINDER: &str = "Message delivered. Wait for the worker t
|
|
|
53
53
|
pub(crate) const STATUS_REMINDER: &str = "To wait for results use --watch-result or team-agent collect. Do not capture-pane worker terminals.";
|
|
54
54
|
|
|
55
55
|
pub mod adapters;
|
|
56
|
+
pub mod attach_app_server_leader;
|
|
56
57
|
pub mod diagnose;
|
|
57
58
|
pub mod emit;
|
|
58
59
|
pub mod helpers;
|
|
@@ -64,6 +65,7 @@ pub mod status;
|
|
|
64
65
|
pub mod types;
|
|
65
66
|
|
|
66
67
|
pub use adapters::*;
|
|
68
|
+
pub use attach_app_server_leader::*;
|
|
67
69
|
pub use diagnose::*;
|
|
68
70
|
pub use emit::*;
|
|
69
71
|
pub use leader::*;
|
|
@@ -120,14 +122,16 @@ pub mod lifecycle_port {
|
|
|
120
122
|
team_id: Option<&str>,
|
|
121
123
|
yes: bool,
|
|
122
124
|
open_display: bool,
|
|
125
|
+
backend: Option<&str>,
|
|
123
126
|
) -> Result<Value, CliError> {
|
|
124
|
-
match crate::lifecycle::
|
|
127
|
+
match crate::lifecycle::quick_start_in_workspace_with_display_and_backend(
|
|
125
128
|
workspace,
|
|
126
129
|
agents_dir,
|
|
127
130
|
name,
|
|
128
131
|
yes,
|
|
129
132
|
team_id,
|
|
130
133
|
open_display,
|
|
134
|
+
backend,
|
|
131
135
|
) {
|
|
132
136
|
Ok(report) => Ok(quick_start_value(report)),
|
|
133
137
|
Err(e) => Ok(error_value(e)),
|
|
@@ -192,16 +196,40 @@ pub mod lifecycle_port {
|
|
|
192
196
|
crate::tmux_backend::attach_command_for_workspace(cwd, session, window.as_str())
|
|
193
197
|
}
|
|
194
198
|
/// `runtime.shutdown`(`cmd_shutdown`)。
|
|
199
|
+
///
|
|
200
|
+
/// 0.5.x Phase 1d Batch 5: the workspace-transport branch now routes
|
|
201
|
+
/// through `transport_factory::resolve_read_only_transport` so a
|
|
202
|
+
/// conpty team's shutdown reaches the shim rather than a
|
|
203
|
+
/// no-op tmux backend. Legacy tmux endpoint literal in state still
|
|
204
|
+
/// takes the `shutdown_transport_for_endpoint` tmux channel helper
|
|
205
|
+
/// path (intentional — this is the "attached explicit tmux
|
|
206
|
+
/// endpoint" branch and stays tmux-typed).
|
|
207
|
+
///
|
|
208
|
+
/// `shutdown_with_transport_and_state` is generic over `&dyn
|
|
209
|
+
/// Transport`, so we hand the boxed factory backend down as
|
|
210
|
+
/// `&*box`. Factory refusal falls back to the workspace tmux
|
|
211
|
+
/// backend byte-equivalent to today so daemon liveness paths
|
|
212
|
+
/// don't crash.
|
|
195
213
|
pub fn shutdown(workspace: &Path, keep_logs: bool, team: Option<&str>) -> Result<Value, CliError> {
|
|
196
214
|
let run_ws = crate::model::paths::canonical_run_workspace(workspace)
|
|
197
215
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
198
216
|
let state = shutdown_state_for_team(&run_ws, team)?;
|
|
199
|
-
|
|
200
|
-
shutdown_transport_for_endpoint(endpoint)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
217
|
+
if let Some(endpoint) = legacy_worker_tmux_endpoint(&state) {
|
|
218
|
+
let transport = shutdown_transport_for_endpoint(endpoint);
|
|
219
|
+
return shutdown_with_transport_and_state(
|
|
220
|
+
workspace, keep_logs, team, &transport, Some(state),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
let boxed: Box<dyn crate::transport::Transport> =
|
|
224
|
+
match crate::transport_factory::resolve_read_only_transport(
|
|
225
|
+
&run_ws,
|
|
226
|
+
Some(&state),
|
|
227
|
+
crate::transport_factory::TransportPurpose::Shutdown,
|
|
228
|
+
) {
|
|
229
|
+
Ok(r) => r.backend,
|
|
230
|
+
Err(_) => Box::new(shutdown_workspace_transport(&run_ws)),
|
|
231
|
+
};
|
|
232
|
+
shutdown_with_transport_and_state(workspace, keep_logs, team, boxed.as_ref(), Some(state))
|
|
205
233
|
}
|
|
206
234
|
|
|
207
235
|
/// E12 ①:从 state 锚 pane_id(leader_receiver/team_owner,top+teams)映射到其所在 session
|
|
@@ -915,7 +943,17 @@ pub mod lifecycle_port {
|
|
|
915
943
|
}),
|
|
916
944
|
)
|
|
917
945
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
918
|
-
|
|
946
|
+
// 0.5.x Windows portability Batch 6 Option A (leader
|
|
947
|
+
// constraint 3): if a ConPTY shim was recorded in
|
|
948
|
+
// `state.transport.shim.pid`, terminate it explicitly via
|
|
949
|
+
// `platform::process::terminate_pid`.
|
|
950
|
+
//
|
|
951
|
+
// Batch 7 refinement: the field is emitted into the shutdown
|
|
952
|
+
// JSON only on Windows so Unix golden fixtures stay
|
|
953
|
+
// byte-preserving. The Unix behavior is a no-op (no shim
|
|
954
|
+
// concept there); adding a placeholder key would just noise
|
|
955
|
+
// up the fixtures.
|
|
956
|
+
let mut response = json!({
|
|
919
957
|
"ok": ok,
|
|
920
958
|
"status": status,
|
|
921
959
|
"phase": phase,
|
|
@@ -938,8 +976,59 @@ pub mod lifecycle_port {
|
|
|
938
976
|
"coordinator": {
|
|
939
977
|
"status": coordinator_status,
|
|
940
978
|
"pid": coordinator_pid,
|
|
979
|
+
},
|
|
980
|
+
});
|
|
981
|
+
#[cfg(windows)]
|
|
982
|
+
{
|
|
983
|
+
if let Some(obj) = response.as_object_mut() {
|
|
984
|
+
obj.insert(
|
|
985
|
+
"conpty_shim".to_string(),
|
|
986
|
+
shutdown_conpty_shim(&run_workspace),
|
|
987
|
+
);
|
|
941
988
|
}
|
|
942
|
-
}
|
|
989
|
+
}
|
|
990
|
+
#[cfg(not(windows))]
|
|
991
|
+
{
|
|
992
|
+
let _ = &run_workspace;
|
|
993
|
+
}
|
|
994
|
+
Ok(response)
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/// 0.5.x Windows portability Batch 6 Option A: locate and
|
|
998
|
+
/// terminate the ConPTY shim if `state.transport.shim.pid` is
|
|
999
|
+
/// recorded. Returns a JSON blob for the shutdown response so
|
|
1000
|
+
/// operators can distinguish "no shim was ever launched" from
|
|
1001
|
+
/// "shim terminated cleanly" from "shim already gone".
|
|
1002
|
+
///
|
|
1003
|
+
/// Windows-only; caller (`shutdown_with_transport_and_state`)
|
|
1004
|
+
/// only invokes on Windows. Unix has no shim concept.
|
|
1005
|
+
#[cfg(windows)]
|
|
1006
|
+
fn shutdown_conpty_shim(workspace: &Path) -> serde_json::Value {
|
|
1007
|
+
let Some(pid) = crate::coordinator::conpty_shim::recorded_shim_pid(workspace) else {
|
|
1008
|
+
return json!({ "action": "no_shim_recorded" });
|
|
1009
|
+
};
|
|
1010
|
+
// Prefer graceful termination — Windows maps this to
|
|
1011
|
+
// `TerminationOutcome::ForceOnly` (Batch 3 anchor: no
|
|
1012
|
+
// SIGTERM equivalent for non-console child), which
|
|
1013
|
+
// surfaces the honest audit signal in the JSON.
|
|
1014
|
+
let outcome = crate::platform::process::terminate_pid(
|
|
1015
|
+
pid,
|
|
1016
|
+
crate::platform::process::SignalKind::TerminateForce,
|
|
1017
|
+
);
|
|
1018
|
+
match outcome {
|
|
1019
|
+
Ok(crate::platform::process::TerminationOutcome::Requested) => {
|
|
1020
|
+
json!({ "pid": pid, "action": "terminated" })
|
|
1021
|
+
}
|
|
1022
|
+
Ok(crate::platform::process::TerminationOutcome::AlreadyGone) => {
|
|
1023
|
+
json!({ "pid": pid, "action": "already_gone" })
|
|
1024
|
+
}
|
|
1025
|
+
Ok(crate::platform::process::TerminationOutcome::ForceOnly { reason }) => {
|
|
1026
|
+
json!({ "pid": pid, "action": "terminated_force_only", "reason": reason })
|
|
1027
|
+
}
|
|
1028
|
+
Err(e) => {
|
|
1029
|
+
json!({ "pid": pid, "action": "terminate_failed", "reason": e.to_string() })
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
943
1032
|
}
|
|
944
1033
|
|
|
945
1034
|
/// T5 (harvest §1 / A2): the bounded stop RETAINS the JoinHandle and reclaims the
|
|
@@ -1191,24 +1280,41 @@ pub mod lifecycle_port {
|
|
|
1191
1280
|
}
|
|
1192
1281
|
}
|
|
1193
1282
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1283
|
+
// 0.5.x Phase 1d Batch 5 (design §Batch 5 point 4): a ConPTY
|
|
1284
|
+
// team's residual check MUST NOT read the workspace tmux socket
|
|
1285
|
+
// + shared default tmux socket as evidence for/against ConPTY
|
|
1286
|
+
// residual. Those probes are meaningless for a shim-owned pane
|
|
1287
|
+
// universe: they will always return `has_session=false` (no
|
|
1288
|
+
// tmux session exists) which would look like "no residual" but
|
|
1289
|
+
// gives zero honest information about whether the shim / child
|
|
1290
|
+
// panes have actually been reaped. Skip the tmux fallback
|
|
1291
|
+
// probes when the primary transport is ConPTY; the primary
|
|
1292
|
+
// transport check above already covered the honest question
|
|
1293
|
+
// ("does the shim still have this session?").
|
|
1294
|
+
let primary_is_conpty = matches!(
|
|
1295
|
+
transport.kind(),
|
|
1296
|
+
crate::transport::BackendKind::ConPty
|
|
1297
|
+
);
|
|
1298
|
+
if !primary_is_conpty {
|
|
1299
|
+
let workspace_transport = shutdown_workspace_transport(workspace);
|
|
1300
|
+
match crate::transport::Transport::has_session(&workspace_transport, session) {
|
|
1301
|
+
Ok(true) => residual = true,
|
|
1302
|
+
Ok(false) => {}
|
|
1303
|
+
Err(err) if tmux_absent_error(&err.to_string()) => {}
|
|
1304
|
+
Err(err) => {
|
|
1305
|
+
error.get_or_insert_with(|| err.to_string());
|
|
1306
|
+
residual = true;
|
|
1307
|
+
}
|
|
1202
1308
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1309
|
+
let default_transport = crate::tmux_backend::TmuxBackend::new();
|
|
1310
|
+
match crate::transport::Transport::has_session(&default_transport, session) {
|
|
1311
|
+
Ok(true) => residual = true,
|
|
1312
|
+
Ok(false) => {}
|
|
1313
|
+
Err(err) if tmux_absent_error(&err.to_string()) => {}
|
|
1314
|
+
Err(err) => {
|
|
1315
|
+
error.get_or_insert_with(|| err.to_string());
|
|
1316
|
+
residual = true;
|
|
1317
|
+
}
|
|
1212
1318
|
}
|
|
1213
1319
|
}
|
|
1214
1320
|
let sessions = if residual {
|
|
@@ -1308,34 +1414,48 @@ pub mod lifecycle_port {
|
|
|
1308
1414
|
return;
|
|
1309
1415
|
}
|
|
1310
1416
|
for pid in pids.iter().rev() {
|
|
1311
|
-
|
|
1417
|
+
let _ = crate::platform::process::terminate_pid(
|
|
1418
|
+
*pid,
|
|
1419
|
+
crate::platform::process::SignalKind::TerminateGraceful,
|
|
1420
|
+
);
|
|
1312
1421
|
}
|
|
1313
1422
|
std::thread::sleep(std::time::Duration::from_millis(150));
|
|
1314
1423
|
for pid in pids.iter().rev() {
|
|
1315
|
-
|
|
1424
|
+
let _ = crate::platform::process::terminate_pid(
|
|
1425
|
+
*pid,
|
|
1426
|
+
crate::platform::process::SignalKind::TerminateForce,
|
|
1427
|
+
);
|
|
1316
1428
|
}
|
|
1317
1429
|
wait_for_processes_gone(&pids, std::time::Duration::from_secs(1));
|
|
1318
1430
|
}
|
|
1319
1431
|
|
|
1320
1432
|
fn reap_process_groups(pgids: &[u32], protected: &ShutdownProtection) {
|
|
1433
|
+
// 0.5.x Windows portability Batch 3: routes group termination
|
|
1434
|
+
// through `platform::process::terminate_group`. Unix keeps
|
|
1435
|
+
// `kill(-pgid, SIGTERM|SIGKILL)` semantics byte-for-byte;
|
|
1436
|
+
// Windows returns AlreadyGone (no pgid concept — Job Object
|
|
1437
|
+
// teardown is the shim-side concern per design §Route B).
|
|
1438
|
+
// The `pgid_t <= 1 || protected.contains_pgid(...)` filter
|
|
1439
|
+
// stays in place so pgid 0/1 (kernel/init) and the caller's
|
|
1440
|
+
// own group are never targeted.
|
|
1321
1441
|
for pgid in pgids {
|
|
1322
|
-
|
|
1323
|
-
continue;
|
|
1324
|
-
};
|
|
1325
|
-
if pgid_t <= 1 || protected.contains_pgid(*pgid) {
|
|
1442
|
+
if *pgid <= 1 || protected.contains_pgid(*pgid) {
|
|
1326
1443
|
continue;
|
|
1327
1444
|
}
|
|
1328
|
-
|
|
1445
|
+
let _ = crate::platform::process::terminate_group(
|
|
1446
|
+
*pgid,
|
|
1447
|
+
crate::platform::process::SignalKind::TerminateGraceful,
|
|
1448
|
+
);
|
|
1329
1449
|
}
|
|
1330
1450
|
std::thread::sleep(std::time::Duration::from_millis(150));
|
|
1331
1451
|
for pgid in pgids {
|
|
1332
|
-
|
|
1333
|
-
continue;
|
|
1334
|
-
};
|
|
1335
|
-
if pgid_t <= 1 || protected.contains_pgid(*pgid) {
|
|
1452
|
+
if *pgid <= 1 || protected.contains_pgid(*pgid) {
|
|
1336
1453
|
continue;
|
|
1337
1454
|
}
|
|
1338
|
-
|
|
1455
|
+
let _ = crate::platform::process::terminate_group(
|
|
1456
|
+
*pgid,
|
|
1457
|
+
crate::platform::process::SignalKind::TerminateForce,
|
|
1458
|
+
);
|
|
1339
1459
|
}
|
|
1340
1460
|
}
|
|
1341
1461
|
|
|
@@ -1660,7 +1780,10 @@ pub mod lifecycle_port {
|
|
|
1660
1780
|
let mut protected = ShutdownProtection::default();
|
|
1661
1781
|
let current = std::process::id();
|
|
1662
1782
|
protected.pids.insert(current);
|
|
1663
|
-
|
|
1783
|
+
// 0.5.x Windows portability Batch 3: use platform primitive.
|
|
1784
|
+
// Windows returns None (no pgid concept) and the branch is
|
|
1785
|
+
// skipped honestly.
|
|
1786
|
+
if let Some(pgid) = crate::platform::process::current_process_group() {
|
|
1664
1787
|
protected.pgids.insert(pgid);
|
|
1665
1788
|
}
|
|
1666
1789
|
let mut cursor = current;
|
|
@@ -1795,56 +1918,33 @@ pub mod lifecycle_port {
|
|
|
1795
1918
|
}
|
|
1796
1919
|
}
|
|
1797
1920
|
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
fn send_process_signal_group(pgid: libc::pid_t, signal: libc::c_int) {
|
|
1808
|
-
unsafe {
|
|
1809
|
-
libc::kill(-pgid, signal);
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1921
|
+
// 0.5.x Windows portability Batch 3: the four inline helpers
|
|
1922
|
+
// (send_process_signal, send_process_signal_group,
|
|
1923
|
+
// reap_child_if_possible, process_is_live) have been replaced
|
|
1924
|
+
// by direct calls through `crate::platform::process::*` at the
|
|
1925
|
+
// shutdown callsites above. The private helpers are removed so
|
|
1926
|
+
// there's a single source of truth for signal/waitpid/liveness
|
|
1927
|
+
// semantics; Unix behavior is byte-preserving (SignalKind maps
|
|
1928
|
+
// 1:1 to SIGTERM/SIGKILL, ProcessLiveness::Live == the previous
|
|
1929
|
+
// `kill(pid, 0) == 0 || EPERM` branch).
|
|
1812
1930
|
|
|
1813
1931
|
fn wait_for_processes_gone(pids: &[u32], timeout: std::time::Duration) {
|
|
1814
1932
|
let start = std::time::Instant::now();
|
|
1815
1933
|
loop {
|
|
1816
1934
|
for pid in pids {
|
|
1817
|
-
reap_child_if_possible(*pid);
|
|
1935
|
+
crate::platform::process::reap_child_if_possible(*pid);
|
|
1818
1936
|
}
|
|
1819
|
-
if !pids
|
|
1937
|
+
if !pids
|
|
1938
|
+
.iter()
|
|
1939
|
+
.any(|pid| crate::platform::process::pid_is_alive(*pid))
|
|
1940
|
+
|| start.elapsed() >= timeout
|
|
1941
|
+
{
|
|
1820
1942
|
return;
|
|
1821
1943
|
}
|
|
1822
1944
|
std::thread::sleep(std::time::Duration::from_millis(25));
|
|
1823
1945
|
}
|
|
1824
1946
|
}
|
|
1825
1947
|
|
|
1826
|
-
fn reap_child_if_possible(pid: u32) {
|
|
1827
|
-
let Ok(pid_t) = libc::pid_t::try_from(pid) else {
|
|
1828
|
-
return;
|
|
1829
|
-
};
|
|
1830
|
-
let mut status = 0;
|
|
1831
|
-
unsafe {
|
|
1832
|
-
libc::waitpid(pid_t, &mut status, libc::WNOHANG);
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
fn process_is_live(pid: u32) -> bool {
|
|
1837
|
-
let Ok(pid_t) = libc::pid_t::try_from(pid) else {
|
|
1838
|
-
return false;
|
|
1839
|
-
};
|
|
1840
|
-
let rc = unsafe { libc::kill(pid_t, 0) };
|
|
1841
|
-
if rc == 0 {
|
|
1842
|
-
return true;
|
|
1843
|
-
}
|
|
1844
|
-
let err = std::io::Error::last_os_error();
|
|
1845
|
-
err.raw_os_error() == Some(libc::EPERM)
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
1948
|
fn process_pgids(
|
|
1849
1949
|
pids: &[u32],
|
|
1850
1950
|
protected: &ShutdownProtection,
|
|
@@ -1855,8 +1955,15 @@ pub mod lifecycle_port {
|
|
|
1855
1955
|
.filter_map(|pid| table.iter().find(|process| process.pid == *pid))
|
|
1856
1956
|
.filter_map(|process| process.pgid)
|
|
1857
1957
|
.filter(|pgid| {
|
|
1858
|
-
libc::pid_t
|
|
1859
|
-
|
|
1958
|
+
// 0.5.x Windows portability Batch 4: `libc::pid_t` was
|
|
1959
|
+
// used here as a signed-integer conversion gate to
|
|
1960
|
+
// reject values > INT_MAX. Replace with the equivalent
|
|
1961
|
+
// `i32::try_from` (pgid_t is `c_int` on every Unix we
|
|
1962
|
+
// support). Windows has no pgid concept so `pgids`
|
|
1963
|
+
// is empty in practice — the filter is dead code on
|
|
1964
|
+
// Windows but must still compile.
|
|
1965
|
+
i32::try_from(*pgid)
|
|
1966
|
+
.map(|pgid_int| pgid_int > 1 && !protected.contains_pgid(*pgid))
|
|
1860
1967
|
.unwrap_or(false)
|
|
1861
1968
|
})
|
|
1862
1969
|
.collect::<Vec<_>>();
|
|
@@ -1882,7 +1989,10 @@ pub mod lifecycle_port {
|
|
|
1882
1989
|
.map(|process| process.pid)
|
|
1883
1990
|
.collect::<std::collections::BTreeSet<_>>();
|
|
1884
1991
|
for pid in root_pids {
|
|
1885
|
-
if !protected.contains_pid(*pid)
|
|
1992
|
+
if !protected.contains_pid(*pid)
|
|
1993
|
+
&& crate::platform::process::pid_is_alive(*pid)
|
|
1994
|
+
&& seen.insert(*pid)
|
|
1995
|
+
{
|
|
1886
1996
|
residuals.push(ProcessInfo {
|
|
1887
1997
|
pid: *pid,
|
|
1888
1998
|
ppid: 0,
|