@team-agent/installer 0.3.14 → 0.3.15

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.
Files changed (36) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -0
  4. package/crates/team-agent/src/cli/emit.rs +92 -11
  5. package/crates/team-agent/src/cli/mod.rs +60 -10
  6. package/crates/team-agent/src/cli/send.rs +252 -0
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
  8. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +159 -2
  9. package/crates/team-agent/src/cli/types.rs +30 -1
  10. package/crates/team-agent/src/compiler/tests.rs +2 -2
  11. package/crates/team-agent/src/compiler.rs +1 -1
  12. package/crates/team-agent/src/fake_worker.rs +32 -145
  13. package/crates/team-agent/src/lifecycle/display.rs +3 -3
  14. package/crates/team-agent/src/lifecycle/launch.rs +560 -69
  15. package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
  16. package/crates/team-agent/src/lifecycle/restart/common.rs +38 -2
  17. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +72 -1
  18. package/crates/team-agent/src/lifecycle/tests/core.rs +14 -5
  19. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +668 -8
  20. package/crates/team-agent/src/lifecycle/types.rs +5 -0
  21. package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
  22. package/crates/team-agent/src/mcp_server/wire.rs +153 -3
  23. package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
  24. package/crates/team-agent/src/messaging/mod.rs +6 -3
  25. package/crates/team-agent/src/messaging/results.rs +240 -158
  26. package/crates/team-agent/src/messaging/send.rs +3 -2
  27. package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
  28. package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
  29. package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
  30. package/crates/team-agent/src/os_probe.rs +11 -0
  31. package/crates/team-agent/src/state/persist.rs +6 -0
  32. package/crates/team-agent/src/tmux_backend.rs +85 -0
  33. package/crates/team-agent/src/transport/test_support.rs +46 -1
  34. package/crates/team-agent/src/transport.rs +27 -0
  35. package/package.json +4 -4
  36. package/skills/team-agent/references/recovery-runbook.md +277 -0
@@ -158,11 +158,33 @@ fn tmp_shutdown_workspace(tag: &str) -> PathBuf {
158
158
 
159
159
  struct CleanShutdownTransport {
160
160
  session_present: Mutex<bool>,
161
+ targets: Vec<PaneInfo>,
162
+ kill_server_called: Mutex<bool>,
163
+ probe_timeout_kind: Option<&'static str>,
161
164
  }
162
165
 
163
166
  impl CleanShutdownTransport {
164
167
  fn new() -> Self {
165
- Self { session_present: Mutex::new(true) }
168
+ Self {
169
+ session_present: Mutex::new(true),
170
+ targets: Vec::new(),
171
+ kill_server_called: Mutex::new(false),
172
+ probe_timeout_kind: None,
173
+ }
174
+ }
175
+
176
+ fn with_targets(mut self, targets: Vec<PaneInfo>) -> Self {
177
+ self.targets = targets;
178
+ self
179
+ }
180
+
181
+ fn kill_server_called(&self) -> bool {
182
+ *self.kill_server_called.lock().unwrap()
183
+ }
184
+
185
+ fn with_probe_timeout(mut self, probe: &'static str) -> Self {
186
+ self.probe_timeout_kind = Some(probe);
187
+ self
166
188
  }
167
189
  }
168
190
 
@@ -227,7 +249,11 @@ impl Transport for CleanShutdownTransport {
227
249
  }
228
250
 
229
251
  fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
230
- Ok(Vec::new())
252
+ if *self.session_present.lock().unwrap() {
253
+ Ok(self.targets.clone())
254
+ } else {
255
+ Ok(Vec::new())
256
+ }
231
257
  }
232
258
 
233
259
  fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
@@ -248,10 +274,18 @@ impl Transport for CleanShutdownTransport {
248
274
  }
249
275
 
250
276
  fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
277
+ if let Some(probe) = self.probe_timeout_kind {
278
+ crate::os_probe::set_probe_timeout_for_test(probe, None, 900);
279
+ }
251
280
  *self.session_present.lock().unwrap() = false;
252
281
  Ok(())
253
282
  }
254
283
 
284
+ fn kill_server(&self) -> Result<(), TransportError> {
285
+ *self.kill_server_called.lock().unwrap() = true;
286
+ Ok(())
287
+ }
288
+
255
289
  fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
256
290
  Ok(())
257
291
  }
@@ -260,3 +294,126 @@ impl Transport for CleanShutdownTransport {
260
294
  Ok(AttachOutcome::Attached)
261
295
  }
262
296
  }
297
+
298
+ #[test]
299
+ fn lsof_cwd_timeout_is_diagnostic_not_shutdown_partial() {
300
+ let ws = tmp_shutdown_workspace("lsof-cwd-timeout-diagnostic");
301
+ crate::state::persist::save_runtime_state(
302
+ &ws,
303
+ &json!({
304
+ "session_name": "team-lsof-cwd-timeout",
305
+ "agents": {
306
+ "fake_impl": {
307
+ "status": "running",
308
+ "provider": "fake",
309
+ "window": "fake_impl"
310
+ }
311
+ }
312
+ }),
313
+ )
314
+ .unwrap();
315
+
316
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(
317
+ &ws,
318
+ true,
319
+ None,
320
+ &CleanShutdownTransport::new().with_probe_timeout("lsof_cwd"),
321
+ )
322
+ .expect("shutdown should complete");
323
+
324
+ assert_eq!(out["ok"], json!(true));
325
+ assert_eq!(out["status"], json!("ok"));
326
+ assert_eq!(out["phase"], json!(null));
327
+ assert_eq!(out["verification_degraded"], json!(true));
328
+ assert_eq!(out["probe_timeout_kind"], json!("lsof_cwd"));
329
+ assert_eq!(out["residuals"]["sessions"], json!([]));
330
+ assert_eq!(out["residuals"]["processes"], json!([]));
331
+
332
+ let events = crate::event_log::EventLog::new(&ws).tail(0).expect("events");
333
+ let shutdown = events
334
+ .iter()
335
+ .find(|event| {
336
+ event.get("event").and_then(serde_json::Value::as_str) == Some("lifecycle.shutdown")
337
+ })
338
+ .unwrap_or_else(|| panic!("missing lifecycle.shutdown event: {events:?}"));
339
+ assert_eq!(shutdown["status"], json!("ok"));
340
+ assert_eq!(shutdown["phase"], json!(null));
341
+ assert_eq!(shutdown["verification_degraded"], json!(true));
342
+ assert_eq!(shutdown["probe_timeout_kind"], json!("lsof_cwd"));
343
+ }
344
+
345
+ #[test]
346
+ fn ps_table_timeout_still_degrades_shutdown_truth() {
347
+ let ws = tmp_shutdown_workspace("ps-table-timeout-partial");
348
+ crate::state::persist::save_runtime_state(
349
+ &ws,
350
+ &json!({
351
+ "session_name": "team-ps-table-timeout",
352
+ "agents": {
353
+ "fake_impl": {
354
+ "status": "running",
355
+ "provider": "fake",
356
+ "window": "fake_impl"
357
+ }
358
+ }
359
+ }),
360
+ )
361
+ .unwrap();
362
+
363
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(
364
+ &ws,
365
+ true,
366
+ None,
367
+ &CleanShutdownTransport::new().with_probe_timeout("ps_table"),
368
+ )
369
+ .expect("shutdown should complete");
370
+
371
+ assert_eq!(out["ok"], json!(false));
372
+ assert_eq!(out["status"], json!("partial"));
373
+ assert_eq!(out["phase"], json!("os_probe"));
374
+ assert_eq!(out["verification_degraded"], json!(true));
375
+ assert_eq!(out["probe_timeout_kind"], json!("ps_table"));
376
+ }
377
+
378
+ #[test]
379
+ fn leader_env_tmux_socket_never_kills_server_even_when_sessions_look_exclusive() {
380
+ let ws = tmp_shutdown_workspace("leader-env-socket-no-kill-server");
381
+ crate::state::persist::save_runtime_state(
382
+ &ws,
383
+ &json!({
384
+ "tmux_socket_source": "leader_env",
385
+ "agents": {
386
+ "fake_impl": {
387
+ "status": "running",
388
+ "provider": "fake",
389
+ "window": "fake_impl"
390
+ }
391
+ }
392
+ }),
393
+ )
394
+ .unwrap();
395
+ let transport = CleanShutdownTransport::new().with_targets(vec![PaneInfo {
396
+ pane_id: PaneId::new("%1"),
397
+ session: SessionName::new("team-layout"),
398
+ window_index: Some(0),
399
+ window_name: Some(WindowName::new("team-w1")),
400
+ pane_index: Some(0),
401
+ tty: None,
402
+ current_command: None,
403
+ current_path: None,
404
+ active: true,
405
+ pane_pid: None,
406
+ leader_env: BTreeMap::new(),
407
+ }]);
408
+
409
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
410
+ .expect("shutdown should complete");
411
+
412
+ assert_eq!(out["ok"], json!(true));
413
+ assert_eq!(out["killed_sessions"], json!(["team-layout"]));
414
+ assert_eq!(out["spared_sessions"], json!([]));
415
+ assert!(
416
+ !transport.kill_server_called(),
417
+ "leader-env/shared socket shutdown must kill sessions individually, never kill-server"
418
+ );
419
+ }
@@ -97,7 +97,8 @@ impl CliError {
97
97
  "tmux session `{session}` already exists. It may be an active team. Do not terminate existing tmux sessions from startup; use a different team name or runtime.session_name and start again."
98
98
  );
99
99
  payload.next_actions = Some(vec![
100
- "Use a different team name or runtime.session_name before starting again.".to_string(),
100
+ "Use a different team name or runtime.session_name before starting again."
101
+ .to_string(),
101
102
  ]);
102
103
  }
103
104
  }
@@ -262,6 +263,7 @@ pub struct QuickStartArgs {
262
263
  pub team_id: Option<String>,
263
264
  pub yes: bool,
264
265
  pub fresh: bool,
266
+ pub no_display: bool,
265
267
  pub json: bool,
266
268
  }
267
269
 
@@ -304,6 +306,33 @@ pub struct SendArgs {
304
306
  pub message_id: Option<String>,
305
307
  }
306
308
 
309
+ /// E23 worker-side emergency fallback for `team_orchestrator.send_message`
310
+ /// transport failures. This is not a general control-plane send path.
311
+ #[derive(Debug, Clone, PartialEq, Eq)]
312
+ pub struct FallbackSendLeaderArgs {
313
+ pub workspace: PathBuf,
314
+ pub team: Option<String>,
315
+ pub sender: String,
316
+ pub task: Option<String>,
317
+ pub message_id: String,
318
+ pub content: String,
319
+ pub primary_error: String,
320
+ pub json: bool,
321
+ }
322
+
323
+ /// E23 worker-side emergency fallback for `team_orchestrator.report_result`
324
+ /// transport failures. It must still persist through the results DB.
325
+ #[derive(Debug, Clone, PartialEq, Eq)]
326
+ pub struct FallbackReportResultArgs {
327
+ pub workspace: PathBuf,
328
+ pub team: Option<String>,
329
+ pub agent_id: String,
330
+ pub task_id: String,
331
+ pub result_json: String,
332
+ pub primary_error: String,
333
+ pub json: bool,
334
+ }
335
+
307
336
  /// `allow-peer-talk`(`parser.py`): allow direct peer communication between two agents.
308
337
  #[derive(Debug, Clone, PartialEq, Eq)]
309
338
  pub struct AllowPeerTalkArgs {
@@ -179,7 +179,7 @@ fn front_matter_non_object_errors() {
179
179
  // separators=(",",":"))` with the workspace path templated to __WS__.
180
180
  // (team-agent-public v0.2.11, /tmp/probe_compiler.py.)
181
181
 
182
- const BASE_NOPROFILE_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks and report result_envelope_v1.","file":null},"tools":["fs_read","fs_write","execute_bash","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"none","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
182
+ const BASE_NOPROFILE_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks and report result_envelope_v1.","file":null},"tools":["fs_read","fs_write","execute_bash","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"adaptive","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
183
183
 
184
184
  #[test]
185
185
  fn compile_base_noprofile_matches_python_dict_order_and_values() {
@@ -352,7 +352,7 @@ tools:
352
352
  Bravo body.
353
353
  ";
354
354
 
355
- const TWO_AGENTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"alpha","role":"Alpha Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Alpha body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["alpha","Alpha Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}},{"id":"bravo","role":"Bravo Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Bravo body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["bravo","Bravo Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"alpha","rules":[{"id":"route-alpha","match":{"assignee":["alpha"]},"assign_to":"alpha","priority":10},{"id":"route-bravo","match":{"assignee":["bravo"]},"assign_to":"bravo","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"none","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":2,"startup_order":["alpha","bravo"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"alpha","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
355
+ const TWO_AGENTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"alpha","role":"Alpha Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Alpha body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["alpha","Alpha Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}},{"id":"bravo","role":"Bravo Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Bravo body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["bravo","Bravo Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"alpha","rules":[{"id":"route-alpha","match":{"assignee":["alpha"]},"assign_to":"alpha","priority":10},{"id":"route-bravo","match":{"assignee":["bravo"]},"assign_to":"bravo","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"adaptive","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":2,"startup_order":["alpha","bravo"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"alpha","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
356
356
 
357
357
  #[test]
358
358
  fn compile_two_agents_sorted_by_filename_with_routing_and_startup_order() {
@@ -247,7 +247,7 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
247
247
  "display_backend",
248
248
  Value::Str(
249
249
  string_field(&team_meta, "display_backend")
250
- .unwrap_or_else(|| "none".to_string()),
250
+ .unwrap_or_else(|| "adaptive".to_string()),
251
251
  ),
252
252
  ),
253
253
  ("session_name", Value::Str(session_name(&team_meta, &team_name))),
@@ -107,7 +107,10 @@ fn parse_rendered_header(line: &str) -> Option<RenderedBlock> {
107
107
  .split_once(" for ")
108
108
  .map(|(_, task)| task.trim().to_string())
109
109
  .filter(|task| !task.is_empty());
110
- Some(RenderedBlock { task_id, lines: Vec::new() })
110
+ Some(RenderedBlock {
111
+ task_id,
112
+ lines: Vec::new(),
113
+ })
111
114
  }
112
115
 
113
116
  fn parse_token(line: &str) -> Option<String> {
@@ -135,149 +138,13 @@ fn report_fake_result(
135
138
  .ok()
136
139
  .map(|value| value.trim().to_string())
137
140
  .filter(|value| !value.is_empty());
138
- mirror_fake_result_to_leader(workspace, owner_team.as_deref(), &envelope, source_content);
141
+ let _ = source_content;
139
142
  crate::messaging::report_result_for_owner_team(workspace, &envelope, owner_team.as_deref())
140
143
  .map_err(|e| FakeWorkerError::Report(e.to_string()))?;
141
- mirror_fake_result_to_leader(workspace, owner_team.as_deref(), &envelope, source_content);
142
144
  writeln!(output, "{}", serde_json::to_string(&envelope)?)?;
143
145
  Ok(())
144
146
  }
145
147
 
146
- fn mirror_fake_result_to_leader(
147
- workspace: &Path,
148
- owner_team: Option<&str>,
149
- envelope: &serde_json::Value,
150
- source_content: Option<&str>,
151
- ) {
152
- let Ok(raw_state) = crate::state::persist::load_runtime_state(workspace) else {
153
- return;
154
- };
155
- let state = owner_team
156
- .and_then(|team| crate::state::projection::resolve_owner_team_id(&raw_state, team).canonical_key().map(str::to_string))
157
- .map(|team| crate::state::projection::project_top_level_view(&raw_state, &team))
158
- .unwrap_or(raw_state);
159
- let attached = state
160
- .get("leader_receiver")
161
- .and_then(|receiver| receiver.get("status"))
162
- .and_then(serde_json::Value::as_str)
163
- == Some("attached");
164
- let pane_id = state
165
- .get("leader_receiver")
166
- .and_then(|receiver| receiver.get("pane_id"))
167
- .and_then(serde_json::Value::as_str)
168
- .filter(|pane| !pane.is_empty())
169
- .map(str::to_string)
170
- .or_else(|| owner_team.and_then(|team| claimed_pane_from_events(workspace, team)));
171
- let Some(pane_id) = pane_id.filter(|_| attached || owner_team.is_some()) else {
172
- return;
173
- };
174
- let summary = envelope
175
- .get("summary")
176
- .and_then(serde_json::Value::as_str)
177
- .unwrap_or("Fake worker completed");
178
- let result_id = envelope
179
- .get("result_id")
180
- .and_then(serde_json::Value::as_str)
181
- .unwrap_or("");
182
- let text = match source_content.filter(|content| !content.trim().is_empty()) {
183
- Some(content) => format!(
184
- "Fake worker result: {summary}\nMessage content: {}\nResult id: {result_id}",
185
- content.trim()
186
- ),
187
- None => format!("Fake worker result: {summary}\nResult id: {result_id}"),
188
- };
189
- let target = crate::transport::Target::Pane(crate::transport::PaneId::new(&pane_id));
190
- let payload = crate::transport::InjectPayload::Text(text.clone());
191
- let key = crate::transport::Key::Enter;
192
- if let Some(socket) = leader_receiver_full_socket(&state) {
193
- let endpoint_backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
194
- if crate::transport::Transport::inject(&endpoint_backend, &target, &payload, key, true).is_ok() {
195
- mirror_to_team_session_panes(&state, workspace, agent_id_from_envelope(envelope), &text);
196
- return;
197
- }
198
- }
199
- let workspace_backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
200
- let _ = crate::transport::Transport::inject(&workspace_backend, &target, &payload, key, true);
201
- mirror_to_team_session_panes(&state, workspace, agent_id_from_envelope(envelope), &text);
202
- }
203
-
204
- fn leader_receiver_full_socket(state: &serde_json::Value) -> Option<&str> {
205
- state
206
- .get("leader_receiver")
207
- .and_then(|receiver| receiver.get("tmux_socket"))
208
- .and_then(serde_json::Value::as_str)
209
- .filter(|socket| !socket.is_empty() && Path::new(socket).is_absolute())
210
- }
211
-
212
- fn claimed_pane_from_events(workspace: &Path, owner_team: &str) -> Option<String> {
213
- crate::event_log::EventLog::new(workspace)
214
- .tail(50)
215
- .ok()?
216
- .into_iter()
217
- .rev()
218
- .find_map(|event| {
219
- if event.get("event").and_then(serde_json::Value::as_str)
220
- != Some("leader_receiver.rebind_applied")
221
- {
222
- return None;
223
- }
224
- if event.get("team_id").and_then(serde_json::Value::as_str) != Some(owner_team) {
225
- return None;
226
- }
227
- event
228
- .get("new_pane_id")
229
- .and_then(serde_json::Value::as_str)
230
- .filter(|pane| !pane.is_empty())
231
- .map(str::to_string)
232
- })
233
- }
234
-
235
- fn agent_id_from_envelope(envelope: &serde_json::Value) -> Option<&str> {
236
- envelope.get("agent_id").and_then(serde_json::Value::as_str)
237
- }
238
-
239
- fn mirror_to_team_session_panes(
240
- state: &serde_json::Value,
241
- workspace: &Path,
242
- agent_id: Option<&str>,
243
- text: &str,
244
- ) {
245
- let Some(session) = state
246
- .get("session_name")
247
- .and_then(serde_json::Value::as_str)
248
- .filter(|session| !session.is_empty())
249
- else {
250
- return;
251
- };
252
- let worker_pane = agent_id.and_then(|agent_id| {
253
- state
254
- .get("agents")
255
- .and_then(|agents| agents.get(agent_id))
256
- .and_then(|agent| agent.get("pane_id"))
257
- .and_then(serde_json::Value::as_str)
258
- });
259
- if let Some(socket) = leader_receiver_full_socket(state) {
260
- let backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
261
- for pane in crate::transport::Transport::list_targets(&backend).unwrap_or_default() {
262
- if pane.session.as_str() != session || Some(pane.pane_id.as_str()) == worker_pane {
263
- continue;
264
- }
265
- let target = crate::transport::Target::Pane(pane.pane_id);
266
- let payload = crate::transport::InjectPayload::Text(text.to_string());
267
- let _ = crate::transport::Transport::inject(&backend, &target, &payload, crate::transport::Key::Enter, true);
268
- }
269
- }
270
- let backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
271
- for pane in crate::transport::Transport::list_targets(&backend).unwrap_or_default() {
272
- if pane.session.as_str() != session || Some(pane.pane_id.as_str()) == worker_pane {
273
- continue;
274
- }
275
- let target = crate::transport::Target::Pane(pane.pane_id);
276
- let payload = crate::transport::InjectPayload::Text(text.to_string());
277
- let _ = crate::transport::Transport::inject(&backend, &target, &payload, crate::transport::Key::Enter, true);
278
- }
279
- }
280
-
281
148
  fn fake_envelope(
282
149
  workspace: &Path,
283
150
  agent_id: &str,
@@ -331,7 +198,9 @@ mod tests {
331
198
  fn stored_result(ws: &std::path::Path) -> (String, String, String, serde_json::Value) {
332
199
  let store = MessageStore::open(ws).unwrap();
333
200
  let conn = open_db(store.db_path()).unwrap();
334
- let mut stmt = conn.prepare("select task_id, agent_id, status, envelope from results").unwrap();
201
+ let mut stmt = conn
202
+ .prepare("select task_id, agent_id, status, envelope from results")
203
+ .unwrap();
335
204
  let row = stmt
336
205
  .query_row([], |r| {
337
206
  Ok((
@@ -354,9 +223,18 @@ mod tests {
354
223
  ("t1", "w1", "success"),
355
224
  "stored result row must be task_id=t1 agent_id=w1 status=success"
356
225
  );
357
- assert_eq!(env["schema_version"], serde_json::json!("result_envelope_v1"));
358
- assert_eq!(env["summary"], serde_json::json!("Fake worker handled message m1"));
359
- assert_eq!(env["tests"], serde_json::json!([{"command": "fake-provider", "status": "passed"}]));
226
+ assert_eq!(
227
+ env["schema_version"],
228
+ serde_json::json!("result_envelope_v1")
229
+ );
230
+ assert_eq!(
231
+ env["summary"],
232
+ serde_json::json!("Fake worker handled message m1")
233
+ );
234
+ assert_eq!(
235
+ env["tests"],
236
+ serde_json::json!([{"command": "fake-provider", "status": "passed"}])
237
+ );
360
238
  let artifact_path = env["artifacts"][0]["path"].as_str().unwrap_or_default();
361
239
  assert!(
362
240
  artifact_path.ends_with(".team/logs/w1.scrollback"),
@@ -375,8 +253,14 @@ mod tests {
375
253
 
376
254
  assert_golden_envelope(&ws);
377
255
  let printed = String::from_utf8(out).unwrap();
378
- assert!(printed.contains("TEAM_AGENT_FAKE_READY agent=w1"), "must print the READY idle marker; got {printed}");
379
- assert!(printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"), "must print the WORKING marker on a non-empty line; got {printed}");
256
+ assert!(
257
+ printed.contains("TEAM_AGENT_FAKE_READY agent=w1"),
258
+ "must print the READY idle marker; got {printed}"
259
+ );
260
+ assert!(
261
+ printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"),
262
+ "must print the WORKING marker on a non-empty line; got {printed}"
263
+ );
380
264
  }
381
265
 
382
266
  // Rendered-message form: a `Team Agent message from leader for t1:` block ending `[team-agent-token:m1]`
@@ -391,6 +275,9 @@ mod tests {
391
275
 
392
276
  assert_golden_envelope(&ws);
393
277
  let printed = String::from_utf8(out).unwrap();
394
- assert!(printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"), "rendered form prints WORKING per line; got {printed}");
278
+ assert!(
279
+ printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"),
280
+ "rendered form prints WORKING per line; got {printed}"
281
+ );
395
282
  }
396
283
  }
@@ -11,15 +11,15 @@ use super::*;
11
11
  // ── lifecycle::display —— 能力门 / 后端解析 / 开关 / rebind 后重建 ────────────
12
12
 
13
13
  /// `resolve_display_backend(requested, recorded, source)`(`display/backend.py`)。默认
14
- /// none;非默认非静默发 `display.backend_resolved`。
14
+ /// adaptive;显式 none 是逃生口。
15
15
  pub fn resolve_display_backend(
16
16
  requested: Option<DisplayBackend>,
17
17
  recorded: Option<DisplayBackend>,
18
18
  ) -> ResolvedBackend {
19
- let backend = requested.or(recorded).unwrap_or(DisplayBackend::None);
19
+ let backend = requested.or(recorded).unwrap_or(DisplayBackend::Adaptive);
20
20
  ResolvedBackend {
21
21
  backend,
22
- non_default: backend != DisplayBackend::None,
22
+ non_default: backend != DisplayBackend::Adaptive,
23
23
  }
24
24
  }
25
25