detached 7d74423

feat(daemon): transition attachments to remote views

commit
7d7442327850868af46acdc1d84c63d0e185a983
author
brice · · 2026-08-06 17:15 UTC
parent
e9e6c54
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +203 -12
M internal/usecase/daemon/attachment_owner_test.go +45 -0
@@ -68,6 +68,26 @@ func TestRemoteAttachmentTokenUsesExactRemoteOwnerMembership(t *testing.T) {
6868 require.False(t, token.current(), "remote membership is part of exact token identity")
6969 }
7070
71+func TestTransitionToRemoteViewPublishesOneAuthoritativeOwner(t *testing.T) {
72+ d, source, ac, _ := newManualSessionWithPTYs(t, newQuietPTY())
73+ view := &remoteView{key: remoteViewKey{endpoint: "host", lifecycleID: domain.SessionLifecycleID{1}, sessionName: "remote"}}
74+ d.mu.Lock()
75+ require.NoError(t, d.registerRemoteViewLocked(view))
76+ d.mu.Unlock()
77+
78+ before := source.attachmentToken(ac, ac.transport())
79+ require.True(t, before.current())
80+ published, err := d.transitionToRemoteView(before, view)
81+ require.NoError(t, err)
82+
83+ require.False(t, before.current())
84+ require.True(t, published.current())
85+ require.Same(t, view, ac.currentAttachmentOwner())
86+ require.Empty(t, source.snapshotAttachments())
87+ require.True(t, view.attachmentRegistered(ac))
88+ require.Same(t, source, ac.previousOwner.Get())
89+}
90+
7191 func TestShutdownRetiresRemoteViewsWithoutLocalSession(t *testing.T) {
7292 d := newTestDaemon(t, nil, stubClock{})
7393 view := &remoteView{key: remoteViewKey{endpoint: "host", lifecycleID: domain.SessionLifecycleID{1}, sessionName: "remote"}}
@@ -93,6 +113,31 @@ func TestShutdownRetiresRemoteViewsWithoutLocalSession(t *testing.T) {
93113 require.False(t, token.current())
94114 }
95115
116+func TestRemoteClientGoneParksExactOwnerBinding(t *testing.T) {
117+ d := newTestDaemon(t, nil, stubClock{})
118+ view := &remoteView{key: remoteViewKey{endpoint: "host", lifecycleID: domain.SessionLifecycleID{1}, sessionName: "remote"}}
119+ d.mu.Lock()
120+ require.NoError(t, d.registerRemoteViewLocked(view))
121+ d.mu.Unlock()
122+
123+ transport := &closeTrackingTransport{}
124+ ac := &attachedClient{tr: transport, resumeCapable: true}
125+ ac.setAttachmentOwner(view)
126+ view.mu.Lock()
127+ require.True(t, view.registerAttachmentLocked(ac))
128+ view.mu.Unlock()
129+ token := attachmentOwnerToken(view, ac, transport)
130+
131+ require.True(t, d.clientGoneRemote(view, token, false))
132+ require.Nil(t, ac.currentAttachmentOwner())
133+ require.False(t, view.attachmentRegistered(ac))
134+ d.mu.Lock()
135+ parked := d.parked[ac.resumeToken]
136+ d.mu.Unlock()
137+ require.NotNil(t, parked)
138+ require.Same(t, view, parked.owner)
139+}
140+
96141 func TestRemoteParkingRetainsStableOwnerBinding(t *testing.T) {
97142 d := newTestDaemon(t, nil, stubClock{})
98143 view := &remoteView{
@@ -68,6 +68,26 @@ func TestRemoteAttachmentTokenUsesExactRemoteOwnerMembership(t *testing.T) {
68 require.False(t, token.current(), "remote membership is part of exact token identity")68 require.False(t, token.current(), "remote membership is part of exact token identity")
69 }69 }
70 70
71+func TestTransitionToRemoteViewPublishesOneAuthoritativeOwner(t *testing.T) {
72+ d, source, ac, _ := newManualSessionWithPTYs(t, newQuietPTY())
73+ view := &remoteView{key: remoteViewKey{endpoint: "host", lifecycleID: domain.SessionLifecycleID{1}, sessionName: "remote"}}
74+ d.mu.Lock()
75+ require.NoError(t, d.registerRemoteViewLocked(view))
76+ d.mu.Unlock()
77+
78+ before := source.attachmentToken(ac, ac.transport())
79+ require.True(t, before.current())
80+ published, err := d.transitionToRemoteView(before, view)
81+ require.NoError(t, err)
82+
83+ require.False(t, before.current())
84+ require.True(t, published.current())
85+ require.Same(t, view, ac.currentAttachmentOwner())
86+ require.Empty(t, source.snapshotAttachments())
87+ require.True(t, view.attachmentRegistered(ac))
88+ require.Same(t, source, ac.previousOwner.Get())
89+}
90+
71 func TestShutdownRetiresRemoteViewsWithoutLocalSession(t *testing.T) {91 func TestShutdownRetiresRemoteViewsWithoutLocalSession(t *testing.T) {
72 d := newTestDaemon(t, nil, stubClock{})92 d := newTestDaemon(t, nil, stubClock{})
73 view := &remoteView{key: remoteViewKey{endpoint: "host", lifecycleID: domain.SessionLifecycleID{1}, sessionName: "remote"}}93 view := &remoteView{key: remoteViewKey{endpoint: "host", lifecycleID: domain.SessionLifecycleID{1}, sessionName: "remote"}}
@@ -93,6 +113,31 @@ func TestShutdownRetiresRemoteViewsWithoutLocalSession(t *testing.T) {
93 require.False(t, token.current())113 require.False(t, token.current())
94 }114 }
95 115
116+func TestRemoteClientGoneParksExactOwnerBinding(t *testing.T) {
117+ d := newTestDaemon(t, nil, stubClock{})
118+ view := &remoteView{key: remoteViewKey{endpoint: "host", lifecycleID: domain.SessionLifecycleID{1}, sessionName: "remote"}}
119+ d.mu.Lock()
120+ require.NoError(t, d.registerRemoteViewLocked(view))
121+ d.mu.Unlock()
122+
123+ transport := &closeTrackingTransport{}
124+ ac := &attachedClient{tr: transport, resumeCapable: true}
125+ ac.setAttachmentOwner(view)
126+ view.mu.Lock()
127+ require.True(t, view.registerAttachmentLocked(ac))
128+ view.mu.Unlock()
129+ token := attachmentOwnerToken(view, ac, transport)
130+
131+ require.True(t, d.clientGoneRemote(view, token, false))
132+ require.Nil(t, ac.currentAttachmentOwner())
133+ require.False(t, view.attachmentRegistered(ac))
134+ d.mu.Lock()
135+ parked := d.parked[ac.resumeToken]
136+ d.mu.Unlock()
137+ require.NotNil(t, parked)
138+ require.Same(t, view, parked.owner)
139+}
140+
96 func TestRemoteParkingRetainsStableOwnerBinding(t *testing.T) {141 func TestRemoteParkingRetainsStableOwnerBinding(t *testing.T) {
97 d := newTestDaemon(t, nil, stubClock{})142 d := newTestDaemon(t, nil, stubClock{})
98 view := &remoteView{143 view := &remoteView{
M internal/usecase/daemon/client_frame_routing.go +16 -12
@@ -13,24 +13,24 @@ const connectionSnapshotAttempts = 8
1313 // currentAttachmentConnection retries the lock-free session-to-role snapshot when a
1414 // handoff lands between those reads. The attachment gate still performs the final
1515 // mutation admission after this function returns.
16-func (d *Daemon) currentAttachmentConnection(ac *attachedClient, tr ports.Transport) (*session, attachmentConnectionToken, bool) {
16+func (d *Daemon) currentAttachmentConnection(ac *attachedClient, tr ports.Transport) (attachmentOwner, attachmentConnectionToken, bool) {
1717 for range connectionSnapshotAttempts {
1818 if !ac.currentTransportIs(tr) {
1919 break
2020 }
21- sess := ac.currentAttachmentSession()
22- if sess == nil {
21+ owner := ac.currentAttachmentOwner()
22+ if owner == nil {
2323 return nil, attachmentConnectionToken{}, false
2424 }
25- if d.afterConnectionSessionSnapshot != nil {
25+ if sess := localSession(owner); sess != nil && d.afterConnectionSessionSnapshot != nil {
2626 d.afterConnectionSessionSnapshot(sess)
2727 }
28- token := attachmentToken(sess, ac, tr)
29- if token.ac == nil || ac.currentAttachmentSession() != sess {
28+ token := attachmentOwnerToken(owner, ac, tr)
29+ if token.ac == nil || !sameAttachmentOwner(ac.currentAttachmentOwner(), owner) {
3030 runtime.Gosched()
3131 continue
3232 }
33- return sess, token, true
33+ return owner, token, true
3434 }
3535 return nil, attachmentConnectionToken{}, false
3636 }
@@ -44,19 +44,23 @@ func (d *Daemon) runConnLoop(ac *attachedClient) {
4444 return
4545 }
4646 for {
47- if !ac.currentTransportIs(tr) || ac.currentAttachmentSession() == nil {
47+ if !ac.currentTransportIs(tr) || ac.currentAttachmentOwner() == nil {
4848 return
4949 }
5050 f, err := tr.Recv()
5151 if err != nil {
5252 for range connectionSnapshotAttempts {
53- sess, _, ok := d.currentAttachmentConnection(ac, tr)
53+ owner, token, ok := d.currentAttachmentConnection(ac, tr)
5454 if !ok {
5555 return
5656 }
57- d.clientGone(sess, ac, tr, false)
58- current := ac.currentAttachmentSession()
59- if current == sess || !ac.currentTransportIs(tr) || current == nil {
57+ if sess := localSession(owner); sess != nil {
58+ d.clientGone(sess, ac, tr, false)
59+ } else if view, remote := owner.(*remoteView); remote {
60+ d.clientGoneRemote(view, token, false)
61+ }
62+ current := ac.currentAttachmentOwner()
63+ if sameAttachmentOwner(current, owner) || !ac.currentTransportIs(tr) || current == nil {
6064 return
6165 }
6266 }
@@ -13,24 +13,24 @@ const connectionSnapshotAttempts = 8
13 // currentAttachmentConnection retries the lock-free session-to-role snapshot when a13 // currentAttachmentConnection retries the lock-free session-to-role snapshot when a
14 // handoff lands between those reads. The attachment gate still performs the final14 // handoff lands between those reads. The attachment gate still performs the final
15 // mutation admission after this function returns.15 // mutation admission after this function returns.
16-func (d *Daemon) currentAttachmentConnection(ac *attachedClient, tr ports.Transport) (*session, attachmentConnectionToken, bool) {16+func (d *Daemon) currentAttachmentConnection(ac *attachedClient, tr ports.Transport) (attachmentOwner, attachmentConnectionToken, bool) {
17 for range connectionSnapshotAttempts {17 for range connectionSnapshotAttempts {
18 if !ac.currentTransportIs(tr) {18 if !ac.currentTransportIs(tr) {
19 break19 break
20 }20 }
21- sess := ac.currentAttachmentSession()21+ owner := ac.currentAttachmentOwner()
22- if sess == nil {22+ if owner == nil {
23 return nil, attachmentConnectionToken{}, false23 return nil, attachmentConnectionToken{}, false
24 }24 }
25- if d.afterConnectionSessionSnapshot != nil {25+ if sess := localSession(owner); sess != nil && d.afterConnectionSessionSnapshot != nil {
26 d.afterConnectionSessionSnapshot(sess)26 d.afterConnectionSessionSnapshot(sess)
27 }27 }
28- token := attachmentToken(sess, ac, tr)28+ token := attachmentOwnerToken(owner, ac, tr)
29- if token.ac == nil || ac.currentAttachmentSession() != sess {29+ if token.ac == nil || !sameAttachmentOwner(ac.currentAttachmentOwner(), owner) {
30 runtime.Gosched()30 runtime.Gosched()
31 continue31 continue
32 }32 }
33- return sess, token, true33+ return owner, token, true
34 }34 }
35 return nil, attachmentConnectionToken{}, false35 return nil, attachmentConnectionToken{}, false
36 }36 }
@@ -44,19 +44,23 @@ func (d *Daemon) runConnLoop(ac *attachedClient) {
44 return44 return
45 }45 }
46 for {46 for {
47- if !ac.currentTransportIs(tr) || ac.currentAttachmentSession() == nil {47+ if !ac.currentTransportIs(tr) || ac.currentAttachmentOwner() == nil {
48 return48 return
49 }49 }
50 f, err := tr.Recv()50 f, err := tr.Recv()
51 if err != nil {51 if err != nil {
52 for range connectionSnapshotAttempts {52 for range connectionSnapshotAttempts {
53- sess, _, ok := d.currentAttachmentConnection(ac, tr)53+ owner, token, ok := d.currentAttachmentConnection(ac, tr)
54 if !ok {54 if !ok {
55 return55 return
56 }56 }
57- d.clientGone(sess, ac, tr, false)57+ if sess := localSession(owner); sess != nil {
58- current := ac.currentAttachmentSession()58+ d.clientGone(sess, ac, tr, false)
59- if current == sess || !ac.currentTransportIs(tr) || current == nil {59+ } else if view, remote := owner.(*remoteView); remote {
60+ d.clientGoneRemote(view, token, false)
61+ }
62+ current := ac.currentAttachmentOwner()
63+ if sameAttachmentOwner(current, owner) || !ac.currentTransportIs(tr) || current == nil {
60 return64 return
61 }65 }
62 }66 }
M internal/usecase/daemon/client_transport_errors.go +69 -0
@@ -37,6 +37,9 @@ func (d *Daemon) clientGoneForAttachment(token attachmentConnectionToken, explic
3737 }
3838 token.effect.bindActionEnd(d, "detach")
3939 token.effect.End()
40+ if view, remote := token.owner.(*remoteView); remote {
41+ return d.clientGoneRemote(view, token, explicit)
42+ }
4043 sess := token.localSession()
4144 if sess == nil {
4245 return false
@@ -53,6 +56,72 @@ func (d *Daemon) clientGoneForAttachment(token attachmentConnectionToken, explic
5356 return true
5457 }
5558
59+// clientGoneRemote retires only the current local-client binding to a remote
60+// view. The remote link itself remains a Phase 4 lifecycle concern; a failed
61+// local transport parks the stable view owner without inventing a local PTY.
62+func (d *Daemon) clientGoneRemote(view *remoteView, token attachmentConnectionToken, explicit bool) bool {
63+ if d == nil || view == nil || token.ac == nil || !sameAttachmentOwner(token.owner, view) {
64+ return false
65+ }
66+ ac := token.ac
67+ var parkingToken uint64
68+ if !explicit {
69+ parkingToken = d.markParkingInFlightOwner(view, ac)
70+ }
71+ if !d.detachRemoteAttachmentCurrent(view, token) {
72+ d.clearParkingInFlightIfAbandonedOwner(view, ac, parkingToken)
73+ return false
74+ }
75+ d.unregisterPreview(ac)
76+ if !explicit && d.parkAttachmentOwner(view, ac) {
77+ _ = ac.closeCapturedTransport(token.transport.transport)
78+ return true
79+ }
80+ d.clearParkingInFlight(ac.resumeToken, ac)
81+ d.closePicker(ac)
82+ ac.clearPreviousSession()
83+ if explicit {
84+ d.boundedSend(ac, frameDetached(ports.ReasonDetach))
85+ }
86+ _ = ac.closeCapturedTransport(token.transport.transport)
87+ return true
88+}
89+
90+// detachRemoteAttachmentCurrent invalidates exactly the frozen remote-view
91+// membership. It performs no transport I/O while architecture locks are held.
92+func (d *Daemon) detachRemoteAttachmentCurrent(view *remoteView, token attachmentConnectionToken) bool {
93+ if d == nil || view == nil || token.ac == nil || token.transport.transport == nil {
94+ return false
95+ }
96+ frozen := freezeAttachmentEffectGates(token.ac)
97+ defer frozen.unfreeze()
98+ if !frozen.acquired || !frozen.drained {
99+ return false
100+ }
101+ d.mu.Lock()
102+ if d.closing || !d.attachmentOwnerRegisteredByDaemonLocked(view) {
103+ d.mu.Unlock()
104+ return false
105+ }
106+ view.mu.Lock()
107+ current := sameAttachmentOwner(token.owner, view) &&
108+ token.generation == token.ac.connectionGeneration.Load() &&
109+ sameAttachmentOwner(token.ac.currentAttachmentOwner(), view) &&
110+ token.ac.transportSnapshotCurrent(token.transport)
111+ if _, registered := view.attachments[token.ac]; !registered {
112+ current = false
113+ }
114+ if current {
115+ view.unregisterAttachmentLocked(token.ac)
116+ token.ac.connectionGeneration.Add(1)
117+ token.ac.setAttachmentOwner(nil)
118+ token.ac.invalidateFrozenAttachmentCapability()
119+ }
120+ view.mu.Unlock()
121+ d.mu.Unlock()
122+ return current
123+}
124+
56125 func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed ports.Transport, explicit bool) {
57126 if sess == nil || ac == nil {
58127 return
@@ -37,6 +37,9 @@ func (d *Daemon) clientGoneForAttachment(token attachmentConnectionToken, explic
37 }37 }
38 token.effect.bindActionEnd(d, "detach")38 token.effect.bindActionEnd(d, "detach")
39 token.effect.End()39 token.effect.End()
40+ if view, remote := token.owner.(*remoteView); remote {
41+ return d.clientGoneRemote(view, token, explicit)
42+ }
40 sess := token.localSession()43 sess := token.localSession()
41 if sess == nil {44 if sess == nil {
42 return false45 return false
@@ -53,6 +56,72 @@ func (d *Daemon) clientGoneForAttachment(token attachmentConnectionToken, explic
53 return true56 return true
54 }57 }
55 58
59+// clientGoneRemote retires only the current local-client binding to a remote
60+// view. The remote link itself remains a Phase 4 lifecycle concern; a failed
61+// local transport parks the stable view owner without inventing a local PTY.
62+func (d *Daemon) clientGoneRemote(view *remoteView, token attachmentConnectionToken, explicit bool) bool {
63+ if d == nil || view == nil || token.ac == nil || !sameAttachmentOwner(token.owner, view) {
64+ return false
65+ }
66+ ac := token.ac
67+ var parkingToken uint64
68+ if !explicit {
69+ parkingToken = d.markParkingInFlightOwner(view, ac)
70+ }
71+ if !d.detachRemoteAttachmentCurrent(view, token) {
72+ d.clearParkingInFlightIfAbandonedOwner(view, ac, parkingToken)
73+ return false
74+ }
75+ d.unregisterPreview(ac)
76+ if !explicit && d.parkAttachmentOwner(view, ac) {
77+ _ = ac.closeCapturedTransport(token.transport.transport)
78+ return true
79+ }
80+ d.clearParkingInFlight(ac.resumeToken, ac)
81+ d.closePicker(ac)
82+ ac.clearPreviousSession()
83+ if explicit {
84+ d.boundedSend(ac, frameDetached(ports.ReasonDetach))
85+ }
86+ _ = ac.closeCapturedTransport(token.transport.transport)
87+ return true
88+}
89+
90+// detachRemoteAttachmentCurrent invalidates exactly the frozen remote-view
91+// membership. It performs no transport I/O while architecture locks are held.
92+func (d *Daemon) detachRemoteAttachmentCurrent(view *remoteView, token attachmentConnectionToken) bool {
93+ if d == nil || view == nil || token.ac == nil || token.transport.transport == nil {
94+ return false
95+ }
96+ frozen := freezeAttachmentEffectGates(token.ac)
97+ defer frozen.unfreeze()
98+ if !frozen.acquired || !frozen.drained {
99+ return false
100+ }
101+ d.mu.Lock()
102+ if d.closing || !d.attachmentOwnerRegisteredByDaemonLocked(view) {
103+ d.mu.Unlock()
104+ return false
105+ }
106+ view.mu.Lock()
107+ current := sameAttachmentOwner(token.owner, view) &&
108+ token.generation == token.ac.connectionGeneration.Load() &&
109+ sameAttachmentOwner(token.ac.currentAttachmentOwner(), view) &&
110+ token.ac.transportSnapshotCurrent(token.transport)
111+ if _, registered := view.attachments[token.ac]; !registered {
112+ current = false
113+ }
114+ if current {
115+ view.unregisterAttachmentLocked(token.ac)
116+ token.ac.connectionGeneration.Add(1)
117+ token.ac.setAttachmentOwner(nil)
118+ token.ac.invalidateFrozenAttachmentCapability()
119+ }
120+ view.mu.Unlock()
121+ d.mu.Unlock()
122+ return current
123+}
124+
56 func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed ports.Transport, explicit bool) {125 func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed ports.Transport, explicit bool) {
57 if sess == nil || ac == nil {126 if sess == nil || ac == nil {
58 return127 return
A internal/usecase/daemon/remote_attachment_transition.go +73 -0
@@ -0,0 +1,73 @@
1+package daemon
2+
3+// transitionToRemoteView publishes an attachment-owned remote surface without
4+// changing the local session registry. Candidate construction and remote-link
5+// I/O deliberately happen before this function; this function only performs
6+// the frozen, exact local ownership hand-off.
7+func (d *Daemon) transitionToRemoteView(token attachmentConnectionToken, target *remoteView) (attachmentConnectionToken, error) {
8+ source := token.localSession()
9+ if d == nil || source == nil || token.ac == nil || target == nil {
10+ return attachmentConnectionToken{}, errAttachmentTransition
11+ }
12+ if token.effect != nil {
13+ d.endActionAttachmentEffect(token.effect, "remote-view-transition")
14+ }
15+ frozen := freezeAttachmentEffectGates(token.ac)
16+ defer frozen.unfreeze()
17+ if !frozen.acquired || !frozen.drained {
18+ return attachmentConnectionToken{}, errAttachmentTransition
19+ }
20+
21+ d.mu.Lock()
22+ if d.closing || d.sessions[source.id] != source || !d.attachmentOwnerRegisteredByDaemonLocked(target) {
23+ d.mu.Unlock()
24+ return attachmentConnectionToken{}, errAttachmentTransition
25+ }
26+ d.notices.routingMu.Lock()
27+ source.mu.Lock()
28+ target.mu.Lock()
29+ current := sameAttachmentOwner(token.owner, source) &&
30+ token.generation == token.ac.connectionGeneration.Load() &&
31+ sameAttachmentOwner(token.ac.currentAttachmentOwner(), source) &&
32+ token.ac.transportSnapshotCurrent(token.transport) &&
33+ attachmentRegisteredLocked(source, token.ac) &&
34+ !target.closed
35+ if !current {
36+ target.mu.Unlock()
37+ source.mu.Unlock()
38+ d.notices.routingMu.Unlock()
39+ d.mu.Unlock()
40+ return attachmentConnectionToken{}, errAttachmentTransition
41+ }
42+ if !target.registerAttachmentLocked(token.ac) {
43+ target.mu.Unlock()
44+ source.mu.Unlock()
45+ d.notices.routingMu.Unlock()
46+ d.mu.Unlock()
47+ return attachmentConnectionToken{}, errAttachmentTransition
48+ }
49+ unregisterAttachmentSessionLocked(source, token.ac)
50+ generation := token.ac.connectionGeneration.Add(1)
51+ token.ac.setAttachmentOwner(target)
52+ published := attachmentConnectionToken{
53+ owner: target,
54+ ac: token.ac,
55+ generation: generation,
56+ transport: token.transport,
57+ rebase: true,
58+ }
59+ token.ac.publishFrozenAttachmentCapability(published)
60+ target.mu.Unlock()
61+ source.mu.Unlock()
62+ d.notices.routingMu.Unlock()
63+ d.mu.Unlock()
64+
65+ // The old coordinator is no longer a lease owner for this attachment. Its
66+ // cleanup runs after publication and outside architecture locks.
67+ if coordinator := source.renderCoordinator(); coordinator != nil {
68+ coordinator.noteDetach(token.ac)
69+ }
70+ d.recalculateSessionGeometryAndInvalidateAsync(source, "remote_attachment_transition.go")
71+ token.ac.recordPreviousOwner(source)
72+ return published, nil
73+}
@@ -0,0 +1,73 @@
1+package daemon
2+
3+// transitionToRemoteView publishes an attachment-owned remote surface without
4+// changing the local session registry. Candidate construction and remote-link
5+// I/O deliberately happen before this function; this function only performs
6+// the frozen, exact local ownership hand-off.
7+func (d *Daemon) transitionToRemoteView(token attachmentConnectionToken, target *remoteView) (attachmentConnectionToken, error) {
8+ source := token.localSession()
9+ if d == nil || source == nil || token.ac == nil || target == nil {
10+ return attachmentConnectionToken{}, errAttachmentTransition
11+ }
12+ if token.effect != nil {
13+ d.endActionAttachmentEffect(token.effect, "remote-view-transition")
14+ }
15+ frozen := freezeAttachmentEffectGates(token.ac)
16+ defer frozen.unfreeze()
17+ if !frozen.acquired || !frozen.drained {
18+ return attachmentConnectionToken{}, errAttachmentTransition
19+ }
20+
21+ d.mu.Lock()
22+ if d.closing || d.sessions[source.id] != source || !d.attachmentOwnerRegisteredByDaemonLocked(target) {
23+ d.mu.Unlock()
24+ return attachmentConnectionToken{}, errAttachmentTransition
25+ }
26+ d.notices.routingMu.Lock()
27+ source.mu.Lock()
28+ target.mu.Lock()
29+ current := sameAttachmentOwner(token.owner, source) &&
30+ token.generation == token.ac.connectionGeneration.Load() &&
31+ sameAttachmentOwner(token.ac.currentAttachmentOwner(), source) &&
32+ token.ac.transportSnapshotCurrent(token.transport) &&
33+ attachmentRegisteredLocked(source, token.ac) &&
34+ !target.closed
35+ if !current {
36+ target.mu.Unlock()
37+ source.mu.Unlock()
38+ d.notices.routingMu.Unlock()
39+ d.mu.Unlock()
40+ return attachmentConnectionToken{}, errAttachmentTransition
41+ }
42+ if !target.registerAttachmentLocked(token.ac) {
43+ target.mu.Unlock()
44+ source.mu.Unlock()
45+ d.notices.routingMu.Unlock()
46+ d.mu.Unlock()
47+ return attachmentConnectionToken{}, errAttachmentTransition
48+ }
49+ unregisterAttachmentSessionLocked(source, token.ac)
50+ generation := token.ac.connectionGeneration.Add(1)
51+ token.ac.setAttachmentOwner(target)
52+ published := attachmentConnectionToken{
53+ owner: target,
54+ ac: token.ac,
55+ generation: generation,
56+ transport: token.transport,
57+ rebase: true,
58+ }
59+ token.ac.publishFrozenAttachmentCapability(published)
60+ target.mu.Unlock()
61+ source.mu.Unlock()
62+ d.notices.routingMu.Unlock()
63+ d.mu.Unlock()
64+
65+ // The old coordinator is no longer a lease owner for this attachment. Its
66+ // cleanup runs after publication and outside architecture locks.
67+ if coordinator := source.renderCoordinator(); coordinator != nil {
68+ coordinator.noteDetach(token.ac)
69+ }
70+ d.recalculateSessionGeometryAndInvalidateAsync(source, "remote_attachment_transition.go")
71+ token.ac.recordPreviousOwner(source)
72+ return published, nil
73+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close