@team-agent/installer 0.5.92 → 0.5.94

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.
@@ -160,6 +160,40 @@ fn coordinator_team_scoped_state(
160
160
  crate::state::projection::select_runtime_state(workspace, Some(selected)).ok()
161
161
  }
162
162
 
163
+ fn live_sibling_team_state(
164
+ state: &Value,
165
+ missing_session: &str,
166
+ transport: &dyn crate::transport::Transport,
167
+ ) -> Result<Option<(String, Value)>, crate::transport::TransportError> {
168
+ let Some(teams) = state.get("teams").and_then(Value::as_object) else {
169
+ return Ok(None);
170
+ };
171
+ for (team_key, team_state) in teams {
172
+ let status = team_state
173
+ .get("status")
174
+ .and_then(Value::as_str)
175
+ .map(|status| status.to_ascii_lowercase());
176
+ if status.is_some_and(|status| status != "alive" && status != "running") {
177
+ continue;
178
+ }
179
+ let Some(session_name) = team_state
180
+ .get("session_name")
181
+ .and_then(Value::as_str)
182
+ .filter(|session| !session.is_empty() && *session != missing_session)
183
+ else {
184
+ continue;
185
+ };
186
+ let session = crate::transport::SessionName::new(session_name);
187
+ if transport.has_session(&session)? {
188
+ return Ok(Some((
189
+ team_key.clone(),
190
+ crate::state::projection::project_top_level_view(state, team_key),
191
+ )));
192
+ }
193
+ }
194
+ Ok(None)
195
+ }
196
+
163
197
  // ===========================================================================
164
198
  // Coordinator struct(daemon lifecycle + tick orchestration)
165
199
  // ===========================================================================
@@ -330,20 +364,40 @@ impl Coordinator {
330
364
  .get("session_name")
331
365
  .and_then(Value::as_str)
332
366
  .filter(|s| !s.is_empty())
367
+ .map(str::to_owned)
333
368
  {
334
- let session = crate::transport::SessionName::new(session_name);
369
+ let session = crate::transport::SessionName::new(&session_name);
335
370
  if !self.transport.has_session(&session)? {
336
371
  event_log.write(
337
372
  "coordinator.session_missing",
338
373
  serde_json::json!({"session": session_name}),
339
374
  )?;
340
- notify_session_missing(self.workspace.as_path(), &state, &event_log, session_name)?;
341
- return Ok(empty_tick_report(
342
- false,
343
- true,
344
- Some(TickStopReason::TmuxSessionMissing),
345
- None,
346
- ));
375
+ if let Some((sibling_team, sibling_state)) =
376
+ live_sibling_team_state(&state, &session_name, self.transport.as_ref())?
377
+ {
378
+ event_log.write(
379
+ "coordinator.session_missing_sibling_alive",
380
+ serde_json::json!({
381
+ "session": session_name,
382
+ "sibling_team": sibling_team,
383
+ "action": "continue_with_sibling_team",
384
+ }),
385
+ )?;
386
+ state = sibling_state;
387
+ } else {
388
+ notify_session_missing(
389
+ self.workspace.as_path(),
390
+ &state,
391
+ &event_log,
392
+ &session_name,
393
+ )?;
394
+ return Ok(empty_tick_report(
395
+ false,
396
+ true,
397
+ Some(TickStopReason::TmuxSessionMissing),
398
+ None,
399
+ ));
400
+ }
347
401
  }
348
402
  }
349
403
 
@@ -164,6 +164,8 @@ pub enum StopOutcome {
164
164
  Missing,
165
165
  /// pid 非整数 → 清文件返回(`lifecycle.py:238`)。
166
166
  InvalidPidRemoved,
167
+ /// The pid file points to a live process owned by another workspace.
168
+ NotOwned,
167
169
  /// SIGTERM 失败(`lifecycle.py:243`)。
168
170
  KillFailed,
169
171
  /// SIGTERM + 清 pid/meta 成功(`lifecycle.py:247`)。
@@ -2,7 +2,7 @@
2
2
 
3
3
  use std::path::Path;
4
4
 
5
- use serde_json::json;
5
+ use serde_json::{json, Value};
6
6
 
7
7
  use crate::event_log::{EventLog, EventLogError};
8
8
  use crate::transport::{BackendKind, Transport};
@@ -29,6 +29,45 @@ pub(crate) fn pre_kill_audit(
29
29
  "argv": argv,
30
30
  "endpoint": endpoint,
31
31
  "targets": targets,
32
+ "workspace": Value::Null,
33
+ "team": Value::Null,
34
+ "generation": Value::Null,
35
+ "owner_evidence": "legacy_unscoped",
36
+ "reason": "legacy_callsite",
37
+ }),
38
+ )?;
39
+ Ok(())
40
+ }
41
+
42
+ pub(crate) fn pre_kill_audit_scoped(
43
+ event_log: &EventLog,
44
+ transport: &dyn Transport,
45
+ caller: &str,
46
+ action: &str,
47
+ targets: &[String],
48
+ workspace: &Path,
49
+ team: Option<&str>,
50
+ generation: Option<&str>,
51
+ owner_evidence: &str,
52
+ reason: &str,
53
+ ) -> Result<(), EventLogError> {
54
+ let endpoint = transport.tmux_endpoint();
55
+ let argv = destructive_argv(transport.kind(), endpoint.as_deref(), action, targets);
56
+ event_log.write(
57
+ "transport.pre_kill_audit",
58
+ json!({
59
+ "phase": "pre_call",
60
+ "caller": caller,
61
+ "caller_pid": std::process::id(),
62
+ "action": action,
63
+ "argv": argv,
64
+ "endpoint": endpoint,
65
+ "targets": targets,
66
+ "workspace": workspace.to_string_lossy(),
67
+ "team": team,
68
+ "generation": generation,
69
+ "owner_evidence": owner_evidence,
70
+ "reason": reason,
32
71
  }),
33
72
  )?;
34
73
  Ok(())
@@ -57,7 +96,7 @@ fn destructive_argv(
57
96
  vec![format!("{kind:?}").to_ascii_lowercase()]
58
97
  };
59
98
  argv.push(action.to_string());
60
- if action == KILL_SESSION {
99
+ if action == KILL_SESSION || action == "kill-pane" {
61
100
  for target in targets {
62
101
  argv.push("-t".to_string());
63
102
  argv.push(target.clone());
@@ -124,11 +124,7 @@ impl FreshQuickStartLeaderBindingOps for RuntimeFreshQuickStartLeaderBindingOps<
124
124
  }
125
125
 
126
126
  fn observe_command(&mut self, pane: &PaneId) -> Option<String> {
127
- self.transport
128
- .query(&Target::Pane(pane.clone()), PaneField::PaneCurrentCommand)
129
- .ok()
130
- .flatten()
131
- .filter(|command| !command.trim().is_empty())
127
+ query_caller_pane_command(self.transport, pane)
132
128
  }
133
129
 
134
130
  fn attach(
@@ -142,7 +138,7 @@ impl FreshQuickStartLeaderBindingOps for RuntimeFreshQuickStartLeaderBindingOps<
142
138
  self.applied_grant = None;
143
139
  self.registry_receipt = None;
144
140
  let event_log = crate::event_log::EventLog::new(workspace);
145
- let targets = match self.transport.list_targets() {
141
+ let targets = match list_caller_targets(self.transport) {
146
142
  Ok(targets) => targets,
147
143
  Err(_) => {
148
144
  self.last_failure_reason = Some("caller_pane_unobservable");
@@ -290,11 +286,19 @@ impl FreshQuickStartLeaderBindingOps for RuntimeFreshQuickStartLeaderBindingOps<
290
286
  self.last_failure_reason = Some("receiver_missing");
291
287
  return false;
292
288
  };
289
+ let leader_transport = receiver
290
+ .get("tmux_socket")
291
+ .and_then(serde_json::Value::as_str)
292
+ .filter(|endpoint| !endpoint.is_empty())
293
+ .map(crate::transport_factory::leader_endpoint_transport);
294
+ let live_channel_transport: &dyn Transport = leader_transport
295
+ .as_deref()
296
+ .unwrap_or(self.transport);
293
297
  if !matches!(
294
298
  crate::messaging::resolve_live_leader_channel(
295
299
  workspace,
296
300
  receiver,
297
- self.transport,
301
+ live_channel_transport,
298
302
  ),
299
303
  crate::messaging::LeaderChannelResolution::Live(_)
300
304
  ) {
@@ -875,6 +879,42 @@ fn existing_runtime_identity_view(
875
879
  state.clone()
876
880
  }
877
881
 
882
+ fn caller_read_transport() -> Option<Box<dyn Transport>> {
883
+ let endpoint = crate::tmux_backend::socket_name_from_tmux_env()?;
884
+ // Unit tests use an injected transport even when they seed a synthetic TMUX
885
+ // tuple. Never make those hermetic tests depend on a host tmux server; the
886
+ // production path below remains a read-only endpoint probe.
887
+ #[cfg(test)]
888
+ if !Path::new(&endpoint).exists() {
889
+ return None;
890
+ }
891
+ Some(Box::new(crate::tmux_backend::TmuxBackend::for_tmux_endpoint(
892
+ &endpoint,
893
+ )))
894
+ }
895
+
896
+ fn query_caller_pane_command(transport: &dyn Transport, pane: &PaneId) -> Option<String> {
897
+ let caller_transport = caller_read_transport();
898
+ let query_transport: &dyn Transport = caller_transport
899
+ .as_deref()
900
+ .unwrap_or(transport);
901
+ query_transport
902
+ .query(&Target::Pane(pane.clone()), PaneField::PaneCurrentCommand)
903
+ .ok()
904
+ .flatten()
905
+ .filter(|command| !command.trim().is_empty())
906
+ }
907
+
908
+ fn list_caller_targets(
909
+ transport: &dyn Transport,
910
+ ) -> Result<Vec<crate::transport::PaneInfo>, crate::transport::TransportError> {
911
+ if let Some(caller_transport) = caller_read_transport() {
912
+ caller_transport.list_targets()
913
+ } else {
914
+ transport.list_targets()
915
+ }
916
+ }
917
+
878
918
  fn preflight_fresh_leader_identity(
879
919
  transport: &dyn Transport,
880
920
  ) -> Option<(String, Vec<String>, Vec<String>)> {
@@ -900,13 +940,7 @@ fn preflight_fresh_leader_identity(
900
940
  let Some(pane) = pane else {
901
941
  return None;
902
942
  };
903
- let command = transport
904
- .query(
905
- &Target::Pane(PaneId::new(pane)),
906
- PaneField::PaneCurrentCommand,
907
- )
908
- .ok()
909
- .flatten()
943
+ let command = query_caller_pane_command(transport, &PaneId::new(pane))
910
944
  .unwrap_or_default();
911
945
  let explicit = std::env::var("TEAM_AGENT_LEADER_PROVIDER")
912
946
  .ok()
@@ -3,7 +3,7 @@
3
3
  //! contract:
4
4
  //! provides:
5
5
  //! - name: quick_start_tmux_backend
6
- //! what: 优先复用调用方所在 tmux socket,否则按 workspace 派生
6
+ //! what: 按 workspace 派生私有 socket;调用方 tmux 只用于 leader receiver 绑定
7
7
  //! - name: annotate_runtime_transport
8
8
  //! what: 把 transport 种类与来源写进 state,tmux 时再写 endpoint 三字段
9
9
  //! - name: attach_commands_for_runtime_windows
@@ -37,15 +37,11 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
37
37
  use super::*;
38
38
 
39
39
  /// ---
40
- /// purpose: 定出 quick-start 使用的 tmux 后端
41
- /// returns: 调用方环境里有 socket 就复用它,否则按 workspace 派生一个
40
+ /// purpose: 定出 quick-start 使用的私有 tmux 后端
41
+ /// returns: 始终按 workspace 派生 endpoint;调用方 $TMUX 仅供 leader receiver 身份绑定
42
42
  /// ---
43
43
  pub(crate) fn quick_start_tmux_backend(workspace: &Path) -> crate::tmux_backend::TmuxBackend {
44
- if let Some(endpoint) = crate::tmux_backend::socket_name_from_tmux_env() {
45
- crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&endpoint)
46
- } else {
47
- crate::tmux_backend::TmuxBackend::for_workspace(workspace)
48
- }
44
+ crate::tmux_backend::TmuxBackend::for_workspace(workspace)
49
45
  }
50
46
 
51
47
  /// ---
@@ -146,6 +146,23 @@ pub fn launch_with_transport_in_workspace(
146
146
  transport,
147
147
  &started,
148
148
  )?;
149
+ let owner_team = load_runtime_state(workspace)
150
+ .ok()
151
+ .map(|state| crate::state::projection::team_state_key(&state))
152
+ .filter(|team| !team.is_empty())
153
+ .unwrap_or_else(|| session_name.as_str().to_string());
154
+ let generation = started
155
+ .first()
156
+ .map(|agent| agent.spawned_at.as_str())
157
+ .unwrap_or(session_name.as_str());
158
+ transport
159
+ .set_session_owner_with_generation(
160
+ &session_name,
161
+ workspace,
162
+ &owner_team,
163
+ generation,
164
+ )
165
+ .map_err(|error| LifecycleError::Transport(error.to_string()))?;
149
166
  // 0.5.38: per-worker timing tags (source="launch") so operators can
150
167
  // trace which worker's spawn dominates wall time. Zeros for now on
151
168
  // the sub-timings — Step 1 first enables shape assertion; a later
@@ -2,6 +2,7 @@
2
2
  mod hermetic_guard;
3
3
 
4
4
  use super::*;
5
+ use crate::tmux_backend::TmuxBackend;
5
6
  use crate::transport::test_support::OfflineTransport;
6
7
  use serde_json::json;
7
8
  use serial_test::serial;
@@ -1428,19 +1429,21 @@ fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
1428
1429
 
1429
1430
  #[test]
1430
1431
  #[serial(env)]
1431
- fn quick_start_tmux_backend_prefers_absolute_tmux_env_endpoint() {
1432
+ fn quick_start_tmux_backend_uses_workspace_private_endpoint() {
1432
1433
  let leader_socket = "/tmp/team-agent-layout-leader-socket";
1433
1434
  let _tmux = EnvVarGuard::set("TMUX", &format!("{leader_socket},123,0"));
1434
- let backend = quick_start_tmux_backend(std::path::Path::new("/tmp/layout-workspace"));
1435
+ let workspace = std::path::Path::new("/tmp/layout-workspace");
1436
+ let backend = quick_start_tmux_backend(workspace);
1437
+ let expected = TmuxBackend::for_workspace(workspace);
1435
1438
 
1436
1439
  assert_eq!(
1437
- crate::transport::Transport::tmux_endpoint(&backend).as_deref(),
1438
- Some(leader_socket),
1439
- "quick-start from inside tmux must use the caller's full $TMUX socket endpoint"
1440
+ crate::transport::Transport::tmux_endpoint(&backend),
1441
+ crate::transport::Transport::tmux_endpoint(&expected),
1442
+ "quick-start workers must use the workspace-private socket, not ambient $TMUX"
1440
1443
  );
1441
1444
  assert_eq!(
1442
- selected_tmux_socket_source(&backend, std::path::Path::new("/tmp/layout-workspace")),
1443
- Some("leader_env")
1445
+ selected_tmux_socket_source(&backend, workspace),
1446
+ Some("workspace")
1444
1447
  );
1445
1448
  }
1446
1449
 
@@ -5,7 +5,7 @@ use crate::cli::{
5
5
  SendArgs, StatusArgs,
6
6
  };
7
7
  use crate::transport::test_support::OfflineTransport;
8
- use crate::transport::WindowName;
8
+ use crate::transport::{PaneId, PaneInfo, SessionName, WindowName};
9
9
  use serde_json::{json, Map, Value};
10
10
  use std::collections::{BTreeMap, BTreeSet};
11
11
  use std::path::{Path, PathBuf};
@@ -229,7 +229,21 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
229
229
  let team = two_worker_team_dir(&hermetic);
230
230
  let workspace = team.parent().expect("workspace").to_path_buf();
231
231
  seed_healthy_coordinator(&workspace);
232
- let launch_transport = codex_ready_transport();
232
+ let _caller_pane = EnvVarGuard::set("TMUX_PANE", "%caller");
233
+ let launch_transport = codex_ready_transport().with_targets(vec![PaneInfo {
234
+ pane_id: PaneId::new("%caller"),
235
+ session: SessionName::new("caller"),
236
+ window_index: Some(0),
237
+ window_name: Some(WindowName::new("caller")),
238
+ pane_index: Some(0),
239
+ tty: None,
240
+ current_command: Some("codex".to_string()),
241
+ current_path: Some(workspace.clone()),
242
+ active: true,
243
+ pane_pid: None,
244
+ leader_env: BTreeMap::new(),
245
+ }])
246
+ .with_pane_current_command("%caller", "codex");
233
247
  let quick_start = quick_start_with_transport_in_workspace_with_display(
234
248
  &workspace,
235
249
  &team,
@@ -275,9 +289,49 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
275
289
  to_name: None,
276
290
  to_leader: None,
277
291
  });
292
+ let runtime_state = crate::state::persist::load_runtime_state(&workspace)
293
+ .expect("quick-start state written before shutdown fixture");
294
+ let generation = runtime_state
295
+ .get("generation")
296
+ .and_then(Value::as_str)
297
+ .or_else(|| {
298
+ runtime_state
299
+ .get("agents")
300
+ .and_then(Value::as_object)
301
+ .and_then(|agents| {
302
+ agents.values().find_map(|agent| {
303
+ agent.get("spawned_at").and_then(Value::as_str)
304
+ })
305
+ })
306
+ })
307
+ .or_else(|| {
308
+ runtime_state
309
+ .pointer("/teams/teamdir/agents")
310
+ .and_then(Value::as_object)
311
+ .and_then(|agents| {
312
+ agents.values().find_map(|agent| {
313
+ agent.get("spawned_at").and_then(Value::as_str)
314
+ })
315
+ })
316
+ })
317
+ .unwrap_or("phase-golden");
278
318
  let lifecycle_transport = codex_ready_transport()
279
319
  .with_session_present(true)
280
- .with_windows(vec![WindowName::new("w1"), WindowName::new("w2")]);
320
+ .with_windows(vec![WindowName::new("w1"), WindowName::new("w2")])
321
+ .with_targets(vec![PaneInfo {
322
+ pane_id: PaneId::new("%w1"),
323
+ session: SessionName::new("team-phasegolden"),
324
+ window_index: Some(0),
325
+ window_name: Some(WindowName::new("w1")),
326
+ pane_index: Some(0),
327
+ tty: None,
328
+ current_command: Some("codex".to_string()),
329
+ current_path: Some(workspace.clone()),
330
+ active: true,
331
+ pane_pid: None,
332
+ leader_env: BTreeMap::new(),
333
+ }])
334
+ .with_session_owner(&workspace, "teamdir", generation);
281
335
  let lifecycle = (spec.lifecycle_op)(&workspace, &lifecycle_transport, spec.team_key);
282
336
  let shutdown = lifecycle_port::shutdown_with_transport(
283
337
  &workspace,
@@ -660,9 +714,12 @@ fn normalize_value(value: Value, ctx: &mut NormalizeCtx, key: Option<&str>) -> V
660
714
 
661
715
  fn normalize_string(text: String, ctx: &mut NormalizeCtx, key: Option<&str>) -> Value {
662
716
  let key = key.unwrap_or_default();
663
- if key_is_timestamp(key) {
717
+ if key_is_timestamp(key) || key == "generation" {
664
718
  return json!("<TS>");
665
719
  }
720
+ if key == "workspace_hash" {
721
+ return json!("<ID>");
722
+ }
666
723
  if text == crate::packaging::Version::current().as_str() {
667
724
  return json!("<VERSION>");
668
725
  }
@@ -696,9 +753,39 @@ fn normalize_path_string(text: &str, ctx: &NormalizeCtx) -> String {
696
753
  out = out.replace(alias, "<TMP>");
697
754
  }
698
755
  out = normalize_tmux_socket_dir(&out);
756
+ out = normalize_hermetic_root(&out);
757
+ out = normalize_registry_hash(&out);
699
758
  normalize_socket_token(&out)
700
759
  }
701
760
 
761
+ fn normalize_registry_hash(text: &str) -> String {
762
+ let Some(start) = text.rfind("/leaders/") else {
763
+ return text.to_string();
764
+ };
765
+ let file_start = start + "/leaders/".len();
766
+ let Some(separator) = text[file_start..].find("__") else {
767
+ return text.to_string();
768
+ };
769
+ let separator = file_start + separator;
770
+ text[..file_start].to_owned() + "<ID>" + &text[separator..]
771
+ }
772
+
773
+ fn normalize_hermetic_root(text: &str) -> String {
774
+ let marker = "ta-phase-golden-";
775
+ let Some(start) = text.find(marker) else {
776
+ return text.to_string();
777
+ };
778
+ let end = text[start..]
779
+ .find('/')
780
+ .map(|offset| start + offset)
781
+ .unwrap_or(text.len());
782
+ let segment = &text[start..end];
783
+ let Some(phase) = text[start + marker.len()..end].split('-').next() else {
784
+ return text.to_string();
785
+ };
786
+ text.replacen(segment, &format!("{marker}{phase}"), 1)
787
+ }
788
+
702
789
  fn value_looks_like_endpoint_path(text: &str) -> bool {
703
790
  text.starts_with('/')
704
791
  || text.starts_with("ta-")
@@ -12,8 +12,8 @@
12
12
  //! `GetExitCodeProcess`, `TerminateProcess`, Toolhelp snapshot,
13
13
  //! Job Objects for shim-owned worker teardown)
14
14
  //!
15
- //! CR C-6: `terminate_pid` / `terminate_group` return
16
- //! `TerminationOutcome` so a caller that requested `TerminateGraceful`
15
+ //! CR C-6: `terminate_pid` returns `TerminationOutcome` so a caller
16
+ //! that requested `TerminateGraceful`
17
17
  //! can see that Windows downgraded to `TerminateForce` and emit a
18
18
  //! `platform.terminate_force_only` event (N38 交底).
19
19
  //!
@@ -41,7 +41,7 @@ pub enum SignalKind {
41
41
  TerminateForce,
42
42
  }
43
43
 
44
- /// Result of a `terminate_pid` / `terminate_group` call. `Graceful`
44
+ /// Result of a `terminate_pid` call. `Graceful`
45
45
  /// means the OS honored the request kind; `ForceOnly` means Windows
46
46
  /// downgraded a `TerminateGraceful` to `TerminateProcess` (CR C-6
47
47
  /// N38 交底 — caller must emit a `platform.terminate_force_only`
@@ -55,7 +55,7 @@ pub enum TerminationOutcome {
55
55
  /// (no grace period). Includes a machine-readable reason so
56
56
  /// callers can log it in the audit event.
57
57
  ForceOnly { reason: &'static str },
58
- /// The pid/group was already gone by the time the call resolved.
58
+ /// The pid was already gone by the time the call resolved.
59
59
  /// Not an error.
60
60
  AlreadyGone,
61
61
  }
@@ -216,34 +216,6 @@ mod unix_impl {
216
216
  }
217
217
  }
218
218
 
219
- /// Send a signal to a process group (`kill(-pgid, ...)`).
220
- /// Byte-equivalent to `cli/mod.rs:1854` (`libc::kill(-pgid, signal)`)
221
- /// used by `send_process_signal_group`.
222
- pub fn terminate_group(
223
- group_id: u32,
224
- kind: SignalKind,
225
- ) -> Result<TerminationOutcome, io::Error> {
226
- let signal = match kind {
227
- SignalKind::TerminateGraceful => libc::SIGTERM,
228
- SignalKind::TerminateForce => libc::SIGKILL,
229
- };
230
- let pgid_t = match libc::pid_t::try_from(group_id) {
231
- Ok(p) => p,
232
- Err(_) => return Ok(TerminationOutcome::AlreadyGone),
233
- };
234
- // `-pgid` targets every process in that group. Preserves the
235
- // existing shutdown semantics inline in cli/mod.rs.
236
- let rc = unsafe { libc::kill(-pgid_t, signal) };
237
- if rc == 0 {
238
- return Ok(TerminationOutcome::Requested);
239
- }
240
- let err = io::Error::last_os_error();
241
- match err.raw_os_error() {
242
- Some(code) if code == libc::ESRCH => Ok(TerminationOutcome::AlreadyGone),
243
- _ => Err(err),
244
- }
245
- }
246
-
247
219
  /// Best-effort waitpid so a killed child doesn't accumulate as a
248
220
  /// zombie. Byte-equivalent to `coordinator/health.rs:350-358` and
249
221
  /// `cli/mod.rs:1871-1879`.
@@ -434,19 +406,6 @@ mod windows_impl {
434
406
  })
435
407
  }
436
408
 
437
- pub fn terminate_group(
438
- _group_id: u32,
439
- _kind: SignalKind,
440
- ) -> Result<TerminationOutcome, io::Error> {
441
- // Windows has no `-pgid` sentinel. Design §Route B: "owned
442
- // worker teardown: prefer Job Objects in the ConPTY shim".
443
- // Job-object teardown is a shim-side concern, not a top-level
444
- // API. Return AlreadyGone honestly here so shutdown code
445
- // falls back to per-pid termination (the caller's outer
446
- // loop retries per-pid after the group attempt is a no-op).
447
- Ok(TerminationOutcome::AlreadyGone)
448
- }
449
-
450
409
  pub fn reap_child_if_possible(_pid: u32) {
451
410
  // Windows has no zombie waitpid model. Child handles are
452
411
  // owned by the spawner and closed at drop; nothing to do.