diff --git a/crates/tor-chanmgr/src/lib.rs b/crates/tor-chanmgr/src/lib.rs
index 92a3ef500462b3dd101a99db184aea45651e2b03..12f3645fd1b22ccd647f8009146003ae2a19570d 100644
--- a/crates/tor-chanmgr/src/lib.rs
+++ b/crates/tor-chanmgr/src/lib.rs
@@ -82,6 +82,17 @@ pub struct ChanMgr<R: Runtime> {
     bootstrap_status: event::ConnStatusEvents,
 }
 
+/// Description of how we got a channel.
+#[non_exhaustive]
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub enum ChanProvenance {
+    /// This channel was newly launched, or was in progress and finished while
+    /// we were waiting.
+    NewlyCreated,
+    /// This channel already existed when we asked for it.
+    Preexisting,
+}
+
 impl<R: Runtime> ChanMgr<R> {
     /// Construct a new channel manager.
     ///
@@ -118,16 +129,19 @@ impl<R: Runtime> ChanMgr<R> {
     /// If there is already a channel launch attempt in progress, this
     /// function will wait until that launch is complete, and succeed
     /// or fail depending on its outcome.
-    pub async fn get_or_launch<T: ChanTarget + ?Sized>(&self, target: &T) -> Result<Channel> {
+    pub async fn get_or_launch<T: ChanTarget + ?Sized>(
+        &self,
+        target: &T,
+    ) -> Result<(Channel, ChanProvenance)> {
         let ed_identity = target.ed_identity();
         let targetinfo = OwnedChanTarget::from_chan_target(target);
 
-        let chan = self.mgr.get_or_launch(*ed_identity, targetinfo).await?;
+        let (chan, provenance) = self.mgr.get_or_launch(*ed_identity, targetinfo).await?;
         // Double-check the match to make sure that the RSA identity is
         // what we wanted too.
         chan.check_match(target)
             .map_err(Error::from_proto_no_skew)?;
-        Ok(chan)
+        Ok((chan, provenance))
     }
 
     /// Return a stream of [`ConnStatus`] events to tell us about changes
diff --git a/crates/tor-chanmgr/src/mgr.rs b/crates/tor-chanmgr/src/mgr.rs
index 89bccc450a580689fe722355a672ccbecd0a22fe..4fe39f1ccdf838b733fc2fce9d6dc5fde44a987b 100644
--- a/crates/tor-chanmgr/src/mgr.rs
+++ b/crates/tor-chanmgr/src/mgr.rs
@@ -1,7 +1,7 @@
 //! Abstract implementation of a channel manager
 
 use crate::mgr::map::OpenEntry;
-use crate::{Error, Result};
+use crate::{ChanProvenance, Error, Result};
 
 use async_trait::async_trait;
 use futures::channel::oneshot;
@@ -110,7 +110,7 @@ impl<CF: ChannelFactory> AbstractChanMgr<CF> {
         &self,
         ident: <<CF as ChannelFactory>::Channel as AbstractChannel>::Ident,
         target: CF::BuildSpec,
-    ) -> Result<CF::Channel> {
+    ) -> Result<(CF::Channel, ChanProvenance)> {
         use map::ChannelState::*;
 
         /// Possible actions that we'll decide to take based on the
@@ -123,7 +123,7 @@ impl<CF: ChannelFactory> AbstractChanMgr<CF> {
             /// We're going to wait for it to finish.
             Wait(Pending<C>),
             /// We found a usable channel.  We're going to return it.
-            Return(Result<C>),
+            Return(Result<(C, ChanProvenance)>),
         }
         /// How many times do we try?
         const N_ATTEMPTS: usize = 2;
@@ -140,7 +140,10 @@ impl<CF: ChannelFactory> AbstractChanMgr<CF> {
                     Some(Open(ref ent)) => {
                         if ent.channel.is_usable() {
                             // Good channel. Return it.
-                            let action = Action::Return(Ok(ent.channel.clone()));
+                            let action = Action::Return(Ok((
+                                ent.channel.clone(),
+                                ChanProvenance::Preexisting,
+                            )));
                             (oldstate, action)
                         } else {
                             // Unusable channel.  Move to the Building
@@ -181,7 +184,7 @@ impl<CF: ChannelFactory> AbstractChanMgr<CF> {
                 }
                 // There's an in-progress channel.  Wait for it.
                 Action::Wait(pend) => match pend.await {
-                    Ok(Ok(chan)) => return Ok(chan),
+                    Ok(Ok(chan)) => return Ok((chan, ChanProvenance::NewlyCreated)),
                     Ok(Err(e)) => {
                         last_err = Some(e);
                     }
@@ -207,7 +210,7 @@ impl<CF: ChannelFactory> AbstractChanMgr<CF> {
                         // It's okay if all the receivers went away:
                         // that means that nobody was waiting for this channel.
                         let _ignore_err = send.send(Ok(chan.clone()));
-                        return Ok(chan);
+                        return Ok((chan, ChanProvenance::NewlyCreated));
                     }
                     Err(e) => {
                         // The channel failed. Make it non-pending, tell the
@@ -340,8 +343,8 @@ mod test {
             let cf = FakeChannelFactory::new(runtime);
             let mgr = AbstractChanMgr::new(cf);
             let target = (413, '!');
-            let chan1 = mgr.get_or_launch(413, target).await.unwrap();
-            let chan2 = mgr.get_or_launch(413, target).await.unwrap();
+            let chan1 = mgr.get_or_launch(413, target).await.unwrap().0;
+            let chan2 = mgr.get_or_launch(413, target).await.unwrap().0;
 
             assert_eq!(chan1, chan2);
 
@@ -411,14 +414,14 @@ mod test {
                 mgr.get_or_launch(5, (5, 'a')),
             );
 
-            let ch3 = ch3.unwrap();
+            let ch3 = ch3.unwrap().0;
             let _ch4 = ch4.unwrap();
-            let ch5 = ch5.unwrap();
+            let ch5 = ch5.unwrap().0;
 
             ch3.start_closing();
             ch5.start_closing();
 
-            let ch3_new = mgr.get_or_launch(3, (3, 'b')).await.unwrap();
+            let ch3_new = mgr.get_or_launch(3, (3, 'b')).await.unwrap().0;
             assert_ne!(ch3, ch3_new);
             assert_eq!(ch3_new.mood, 'b');
 
diff --git a/crates/tor-circmgr/src/build.rs b/crates/tor-circmgr/src/build.rs
index c0e30ff364c8c3df07e257b609377311b1712c55..0d1269f20e366568120c7a40f3402e7f8c99ac95 100644
--- a/crates/tor-circmgr/src/build.rs
+++ b/crates/tor-circmgr/src/build.rs
@@ -73,13 +73,14 @@ async fn create_common<RT: Runtime, CT: ChanTarget>(
     rt: &RT,
     target: &CT,
 ) -> Result<PendingClientCirc> {
-    let chan = chanmgr
-        .get_or_launch(target)
-        .await
-        .map_err(|cause| Error::Channel {
-            peer: OwnedChanTarget::from_chan_target(target),
-            cause,
-        })?;
+    let (chan, _provenance) =
+        chanmgr
+            .get_or_launch(target)
+            .await
+            .map_err(|cause| Error::Channel {
+                peer: OwnedChanTarget::from_chan_target(target),
+                cause,
+            })?;
     let (pending_circ, reactor) = chan.new_circ().await.map_err(|error| Error::Protocol {
         error,
         peer: None, // we don't blame the peer, because new_circ() does no networking.
diff --git a/doc/semver_status.md b/doc/semver_status.md
index d5bdfa75bedd97bd04a3d2112c77264621d34f35..93848310e15e4b40be8c9968a24e6c16106d6541 100644
--- a/doc/semver_status.md
+++ b/doc/semver_status.md
@@ -25,6 +25,7 @@ MODIFIED: Added `reset()` method to RetrySchedule.
 ### tor-chanmgr
 
 BREAKING: Added members to `Error::Proto`
+BREAKING: Added `ChanProvenance` to `ChanMgr::get_or_launch`.
 
 ### tor-circmgr