Index: /trunk/db.go
===================================================================
--- /trunk/db.go	(revision 283)
+++ /trunk/db.go	(revision 284)
@@ -44,7 +44,8 @@
 
 type Channel struct {
-	ID   int64
-	Name string
-	Key  string
+	ID       int64
+	Name     string
+	Key      string
+	Detached bool
 }
 
@@ -77,4 +78,5 @@
 	name VARCHAR(255) NOT NULL,
 	key VARCHAR(255),
+	detached INTEGER NOT NULL DEFAULT 0,
 	FOREIGN KEY(network) REFERENCES Network(id),
 	UNIQUE(network, name)
@@ -85,4 +87,5 @@
 	"", // migration #0 is reserved for schema initialization
 	"ALTER TABLE Network ADD COLUMN connect_commands VARCHAR(1023)",
+	"ALTER TABLE Channel ADD COLUMN detached INTEGER NOT NULL DEFAULT 0",
 }
 
@@ -347,5 +350,7 @@
 	defer db.lock.RUnlock()
 
-	rows, err := db.db.Query("SELECT id, name, key FROM Channel WHERE network = ?", networkID)
+	rows, err := db.db.Query(`SELECT id, name, key, detached
+		FROM Channel
+		WHERE network = ?`, networkID)
 	if err != nil {
 		return nil, err
@@ -357,5 +362,5 @@
 		var ch Channel
 		var key *string
-		if err := rows.Scan(&ch.ID, &ch.Name, &key); err != nil {
+		if err := rows.Scan(&ch.ID, &ch.Name, &key, &ch.Detached); err != nil {
 			return nil, err
 		}
@@ -379,10 +384,12 @@
 	if ch.ID != 0 {
 		_, err = db.db.Exec(`UPDATE Channel
-			SET network = ?, name = ?, key = ?
-			WHERE id = ?`, networkID, ch.Name, key, ch.ID)
+			SET network = ?, name = ?, key = ?, detached = ?
+			WHERE id = ?`,
+			networkID, ch.Name, key, ch.Detached, ch.ID)
 	} else {
 		var res sql.Result
-		res, err = db.db.Exec(`INSERT INTO Channel(network, name, key)
-			VALUES (?, ?, ?)`, networkID, ch.Name, key)
+		res, err = db.db.Exec(`INSERT INTO Channel(network, name, key, detached)
+			VALUES (?, ?, ?, ?)`,
+			networkID, ch.Name, key, ch.Detached)
 		if err != nil {
 			return err
Index: /trunk/doc/soju.1.scd
===================================================================
--- /trunk/doc/soju.1.scd	(revision 283)
+++ /trunk/doc/soju.1.scd	(revision 284)
@@ -24,5 +24,6 @@
 the next connection. When registering or authenticating with NickServ, the
 credentials will be saved and automatically used on the next connection if the
-server supports SASL.
+server supports SASL. When parting a channel with the reason "detach", the
+channel will be detached instead of being left.
 
 When all clients are disconnected from the bouncer, the user is automatically
Index: /trunk/downstream.go
===================================================================
--- /trunk/downstream.go	(revision 283)
+++ /trunk/downstream.go	(revision 284)
@@ -768,13 +768,18 @@
 	dc.forEachUpstream(func(uc *upstreamConn) {
 		for _, ch := range uc.channels {
-			if ch.complete {
-				dc.SendMessage(&irc.Message{
-					Prefix:  dc.prefix(),
-					Command: "JOIN",
-					Params:  []string{dc.marshalEntity(ch.conn.network, ch.Name)},
-				})
-
-				forwardChannel(dc, ch)
-			}
+			if !ch.complete {
+				continue
+			}
+			if record, ok := uc.network.channels[ch.Name]; ok && record.Detached {
+				continue
+			}
+
+			dc.SendMessage(&irc.Message{
+				Prefix:  dc.prefix(),
+				Command: "JOIN",
+				Params:  []string{dc.marshalEntity(ch.conn.network, ch.Name)},
+			})
+
+			forwardChannel(dc, ch)
 		}
 	})
@@ -794,4 +799,8 @@
 func (dc *downstreamConn) sendNetworkHistory(net *network) {
 	for target, history := range net.history {
+		if ch, ok := net.channels[target]; ok && ch.Detached {
+			continue
+		}
+
 		seq, ok := history.offlineClients[dc.clientName]
 		if !ok {
@@ -946,5 +955,5 @@
 			})
 
-			ch := &Channel{Name: upstreamName, Key: key}
+			ch := &Channel{Name: upstreamName, Key: key, Detached: false}
 			if err := uc.network.createUpdateChannel(ch); err != nil {
 				dc.logger.Printf("failed to create or update channel %q: %v", upstreamName, err)
@@ -968,15 +977,22 @@
 			}
 
-			params := []string{upstreamName}
-			if reason != "" {
-				params = append(params, reason)
-			}
-			uc.SendMessage(&irc.Message{
-				Command: "PART",
-				Params:  params,
-			})
-
-			if err := uc.network.deleteChannel(upstreamName); err != nil {
-				dc.logger.Printf("failed to delete channel %q: %v", upstreamName, err)
+			if strings.EqualFold(reason, "detach") {
+				ch := &Channel{Name: upstreamName, Detached: true}
+				if err := uc.network.createUpdateChannel(ch); err != nil {
+					dc.logger.Printf("failed to detach channel %q: %v", upstreamName, err)
+				}
+			} else {
+				params := []string{upstreamName}
+				if reason != "" {
+					params = append(params, reason)
+				}
+				uc.SendMessage(&irc.Message{
+					Command: "PART",
+					Params:  params,
+				})
+
+				if err := uc.network.deleteChannel(upstreamName); err != nil {
+					dc.logger.Printf("failed to delete channel %q: %v", upstreamName, err)
+				}
 			}
 		}
Index: /trunk/upstream.go
===================================================================
--- /trunk/upstream.go	(revision 283)
+++ /trunk/upstream.go	(revision 284)
@@ -1406,6 +1406,11 @@
 // appendHistory appends a message to the history. entity can be empty.
 func (uc *upstreamConn) appendHistory(entity string, msg *irc.Message) {
+	detached := false
+	if ch, ok := uc.network.channels[entity]; ok {
+		detached = ch.Detached
+	}
+
 	// If no client is offline, no need to append the message to the buffer
-	if len(uc.network.offlineClients) == 0 {
+	if len(uc.network.offlineClients) == 0 && !detached {
 		return
 	}
@@ -1421,4 +1426,12 @@
 		for clientName, _ := range uc.network.offlineClients {
 			history.offlineClients[clientName] = 0
+		}
+
+		if detached {
+			// If the channel is detached, online clients act as offline
+			// clients too
+			uc.forEachDownstream(func(dc *downstreamConn) {
+				history.offlineClients[dc.clientName] = 0
+			})
 		}
 	}
@@ -1439,4 +1452,9 @@
 	uc.appendHistory(target, msg)
 
+	// Don't forward messages if it's a detached channel
+	if ch, ok := uc.network.channels[target]; ok && ch.Detached {
+		return
+	}
+
 	uc.forEachDownstream(func(dc *downstreamConn) {
 		if dc != origin || dc.caps["echo-message"] {
Index: /trunk/user.go
===================================================================
--- /trunk/user.go	(revision 283)
+++ /trunk/user.go	(revision 284)
@@ -151,5 +151,49 @@
 		return err
 	}
+	prev := net.channels[ch.Name]
 	net.channels[ch.Name] = ch
+
+	if prev != nil && prev.Detached != ch.Detached {
+		history := net.history[ch.Name]
+		if ch.Detached {
+			net.user.srv.Logger.Printf("network %q: detaching channel %q", net.GetName(), ch.Name)
+			net.forEachDownstream(func(dc *downstreamConn) {
+				net.offlineClients[dc.clientName] = struct{}{}
+				if history != nil {
+					history.offlineClients[dc.clientName] = history.ring.Cur()
+				}
+
+				dc.SendMessage(&irc.Message{
+					Prefix:  dc.prefix(),
+					Command: "PART",
+					Params:  []string{dc.marshalEntity(net, ch.Name), "Detach"},
+				})
+			})
+		} else {
+			net.user.srv.Logger.Printf("network %q: attaching channel %q", net.GetName(), ch.Name)
+
+			var uch *upstreamChannel
+			if net.conn != nil {
+				uch = net.conn.channels[ch.Name]
+			}
+
+			net.forEachDownstream(func(dc *downstreamConn) {
+				dc.SendMessage(&irc.Message{
+					Prefix:  dc.prefix(),
+					Command: "JOIN",
+					Params:  []string{dc.marshalEntity(net, ch.Name)},
+				})
+
+				if uch != nil {
+					forwardChannel(dc, uch)
+				}
+
+				if history != nil {
+					dc.sendNetworkHistory(net)
+				}
+			})
+		}
+	}
+
 	return nil
 }
@@ -343,5 +387,8 @@
 
 				net.offlineClients[dc.clientName] = struct{}{}
-				for _, history := range net.history {
+				for target, history := range net.history {
+					if ch, ok := net.channels[target]; ok && ch.Detached {
+						continue
+					}
 					history.offlineClients[dc.clientName] = history.ring.Cur()
 				}
