@team-agent/installer 0.5.77 → 0.5.80

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 (95) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +161 -29
  4. package/crates/team-agent/src/cli/diagnose.rs +333 -68
  5. package/crates/team-agent/src/cli/emit.rs +24 -8
  6. package/crates/team-agent/src/cli/mod.rs +543 -98
  7. package/crates/team-agent/src/cli/models.rs +393 -32
  8. package/crates/team-agent/src/cli/named_address.rs +19 -5
  9. package/crates/team-agent/src/cli/profile.rs +35 -4
  10. package/crates/team-agent/src/cli/spec.rs +7 -2
  11. package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
  12. package/crates/team-agent/src/cli/status_port/runtime.rs +3 -1
  13. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  14. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  15. package/crates/team-agent/src/cli/status_port/tests.rs +37 -1
  16. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  17. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  18. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  19. package/crates/team-agent/src/cli/types.rs +1 -0
  20. package/crates/team-agent/src/coordinator/steps/abnormal.rs +122 -2
  21. package/crates/team-agent/src/leader/lease.rs +492 -33
  22. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  23. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  24. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  25. package/crates/team-agent/src/leader/start.rs +197 -88
  26. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  27. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  28. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  29. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  30. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  31. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +86 -23
  32. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  33. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  34. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  35. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  36. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  37. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  38. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  39. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  40. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  41. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
  42. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  43. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  44. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  45. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  46. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  47. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
  48. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  49. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  50. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  51. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  52. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  53. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  54. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -0
  55. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  56. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  57. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  58. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  59. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  60. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
  61. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  62. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  63. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  64. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
  65. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +71 -0
  66. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  67. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  68. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  69. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  70. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  71. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  72. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  73. package/crates/team-agent/src/messaging/delivery.rs +221 -80
  74. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  75. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -0
  76. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  77. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  78. package/crates/team-agent/src/provider/session_scan/common/tests.rs +204 -0
  79. package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
  80. package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
  81. package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
  82. package/crates/team-agent/src/state/mod.rs +1 -0
  83. package/crates/team-agent/src/state/persist.rs +102 -4
  84. package/crates/team-agent/src/state/projection.rs +7 -1
  85. package/crates/team-agent/src/state/repository.rs +29 -0
  86. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
  87. package/crates/team-agent/src/tmux_backend/tests.rs +149 -4
  88. package/crates/team-agent/src/tmux_backend.rs +239 -127
  89. package/crates/team-agent/src/transport.rs +78 -0
  90. package/crates/team-agent/src/transport_factory.rs +53 -0
  91. package/package.json +4 -5
  92. package/skills/team-agent/SKILL.md +45 -17
  93. package/skills/team-agent/command-coverage.json +4 -29
  94. package/npm/check-skill-links.mjs +0 -88
  95. package/skills/team-agent/references/team-agent-operator.md +0 -672
@@ -333,7 +333,7 @@ pub(crate) fn start_agent_at_paths(
333
333
  )));
334
334
  }
335
335
  let spawn_session_id = if matches!(start_mode, StartMode::Resumed) {
336
- session_id.as_ref()
336
+ session_id.clone()
337
337
  } else {
338
338
  None
339
339
  };
@@ -375,7 +375,7 @@ pub(crate) fn start_agent_at_paths(
375
375
  &session_name,
376
376
  agent_id,
377
377
  &agent,
378
- spawn_session_id,
378
+ spawn_session_id.as_ref(),
379
379
  into_existing_session,
380
380
  transport,
381
381
  Some(&safety),
@@ -390,106 +390,95 @@ pub(crate) fn start_agent_at_paths(
390
390
  &session_name,
391
391
  &spawn.spawn.window,
392
392
  ) {
393
- if let Err(rollback_error) = transport.kill_pane(&spawn.spawn.pane_id) {
394
- return Err(LifecycleError::RequirementUnmet(format!(
395
- "{error}; failed to roll back spawned pane {}: {rollback_error}",
396
- spawn.spawn.pane_id.as_str()
397
- )));
398
- }
399
- return Err(error);
393
+ return Err(rollback_spawned_pane_keep_error(
394
+ transport,
395
+ &spawn.spawn.pane_id,
396
+ error,
397
+ ));
400
398
  }
401
399
  let actual_spawn_window = spawn.spawn.window.as_str().to_string();
402
- let post_spawn = (|| -> Result<bool, LifecycleError> {
403
- mark_agent_started(
404
- &mut state,
405
- agent_id,
406
- &actual_spawn_window,
407
- &spawn,
408
- transport,
409
- &safety,
410
- start_mode,
411
- )?;
412
- // 0.5.66 bypass 单源 §2.7:启动即带 bypass → 审计留痕。
413
- if safety.enabled {
414
- let state_agent = state.get("agents").and_then(|v| v.get(agent_id.as_str()));
415
- let _ = crate::event_log::EventLog::new(workspace).write(
416
- "worker.spawn_dangerously_skip_permissions",
417
- serde_json::json!({
418
- "agent_id": agent_id.as_str(),
419
- "role": state_agent
420
- .and_then(|a| a.get("role"))
421
- .and_then(serde_json::Value::as_str)
422
- .unwrap_or_default(),
423
- "provider": state_agent
424
- .and_then(|a| a.get("provider"))
425
- .and_then(serde_json::Value::as_str)
426
- .unwrap_or_default(),
427
- "trigger": "start-agent",
428
- "spec_source_path": crate::lifecycle::launch::agent_id_spec_source_path(
429
- workspace, agent_id.as_str(),
430
- ),
431
- }),
432
- );
433
- }
434
- // **0.3.24 add-agent socket drift fix**: keep `state.tmux_endpoint` /
435
- // `state.tmux_socket` synchronized with the transport actually used for the
436
- // spawn. Without this, add-agent / fork-agent could spawn to a socket that
437
- // never gets persisted, and the next coordinator tick would re-resolve to
438
- // the workspace-hash socket and lose the new pane. Annotation runs inside
439
- // the same `save_restart_projected_state` window — no parallel "annotate
440
- // after spawn" race with coordinator and no double source of truth.
441
- crate::lifecycle::launch::annotate_runtime_tmux_endpoint(&mut state, transport, workspace);
442
- let team_key = restart_projection_team_key(&state, team);
443
- // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
444
- // clear the matching `agent_health` observation on the new spawn cohort
445
- // so five-line status summary and status --json do not surface a stale
446
- // WORKING row from the pre-shutdown process. Best-effort: DB failure
447
- // must not fail the start-agent path.
448
- let _ = crate::db::agent_health_capture::clear_agent_health_observation(
449
- workspace, &team_key, agent_id,
400
+ let spawned = (|| {
401
+ mark_agent_started(
402
+ &mut state,
403
+ agent_id,
404
+ &actual_spawn_window,
405
+ &spawn,
406
+ transport,
407
+ &safety,
408
+ start_mode,
409
+ )?;
410
+ // 0.5.66 bypass 单源 §2.7:启动即带 bypass → 审计留痕。
411
+ if safety.enabled {
412
+ let state_agent = state
413
+ .get("agents")
414
+ .and_then(|v| v.get(agent_id.as_str()));
415
+ let _ = crate::event_log::EventLog::new(workspace).write(
416
+ "worker.spawn_dangerously_skip_permissions",
417
+ serde_json::json!({
418
+ "agent_id": agent_id.as_str(),
419
+ "role": state_agent
420
+ .and_then(|a| a.get("role"))
421
+ .and_then(serde_json::Value::as_str)
422
+ .unwrap_or_default(),
423
+ "provider": state_agent
424
+ .and_then(|a| a.get("provider"))
425
+ .and_then(serde_json::Value::as_str)
426
+ .unwrap_or_default(),
427
+ "trigger": "start-agent",
428
+ "spec_source_path": crate::lifecycle::launch::agent_id_spec_source_path(
429
+ workspace, agent_id.as_str(),
430
+ ),
431
+ }),
450
432
  );
451
- let skip_capture_backfill = if matches!(
452
- start_mode,
453
- StartMode::Fresh | StartMode::FreshAfterMissingRollout
454
- ) {
455
- vec![agent_id.as_str()]
456
- } else {
457
- Vec::new()
458
- };
459
- save_restart_projected_state_with_capture_backfill_skip(
460
- workspace,
461
- &mut state,
462
- &team_key,
463
- &skip_capture_backfill,
464
- &[agent_id.as_str()],
465
- )?;
466
- write_start_agent_start_event(
467
- workspace,
468
- agent_id,
469
- &agent,
470
- provider,
471
- &spawn.plan,
472
- start_mode,
473
- &session_name,
474
- &actual_spawn_window,
475
- spawn_session_id,
476
- tmux_start_mode_for_spawn(&spawn, into_existing_session),
477
- )?;
478
- replay_worker_target_missing_messages(workspace, agent_id, &team_key, &state, transport)?;
479
- Ok(start_coordinator_for_workspace(workspace, Some(&team_key))?.ok)
480
- })();
481
- let coordinator_started = match post_spawn {
482
- Ok(started) => started,
483
- Err(error) => {
484
- if let Err(rollback_error) = transport.kill_pane(&spawn.spawn.pane_id) {
485
- return Err(LifecycleError::RequirementUnmet(format!(
486
- "{error}; failed to roll back spawned pane {}: {rollback_error}",
487
- spawn.spawn.pane_id.as_str()
488
- )));
489
- }
490
- return Err(error);
491
- }
433
+ }
434
+ // **0.3.24 add-agent socket drift fix**: keep `state.tmux_endpoint` /
435
+ // `state.tmux_socket` synchronized with the transport actually used for the
436
+ // spawn. Without this, add-agent / fork-agent could spawn to a socket that
437
+ // never gets persisted, and the next coordinator tick would re-resolve to
438
+ // the workspace-hash socket and lose the new pane. Annotation runs inside
439
+ // the same `save_restart_projected_state` window — no parallel "annotate
440
+ // after spawn" race with coordinator and no double source of truth.
441
+ crate::lifecycle::launch::annotate_runtime_tmux_endpoint(&mut state, transport, workspace);
442
+ let team_key = restart_projection_team_key(&state, team);
443
+ // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
444
+ // clear the matching `agent_health` observation on the new spawn cohort
445
+ // so five-line status summary and status --json do not surface a stale
446
+ // WORKING row from the pre-shutdown process. Best-effort: DB failure
447
+ // must not fail the start-agent path.
448
+ let _ = crate::db::agent_health_capture::clear_agent_health_observation(
449
+ workspace, &team_key, agent_id,
450
+ );
451
+ let skip_capture_backfill = if matches!(
452
+ start_mode,
453
+ StartMode::Fresh | StartMode::FreshAfterMissingRollout
454
+ ) {
455
+ vec![agent_id.as_str()]
456
+ } else {
457
+ Vec::new()
492
458
  };
459
+ persist_selected_receiver_session_name(&mut state, &session_name);
460
+ save_restart_projected_state_with_capture_backfill_skip(
461
+ workspace,
462
+ &mut state,
463
+ &team_key,
464
+ &skip_capture_backfill,
465
+ &[agent_id.as_str()],
466
+ )?;
467
+ write_start_agent_start_event(
468
+ workspace,
469
+ agent_id,
470
+ &agent,
471
+ &spawn.plan,
472
+ provider,
473
+ start_mode,
474
+ &session_name,
475
+ &actual_spawn_window,
476
+ spawn_session_id.as_ref(),
477
+ tmux_start_mode_for_spawn(&spawn, into_existing_session),
478
+ )?;
479
+ replay_worker_target_missing_messages(workspace, agent_id, &team_key, &state, transport)?;
480
+ let coordinator = start_coordinator_for_workspace(workspace, Some(&team_key))?;
481
+ let coordinator_started = coordinator.ok;
493
482
  Ok(StartAgentOutcome::Running {
494
483
  env: AgentActionEnvelope {
495
484
  agent_id: agent_id.clone(),
@@ -502,6 +491,29 @@ pub(crate) fn start_agent_at_paths(
502
491
  new_session_id: spawn.plan.expected_session_id.clone(),
503
492
  rollout_path,
504
493
  })
494
+ })();
495
+ match spawned {
496
+ Ok(outcome) => Ok(outcome),
497
+ Err(error) => Err(rollback_spawned_pane_keep_error(
498
+ transport,
499
+ &spawn.spawn.pane_id,
500
+ error,
501
+ )),
502
+ }
503
+ }
504
+
505
+ fn rollback_spawned_pane_keep_error(
506
+ transport: &dyn crate::transport::Transport,
507
+ pane: &crate::transport::PaneId,
508
+ error: LifecycleError,
509
+ ) -> LifecycleError {
510
+ match transport.kill_pane(pane) {
511
+ Ok(()) => error,
512
+ Err(rollback_error) => LifecycleError::Transport(format!(
513
+ "{error}; failed to roll back spawned pane {}: {rollback_error}",
514
+ pane.as_str()
515
+ )),
516
+ }
505
517
  }
506
518
 
507
519
  fn replay_worker_target_missing_messages(
@@ -1783,8 +1795,8 @@ fn write_start_agent_start_event(
1783
1795
  workspace: &Path,
1784
1796
  agent_id: &AgentId,
1785
1797
  agent: &serde_json::Value,
1798
+ spawn_plan: &crate::provider::CommandPlan,
1786
1799
  provider: crate::provider::Provider,
1787
- plan: &crate::provider::CommandPlan,
1788
1800
  start_mode: StartMode,
1789
1801
  session_name: &SessionName,
1790
1802
  window: &str,
@@ -1802,7 +1814,7 @@ fn write_start_agent_start_event(
1802
1814
  "session": session_name.as_str(),
1803
1815
  "window": window,
1804
1816
  "tmux_start_mode": tmux_start_mode,
1805
- "command": plan.argv,
1817
+ "command": spawn_plan.argv,
1806
1818
  "mcp_config": agent.get("mcp_config").cloned().unwrap_or(serde_json::Value::Null),
1807
1819
  }),
1808
1820
  )
@@ -37,7 +37,6 @@
37
37
  //! maturity: wired
38
38
  //! ---
39
39
  use super::*;
40
- use crate::transport::Transport;
41
40
 
42
41
  pub(super) struct SpawnedAgentWindow {
43
42
  pub spawn: crate::transport::SpawnResult,
@@ -341,7 +340,7 @@ pub(super) fn spawn_agent_window(
341
340
  agent_id.as_str(),
342
341
  team_id.as_deref().unwrap_or(""),
343
342
  );
344
- let mcp_config_path = if provider == Provider::Pi {
343
+ let mcp_config_path = if provider == crate::provider::Provider::Pi {
345
344
  None
346
345
  } else {
347
346
  Some(crate::lifecycle::launch::write_worker_mcp_config(
@@ -379,19 +378,19 @@ pub(super) fn spawn_agent_window(
379
378
  agent_id_hint: Some(agent_id.as_str()),
380
379
  effort: restart_effort,
381
380
  };
382
- let pi_spawn_cwd = (provider == Provider::Pi)
381
+ let pi_spawn_cwd = (provider == crate::provider::Provider::Pi)
383
382
  .then(|| {
384
383
  agent
385
384
  .get("spawn_cwd")
386
385
  .and_then(serde_json::Value::as_str)
387
386
  .filter(|value| !value.is_empty())
388
- .map(PathBuf::from)
387
+ .map(std::path::PathBuf::from)
389
388
  })
390
389
  .flatten();
391
390
  let spawn_cwd = spawn_cwd_override
392
391
  .or(pi_spawn_cwd.as_deref())
393
392
  .unwrap_or(workspace);
394
- let mut plan = if provider == Provider::Pi {
393
+ let mut plan = if provider == crate::provider::Provider::Pi {
395
394
  let request = crate::lifecycle::launch::pi_mcp::PiMaterializeRequest {
396
395
  workspace,
397
396
  team_id: team_id.as_deref().unwrap_or(""),
@@ -477,41 +476,33 @@ pub(super) fn spawn_agent_window(
477
476
  agent_id.as_str(),
478
477
  &system_prompt,
479
478
  )?;
480
- crate::lifecycle::launch::apply_cursor_mcp_overlay(workspace, &mcp_config)?;
481
- let project = if crate::lifecycle::launch::cursor_mcp_isolation_enabled() {
482
- Some(crate::lifecycle::launch::cursor_mcp_project_dir(
483
- workspace,
484
- agent_id.as_str(),
485
- )?)
486
- } else {
487
- None
488
- };
489
- crate::lifecycle::launch::enable_cursor_workspace_mcp(workspace, project.as_deref())?;
479
+ let project = crate::lifecycle::launch::prepare_cursor_seat_mcp(
480
+ workspace,
481
+ agent_id.as_str(),
482
+ &mcp_config,
483
+ )?;
484
+ crate::lifecycle::launch::enable_cursor_workspace_mcp_with_profile(
485
+ workspace,
486
+ project.as_deref(),
487
+ Some(&profile_launch),
488
+ )?;
490
489
  crate::lifecycle::launch::apply_cursor_spawn_workspace_pointers(
491
490
  &mut plan.argv,
492
491
  workspace,
493
492
  agent_id.as_str(),
494
493
  )?;
495
- crate::lifecycle::launch::apply_cursor_subscription_proxy_env(&mut env);
494
+ crate::lifecycle::launch::apply_cursor_subscription_proxy_env_with_profile(
495
+ &mut env,
496
+ &profile_launch,
497
+ );
496
498
  }
497
499
  if provider == crate::provider::Provider::Grok {
498
500
  crate::lifecycle::launch::ensure_grok_login_and_folder_trust(workspace)?;
499
501
  crate::lifecycle::launch::apply_grok_mcp_overlay(workspace, &mcp_config)?;
500
502
  }
501
- // 0.3.28 Step 3: per Python parity, worker spawn cwd is ALWAYS `workspace`.
502
- // The persisted-state `agent.spawn_cwd` override is ignored (it was a
503
- // Rust-only extension that drifted to `.team/runtime/<team_key>/` after
504
- // rebuild.rs:138 — root cause of E56). The `spawn_cwd_override` parameter
505
- // is still honoured for callers that need an explicit cwd (e.g. spec
506
- // YAML-resolved cwd at first launch in `lifecycle/launch.rs`), but
507
- // restart never passes it (see commit 71864c0 which fixed rebuild.rs:297
508
- // to stop pinning `.team/runtime/<team_key>/`).
509
- //
510
- // NOTE: Step 4 will thread the YAML spec down to here so we can honour
511
- // a per-agent YAML `spawn_cwd` field if one is set. Until then, override
512
- // > workspace; state-based override is silently dropped for existing
513
- // providers. Pi is the narrow exception because exact header/path resume
514
- // must preserve the captured cwd or refuse.
503
+ // Non-Pi spawn cwd is override-or-workspace (E56: ignore drifted state
504
+ // `.team/runtime/<team_key>/`). Pi resume uses captured agent.spawn_cwd
505
+ // already resolved above so exact-path header checks match capture.
515
506
  // 0.4.x provider effort MVP step 9: scrub CLAUDE_EFFORT for Claude
516
507
  // worker spawn so a parent shell env cannot silently override the
517
508
  // framework's effort decision.
@@ -1158,34 +1149,19 @@ fn json_team_identity_matches(existing: &serde_json::Value, compact: &serde_json
1158
1149
  /// returns: 非空的 session_name,缺失时退到默认名
1159
1150
  /// ---
1160
1151
  pub(super) fn state_session_name(state: &serde_json::Value) -> SessionName {
1161
- worker_session_name_from_state(state)
1162
- .map(SessionName::new)
1163
- .unwrap_or_else(|| SessionName::new("team-agent"))
1164
- }
1165
-
1166
- fn worker_session_name_from_state(state: &serde_json::Value) -> Option<&str> {
1167
1152
  state
1168
1153
  .get("session_name")
1169
1154
  .and_then(|v| v.as_str())
1170
1155
  .filter(|s| !s.is_empty())
1171
1156
  .or_else(|| {
1172
- let team_key = state
1173
- .get("active_team_key")
1174
- .and_then(serde_json::Value::as_str)
1175
- .filter(|team_key| !team_key.is_empty())?;
1176
1157
  state
1177
- .get("teams")?
1178
- .get(team_key)?
1179
- .pointer("/leader_receiver/session_name")?
1180
- .as_str()
1181
- .filter(|session| !session.is_empty())
1182
- })
1183
- .or_else(|| {
1184
- state
1185
- .pointer("/leader_receiver/session_name")
1186
- .and_then(serde_json::Value::as_str)
1187
- .filter(|session| !session.is_empty())
1158
+ .get("leader_receiver")
1159
+ .and_then(|receiver| receiver.get("session_name"))
1160
+ .and_then(|v| v.as_str())
1161
+ .filter(|s| !s.is_empty())
1188
1162
  })
1163
+ .map(SessionName::new)
1164
+ .unwrap_or_else(|| SessionName::new("team-agent"))
1189
1165
  }
1190
1166
 
1191
1167
  /// ---
@@ -1193,7 +1169,40 @@ fn worker_session_name_from_state(state: &serde_json::Value) -> Option<&str> {
1193
1169
  /// returns: 记了则 true
1194
1170
  /// ---
1195
1171
  pub(super) fn session_name_present(state: &serde_json::Value) -> bool {
1196
- worker_session_name_from_state(state).is_some()
1172
+ state
1173
+ .get("session_name")
1174
+ .and_then(|v| v.as_str())
1175
+ .map(|s| !s.is_empty())
1176
+ .unwrap_or(false)
1177
+ }
1178
+
1179
+ /// Persist the session actually used for spawn when the selected team only
1180
+ /// had it on `leader_receiver`. Send/status route on `session_name`; an
1181
+ /// explicit null must stay a miss and must not rewrite a sibling team.
1182
+ pub(super) fn persist_selected_receiver_session_name(
1183
+ state: &mut serde_json::Value,
1184
+ session: &SessionName,
1185
+ ) {
1186
+ if session_name_present(state) {
1187
+ return;
1188
+ }
1189
+ let Some(from_receiver) = state
1190
+ .get("leader_receiver")
1191
+ .and_then(|receiver| receiver.get("session_name"))
1192
+ .and_then(serde_json::Value::as_str)
1193
+ .filter(|name| !name.is_empty())
1194
+ else {
1195
+ return;
1196
+ };
1197
+ if from_receiver != session.as_str() {
1198
+ return;
1199
+ }
1200
+ if let Some(object) = state.as_object_mut() {
1201
+ object.insert(
1202
+ "session_name".to_string(),
1203
+ serde_json::json!(session.as_str()),
1204
+ );
1205
+ }
1197
1206
  }
1198
1207
 
1199
1208
  /// ---
@@ -2291,6 +2300,27 @@ mod e36_transcript_backing_tests {
2291
2300
  }
2292
2301
  }
2293
2302
 
2303
+ #[test]
2304
+ fn agent_rollout_path_keeps_captured_exact_session_file() {
2305
+ // Live T1 public session.captured (events-before.jsonl / events-after-t1.jsonl):
2306
+ // rollout_path was already assigned before shutdown; resume must not
2307
+ // invent a different file or drop the captured path.
2308
+ let captured = "/Volumes/nvme/tmp/team-agent-live-0578-r2-3087abc5/.team/runtime/pi/claim-live-0578-r2/echo/sessions/2026-09-09T15-53-03-460Z_0dac7250-aa88-45be-aa3e-03a6833f9552.jsonl";
2309
+ let agent = serde_json::json!({
2310
+ "session_id": "0dac7250-aa88-45be-aa3e-03a6833f9552",
2311
+ "rollout_path": captured,
2312
+ });
2313
+ let path = agent_rollout_path(&agent).expect("captured exact path");
2314
+ assert_eq!(path.as_path().to_string_lossy(), captured);
2315
+ assert!(
2316
+ agent_rollout_path(&serde_json::json!({
2317
+ "session_id": "0dac7250-aa88-45be-aa3e-03a6833f9552",
2318
+ }))
2319
+ .is_none(),
2320
+ "session_id alone is not an exact session path"
2321
+ );
2322
+ }
2323
+
2294
2324
  #[test]
2295
2325
  fn convergence_event_uses_central_scrub_and_preserves_required_failure() {
2296
2326
  let scratch = ScratchDir::new("event-log-central");