Patchwork D2951: wireproto: use CBOR for command requests

login
register
mail settings
Submitter phabricator
Date April 3, 2018, 4:51 p.m.
Message ID <22aa25311447b6699666a5fc95e4180f@localhost.localdomain>
Download mbox | patch
Permalink /patch/30185/
State Not Applicable
Headers show

Comments

phabricator - April 3, 2018, 4:51 p.m.
This revision was automatically updated to reflect the committed changes.
Closed by commit rHG3d0e2cd86e05: wireproto: use CBOR for command requests (authored by indygreg, committed by ).

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D2951?vs=7439&id=7553

REVISION DETAIL
  https://phab.mercurial-scm.org/D2951

AFFECTED FILES
  mercurial/help/internals/wireprotocol.txt
  mercurial/wireprotoframing.py
  mercurial/wireprotoserver.py
  tests/test-http-api-httpv2.t
  tests/test-wireproto-serverreactor.py

CHANGE DETAILS




To: indygreg, #hg-reviewers, durin42
Cc: mercurial-devel

Patch

diff --git a/tests/test-wireproto-serverreactor.py b/tests/test-wireproto-serverreactor.py
--- a/tests/test-wireproto-serverreactor.py
+++ b/tests/test-wireproto-serverreactor.py
@@ -2,6 +2,9 @@ 
 
 import unittest
 
+from mercurial.thirdparty import (
+    cbor,
+)
 from mercurial import (
     util,
     wireprotoframing as framing,
@@ -96,7 +99,8 @@ 
         frames = list(framing.createcommandframes(stream, 1, b'command',
                                                   {}, data))
         self.assertEqual(frames, [
-            ffs(b'1 1 stream-begin command-name have-data command'),
+            ffs(b'1 1 stream-begin command-request new|have-data '
+                b"cbor:{b'name': b'command'}"),
             ffs(b'1 1 0 command-data continuation %s' % data.getvalue()),
             ffs(b'1 1 0 command-data eos ')
         ])
@@ -108,7 +112,8 @@ 
         frames = list(framing.createcommandframes(stream, 1, b'command', {},
                                                   data))
         self.assertEqual(frames, [
-            ffs(b'1 1 stream-begin command-name have-data command'),
+            ffs(b'1 1 stream-begin command-request new|have-data '
+                b"cbor:{b'name': b'command'}"),
             ffs(b'1 1 0 command-data continuation %s' % (
                 b'x' * framing.DEFAULT_MAX_FRAME_SIZE)),
             ffs(b'1 1 0 command-data eos x'),
@@ -125,10 +130,9 @@ 
         }, data))
 
         self.assertEqual(frames, [
-            ffs(b'1 1 stream-begin command-name have-args|have-data command'),
-            ffs(br'1 1 0 command-argument 0 \x04\x00\x09\x00key1key1value'),
-            ffs(br'1 1 0 command-argument 0 \x04\x00\x09\x00key2key2value'),
-            ffs(br'1 1 0 command-argument eoa \x04\x00\x09\x00key3key3value'),
+            ffs(b'1 1 stream-begin command-request new|have-data '
+                b"cbor:{b'name': b'command', b'args': {b'key1': b'key1value', "
+                b"b'key2': b'key2value', b'key3': b'key3value'}}"),
             ffs(b'1 1 0 command-data eos %s' % data.getvalue()),
         ])
 
@@ -286,10 +290,9 @@ 
         stream = framing.stream(1)
         results = list(sendcommandframes(reactor, stream, 41, b'mycommand',
                                          {b'foo': b'bar'}))
-        self.assertEqual(len(results), 2)
-        self.assertaction(results[0], 'wantframe')
-        self.assertaction(results[1], 'runcommand')
-        self.assertEqual(results[1][1], {
+        self.assertEqual(len(results), 1)
+        self.assertaction(results[0], 'runcommand')
+        self.assertEqual(results[0][1], {
             'requestid': 41,
             'command': b'mycommand',
             'args': {b'foo': b'bar'},
@@ -301,11 +304,9 @@ 
         stream = framing.stream(1)
         results = list(sendcommandframes(reactor, stream, 1, b'mycommand',
                                          {b'foo': b'bar', b'biz': b'baz'}))
-        self.assertEqual(len(results), 3)
-        self.assertaction(results[0], 'wantframe')
-        self.assertaction(results[1], 'wantframe')
-        self.assertaction(results[2], 'runcommand')
-        self.assertEqual(results[2][1], {
+        self.assertEqual(len(results), 1)
+        self.assertaction(results[0], 'runcommand')
+        self.assertEqual(results[0][1], {
             'requestid': 1,
             'command': b'mycommand',
             'args': {b'foo': b'bar', b'biz': b'baz'},
@@ -329,7 +330,8 @@ 
 
     def testmultipledataframes(self):
         frames = [
-            ffs(b'1 1 stream-begin command-name have-data mycommand'),
+            ffs(b'1 1 stream-begin command-request new|have-data '
+                b"cbor:{b'name': b'mycommand'}"),
             ffs(b'1 1 0 command-data continuation data1'),
             ffs(b'1 1 0 command-data continuation data2'),
             ffs(b'1 1 0 command-data eos data3'),
@@ -350,9 +352,9 @@ 
 
     def testargumentanddata(self):
         frames = [
-            ffs(b'1 1 stream-begin command-name have-args|have-data command'),
-            ffs(br'1 1 0 command-argument 0 \x03\x00\x03\x00keyval'),
-            ffs(br'1 1 0 command-argument eoa \x03\x00\x03\x00foobar'),
+            ffs(b'1 1 stream-begin command-request new|have-data '
+                b"cbor:{b'name': b'command', b'args': {b'key': b'val',"
+                b"b'foo': b'bar'}}"),
             ffs(b'1 1 0 command-data continuation value1'),
             ffs(b'1 1 0 command-data eos value2'),
         ]
@@ -371,76 +373,68 @@ 
             'data': b'value1value2',
         })
 
-    def testunexpectedcommandargument(self):
-        """Command argument frame when not running a command is an error."""
-        result = self._sendsingleframe(
-            makereactor(), ffs(b'1 1 stream-begin command-argument 0 ignored'))
+    def testnewandcontinuation(self):
+        result = self._sendsingleframe(makereactor(),
+            ffs(b'1 1 stream-begin command-request new|continuation '))
         self.assertaction(result, 'error')
         self.assertEqual(result[1], {
-            'message': b'expected command frame; got 2',
+            'message': b'received command request frame with both new and '
+                       b'continuation flags set',
         })
 
-    def testunexpectedcommandargumentreceiving(self):
-        """Same as above but the command is receiving."""
-        results = list(sendframes(makereactor(), [
-            ffs(b'1 1 stream-begin command-name have-data command'),
-            ffs(b'1 1 0 command-argument eoa ignored'),
-        ]))
-
-        self.assertaction(results[1], 'error')
-        self.assertEqual(results[1][1], {
-            'message': b'received command argument frame for request that is '
-                       b'not expecting arguments: 1',
+    def testneithernewnorcontinuation(self):
+        result = self._sendsingleframe(makereactor(),
+            ffs(b'1 1 stream-begin command-request 0 '))
+        self.assertaction(result, 'error')
+        self.assertEqual(result[1], {
+            'message': b'received command request frame with neither new nor '
+                       b'continuation flags set',
         })
 
     def testunexpectedcommanddata(self):
-        """Command argument frame when not running a command is an error."""
-        result = self._sendsingleframe(
-            makereactor(), ffs(b'1 1 stream-begin command-data 0 ignored'))
+        """Command data frame when not running a command is an error."""
+        result = self._sendsingleframe(makereactor(),
+            ffs(b'1 1 stream-begin command-data 0 ignored'))
         self.assertaction(result, 'error')
         self.assertEqual(result[1], {
-            'message': b'expected command frame; got 3',
+            'message': b'expected command request frame; got 3',
         })
 
     def testunexpectedcommanddatareceiving(self):
         """Same as above except the command is receiving."""
         results = list(sendframes(makereactor(), [
-            ffs(b'1 1 stream-begin command-name have-args command'),
+            ffs(b'1 1 stream-begin command-request new|more '
+                b"cbor:{b'name': b'ignored'}"),
             ffs(b'1 1 0 command-data eos ignored'),
         ]))
 
+        self.assertaction(results[0], 'wantframe')
         self.assertaction(results[1], 'error')
         self.assertEqual(results[1][1], {
             'message': b'received command data frame for request that is not '
                        b'expecting data: 1',
         })
 
-    def testmissingcommandframeflags(self):
-        """Command name frame must have flags set."""
-        result = self._sendsingleframe(
-            makereactor(), ffs(b'1 1 stream-begin command-name 0 command'))
-        self.assertaction(result, 'error')
-        self.assertEqual(result[1], {
-            'message': b'missing frame flags on command frame',
-        })
-
     def testconflictingrequestidallowed(self):
         """Multiple fully serviced commands with same request ID is allowed."""
         reactor = makereactor()
         results = []
         outstream = reactor.makeoutputstream()
         results.append(self._sendsingleframe(
-            reactor, ffs(b'1 1 stream-begin command-name eos command')))
+            reactor, ffs(b'1 1 stream-begin command-request new '
+                         b"cbor:{b'name': b'command'}")))
         result = reactor.onbytesresponseready(outstream, 1, b'response1')
         self.assertaction(result, 'sendframes')
         list(result[1]['framegen'])
         results.append(self._sendsingleframe(
-            reactor, ffs(b'1 1 0 command-name eos command')))
+            reactor, ffs(b'1 1 stream-begin command-request new '
+                         b"cbor:{b'name': b'command'}")))
         result = reactor.onbytesresponseready(outstream, 1, b'response2')
         self.assertaction(result, 'sendframes')
         list(result[1]['framegen'])
         results.append(self._sendsingleframe(
-            reactor, ffs(b'1 1 0 command-name eos command')))
+            reactor, ffs(b'1 1 stream-begin command-request new '
+                         b"cbor:{b'name': b'command'}")))
         result = reactor.onbytesresponseready(outstream, 1, b'response3')
         self.assertaction(result, 'sendframes')
         list(result[1]['framegen'])
@@ -457,8 +451,10 @@ 
     def testconflictingrequestid(self):
         """Request ID for new command matching in-flight command is illegal."""
         results = list(sendframes(makereactor(), [
-            ffs(b'1 1 stream-begin command-name have-args command'),
-            ffs(b'1 1 0 command-name eos command'),
+            ffs(b'1 1 stream-begin command-request new|more '
+                b"cbor:{b'name': b'command'}"),
+            ffs(b'1 1 0 command-request new '
+                b"cbor:{b'name': b'command1'}"),
         ]))
 
         self.assertaction(results[0], 'wantframe')
@@ -468,13 +464,28 @@ 
         })
 
     def testinterleavedcommands(self):
+        cbor1 = cbor.dumps({
+            b'name': b'command1',
+            b'args': {
+                b'foo': b'bar',
+                b'key1': b'val',
+            }
+        }, canonical=True)
+        cbor3 = cbor.dumps({
+            b'name': b'command3',
+            b'args': {
+                b'biz': b'baz',
+                b'key': b'val',
+            },
+        }, canonical=True)
+
         results = list(sendframes(makereactor(), [
-            ffs(b'1 1 stream-begin command-name have-args command1'),
-            ffs(b'3 1 0 command-name have-args command3'),
-            ffs(br'1 1 0 command-argument 0 \x03\x00\x03\x00foobar'),
-            ffs(br'3 1 0 command-argument 0 \x03\x00\x03\x00bizbaz'),
-            ffs(br'3 1 0 command-argument eoa \x03\x00\x03\x00keyval'),
-            ffs(br'1 1 0 command-argument eoa \x04\x00\x03\x00key1val'),
+            ffs(b'1 1 stream-begin command-request new|more %s' % cbor1[0:6]),
+            ffs(b'3 1 0 command-request new|more %s' % cbor3[0:10]),
+            ffs(b'1 1 0 command-request continuation|more %s' % cbor1[6:9]),
+            ffs(b'3 1 0 command-request continuation|more %s' % cbor3[10:13]),
+            ffs(b'3 1 0 command-request continuation %s' % cbor3[13:]),
+            ffs(b'1 1 0 command-request continuation %s' % cbor1[9:]),
         ]))
 
         self.assertEqual([t[0] for t in results], [
@@ -499,62 +510,24 @@ 
             'data': None,
         })
 
-    def testmissingargumentframe(self):
-        # This test attempts to test behavior when reactor has an incomplete
-        # command request waiting on argument data. But it doesn't handle that
-        # scenario yet. So this test does nothing of value.
-        frames = [
-            ffs(b'1 1 stream-begin command-name have-args command'),
-        ]
-
-        results = list(sendframes(makereactor(), frames))
-        self.assertaction(results[0], 'wantframe')
-
-    def testincompleteargumentname(self):
-        """Argument frame with incomplete name."""
-        frames = [
-            ffs(b'1 1 stream-begin command-name have-args command1'),
-            ffs(br'1 1 0 command-argument eoa \x04\x00\xde\xadfoo'),
-        ]
-
-        results = list(sendframes(makereactor(), frames))
-        self.assertEqual(len(results), 2)
-        self.assertaction(results[0], 'wantframe')
-        self.assertaction(results[1], 'error')
-        self.assertEqual(results[1][1], {
-            'message': b'malformed argument frame: partial argument name',
-        })
-
-    def testincompleteargumentvalue(self):
-        """Argument frame with incomplete value."""
-        frames = [
-            ffs(b'1 1 stream-begin command-name have-args command'),
-            ffs(br'1 1 0 command-argument eoa \x03\x00\xaa\xaafoopartialvalue'),
-        ]
-
-        results = list(sendframes(makereactor(), frames))
-        self.assertEqual(len(results), 2)
-        self.assertaction(results[0], 'wantframe')
-        self.assertaction(results[1], 'error')
-        self.assertEqual(results[1][1], {
-            'message': b'malformed argument frame: partial argument value',
-        })
-
     def testmissingcommanddataframe(self):
         # The reactor doesn't currently handle partially received commands.
         # So this test is failing to do anything with request 1.
         frames = [
-            ffs(b'1 1 stream-begin command-name have-data command1'),
-            ffs(b'3 1 0 command-name eos command2'),
+            ffs(b'1 1 stream-begin command-request new|have-data '
+                b"cbor:{b'name': b'command1'}"),
+            ffs(b'3 1 0 command-request new '
+                b"cbor:{b'name': b'command2'}"),
         ]
         results = list(sendframes(makereactor(), frames))
         self.assertEqual(len(results), 2)
         self.assertaction(results[0], 'wantframe')
         self.assertaction(results[1], 'runcommand')
 
     def testmissingcommanddataframeflags(self):
         frames = [
-            ffs(b'1 1 stream-begin command-name have-data command1'),
+            ffs(b'1 1 stream-begin command-request new|have-data '
+                b"cbor:{b'name': b'command1'}"),
             ffs(b'1 1 0 command-data 0 data'),
         ]
         results = list(sendframes(makereactor(), frames))
@@ -568,9 +541,11 @@ 
     def testframefornonreceivingrequest(self):
         """Receiving a frame for a command that is not receiving is illegal."""
         results = list(sendframes(makereactor(), [
-            ffs(b'1 1 stream-begin command-name eos command1'),
-            ffs(b'3 1 0 command-name have-data command3'),
-            ffs(b'5 1 0 command-argument eoa ignored'),
+            ffs(b'1 1 stream-begin command-request new '
+                b"cbor:{b'name': b'command1'}"),
+            ffs(b'3 1 0 command-request new|have-data '
+                b"cbor:{b'name': b'command3'}"),
+            ffs(b'5 1 0 command-data eos ignored'),
         ]))
         self.assertaction(results[2], 'error')
         self.assertEqual(results[2][1], {
@@ -705,21 +680,6 @@ 
             'message': b'request with ID 1 is already active',
         })
 
-    def testduplicaterequestargumentframe(self):
-        """Variant on above except we sent an argument frame instead of name."""
-        reactor = makereactor()
-        stream = framing.stream(1)
-        list(sendcommandframes(reactor, stream, 1, b'command', {}))
-        results = list(sendframes(reactor, [
-            ffs(b'3 1 stream-begin command-name have-args command'),
-            ffs(b'1 1 0 command-argument 0 ignored'),
-        ]))
-        self.assertaction(results[0], 'wantframe')
-        self.assertaction(results[1], 'error')
-        self.assertEqual(results[1][1], {
-            'message': 'received frame for request that is still active: 1',
-        })
-
     def testduplicaterequestaftersend(self):
         """We can use a duplicate request ID after we've sent the response."""
         reactor = makereactor()
diff --git a/tests/test-http-api-httpv2.t b/tests/test-http-api-httpv2.t
--- a/tests/test-http-api-httpv2.t
+++ b/tests/test-http-api-httpv2.t
@@ -1,5 +1,5 @@ 
   $ HTTPV2=exp-http-v2-0001
-  $ MEDIATYPE=application/mercurial-exp-framing-0002
+  $ MEDIATYPE=application/mercurial-exp-framing-0003
 
   $ send() {
   >   hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT/
@@ -122,7 +122,7 @@ 
   s>     Content-Type: text/plain\r\n
   s>     Content-Length: 85\r\n
   s>     \r\n
-  s>     client MUST specify Accept header with value: application/mercurial-exp-framing-0002\n
+  s>     client MUST specify Accept header with value: application/mercurial-exp-framing-0003\n
 
 Bad Accept header results in 406
 
@@ -145,7 +145,7 @@ 
   s>     Content-Type: text/plain\r\n
   s>     Content-Length: 85\r\n
   s>     \r\n
-  s>     client MUST specify Accept header with value: application/mercurial-exp-framing-0002\n
+  s>     client MUST specify Accept header with value: application/mercurial-exp-framing-0003\n
 
 Bad Content-Type header results in 415
 
@@ -158,7 +158,7 @@ 
   using raw connection to peer
   s>     POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
   s>     Accept-Encoding: identity\r\n
-  s>     accept: application/mercurial-exp-framing-0002\r\n
+  s>     accept: application/mercurial-exp-framing-0003\r\n
   s>     content-type: badmedia\r\n
   s>     user-agent: test\r\n
   s>     host: $LOCALIP:$HGPORT\r\n (glob)
@@ -170,32 +170,32 @@ 
   s>     Content-Type: text/plain\r\n
   s>     Content-Length: 88\r\n
   s>     \r\n
-  s>     client MUST send Content-Type header with value: application/mercurial-exp-framing-0002\n
+  s>     client MUST send Content-Type header with value: application/mercurial-exp-framing-0003\n
 
 Request to read-only command works out of the box
 
   $ send << EOF
   > httprequest POST api/$HTTPV2/ro/customreadonly
   >     accept: $MEDIATYPE
   >     content-type: $MEDIATYPE
   >     user-agent: test
-  >     frame 1 1 stream-begin command-name eos customreadonly
+  >     frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
   > EOF
   using raw connection to peer
   s>     POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
   s>     Accept-Encoding: identity\r\n
-  s>     accept: application/mercurial-exp-framing-0002\r\n
-  s>     content-type: application/mercurial-exp-framing-0002\r\n
+  s>     *\r\n (glob)
+  s>     content-type: application/mercurial-exp-framing-0003\r\n
   s>     user-agent: test\r\n
-  s>     *\r\n (glob)
+  s>     content-length: 29\r\n
   s>     host: $LOCALIP:$HGPORT\r\n (glob)
   s>     \r\n
-  s>     \x0e\x00\x00\x01\x00\x01\x01\x11customreadonly
+  s>     \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
   s> makefile('rb', None)
   s>     HTTP/1.1 200 OK\r\n
   s>     Server: testing stub value\r\n
   s>     Date: $HTTP_DATE$\r\n
-  s>     Content-Type: application/mercurial-exp-framing-0002\r\n
+  s>     Content-Type: application/mercurial-exp-framing-0003\r\n
   s>     Transfer-Encoding: chunked\r\n
   s>     \r\n
   s>     25\r\n
@@ -290,23 +290,23 @@ 
   >     user-agent: test
   >     accept: $MEDIATYPE
   >     content-type: $MEDIATYPE
-  >     frame 1 1 stream-begin command-name eos customreadonly
+  >     frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
   > EOF
   using raw connection to peer
   s>     POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
   s>     Accept-Encoding: identity\r\n
-  s>     accept: application/mercurial-exp-framing-0002\r\n
-  s>     content-type: application/mercurial-exp-framing-0002\r\n
+  s>     accept: application/mercurial-exp-framing-0003\r\n
+  s>     content-type: application/mercurial-exp-framing-0003\r\n
   s>     user-agent: test\r\n
-  s>     content-length: 22\r\n
+  s>     content-length: 29\r\n
   s>     host: $LOCALIP:$HGPORT\r\n (glob)
   s>     \r\n
-  s>     \x0e\x00\x00\x01\x00\x01\x01\x11customreadonly
+  s>     \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
   s> makefile('rb', None)
   s>     HTTP/1.1 200 OK\r\n
   s>     Server: testing stub value\r\n
   s>     Date: $HTTP_DATE$\r\n
-  s>     Content-Type: application/mercurial-exp-framing-0002\r\n
+  s>     Content-Type: application/mercurial-exp-framing-0003\r\n
   s>     Transfer-Encoding: chunked\r\n
   s>     \r\n
   s>     25\r\n
@@ -325,7 +325,7 @@ 
   using raw connection to peer
   s>     POST /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
   s>     Accept-Encoding: identity\r\n
-  s>     accept: application/mercurial-exp-framing-0002\r\n
+  s>     accept: application/mercurial-exp-framing-0003\r\n
   s>     user-agent: test\r\n
   s>     host: $LOCALIP:$HGPORT\r\n (glob)
   s>     \r\n
@@ -382,32 +382,26 @@ 
   >     accept: $MEDIATYPE
   >     content-type: $MEDIATYPE
   >     user-agent: test
-  >     frame 1 1 stream-begin command-name have-args command1
-  >     frame 1 1 0 command-argument 0 \x03\x00\x04\x00fooval1
-  >     frame 1 1 0 command-argument eoa \x04\x00\x03\x00bar1val
+  >     frame 1 1 stream-begin command-request new cbor:{b'name': b'command1', b'args': {b'foo': b'val1', b'bar1': b'val'}}
   > EOF
   using raw connection to peer
   s>     POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
   s>     Accept-Encoding: identity\r\n
-  s>     accept: application/mercurial-exp-framing-0002\r\n
-  s>     content-type: application/mercurial-exp-framing-0002\r\n
+  s>     accept: application/mercurial-exp-framing-0003\r\n
+  s>     content-type: application/mercurial-exp-framing-0003\r\n
   s>     user-agent: test\r\n
-  s>     content-length: 54\r\n
+  s>     content-length: 47\r\n
   s>     host: $LOCALIP:$HGPORT\r\n (glob)
   s>     \r\n
-  s>     \x08\x00\x00\x01\x00\x01\x01\x12command1\x0b\x00\x00\x01\x00\x01\x00 \x03\x00\x04\x00fooval1\x0b\x00\x00\x01\x00\x01\x00"\x04\x00\x03\x00bar1val
+  s>     '\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa2CfooDval1Dbar1CvalDnameHcommand1
   s> makefile('rb', None)
   s>     HTTP/1.1 200 OK\r\n
   s>     Server: testing stub value\r\n
   s>     Date: $HTTP_DATE$\r\n
   s>     Content-Type: text/plain\r\n
-  s>     Content-Length: 322\r\n
+  s>     Content-Length: 205\r\n
   s>     \r\n
-  s>     received: 1 2 1 command1\n
-  s>     ["wantframe", {"state": "command-receiving"}]\n
-  s>     received: 2 0 1 \x03\x00\x04\x00fooval1\n
-  s>     ["wantframe", {"state": "command-receiving"}]\n
-  s>     received: 2 2 1 \x04\x00\x03\x00bar1val\n
+  s>     received: 1 1 1 \xa2Dargs\xa2CfooDval1Dbar1CvalDnameHcommand1\n
   s>     ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null, "requestid": 1}]\n
   s>     received: <no frame>\n
   s>     {"action": "noop"}
@@ -419,56 +413,59 @@ 
   >     accept: $MEDIATYPE
   >     content-type: $MEDIATYPE
   >     user-agent: test
-  >     frame 1 1 stream-begin command-name eos customreadonly
-  >     frame 3 1 0 command-name eos customreadonly
+  >     frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
   > EOF
   using raw connection to peer
   s>     POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
   s>     Accept-Encoding: identity\r\n
-  s>     accept: application/mercurial-exp-framing-0002\r\n
-  s>     content-type: application/mercurial-exp-framing-0002\r\n
+  s>     accept: application/mercurial-exp-framing-0003\r\n
+  s>     content-type: application/mercurial-exp-framing-0003\r\n
   s>     user-agent: test\r\n
-  s>     content-length: 44\r\n
+  s>     content-length: 29\r\n
   s>     host: $LOCALIP:$HGPORT\r\n (glob)
   s>     \r\n
-  s>     \x0e\x00\x00\x01\x00\x01\x01\x11customreadonly\x0e\x00\x00\x03\x00\x01\x00\x11customreadonly
+  s>     \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly
   s> makefile('rb', None)
   s>     HTTP/1.1 200 OK\r\n
   s>     Server: testing stub value\r\n
   s>     Date: $HTTP_DATE$\r\n
-  s>     Content-Type: text/plain\r\n
-  s>     Content-Length: 46\r\n
+  s>     Content-Type: application/mercurial-exp-framing-0003\r\n
+  s>     Transfer-Encoding: chunked\r\n
   s>     \r\n
-  s>     multiple commands cannot be issued to this URL
+  s>     25\r\n
+  s>     \x1d\x00\x00\x01\x00\x02\x01Bcustomreadonly bytes response
+  s>     \r\n
+  s>     0\r\n
+  s>     \r\n
 
 Multiple requests to "multirequest" URL are allowed
 
   $ send << EOF
   > httprequest POST api/$HTTPV2/ro/multirequest
   >     accept: $MEDIATYPE
   >     content-type: $MEDIATYPE
   >     user-agent: test
-  >     frame 1 1 stream-begin command-name eos customreadonly
-  >     frame 3 1 0 command-name eos customreadonly
+  >     frame 1 1 stream-begin command-request new cbor:{b'name': b'customreadonly'}
+  >     frame 3 1 0 command-request new cbor:{b'name': b'customreadonly'}
   > EOF
   using raw connection to peer
   s>     POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
   s>     Accept-Encoding: identity\r\n
-  s>     accept: application/mercurial-exp-framing-0002\r\n
-  s>     content-type: application/mercurial-exp-framing-0002\r\n
+  s>     *\r\n (glob)
+  s>     *\r\n (glob)
   s>     user-agent: test\r\n
-  s>     *\r\n (glob)
+  s>     content-length: 58\r\n
   s>     host: $LOCALIP:$HGPORT\r\n (glob)
   s>     \r\n
-  s>     \x0e\x00\x00\x01\x00\x01\x01\x11customreadonly\x0e\x00\x00\x03\x00\x01\x00\x11customreadonly
+  s>     \x15\x00\x00\x01\x00\x01\x01\x11\xa1DnameNcustomreadonly\x15\x00\x00\x03\x00\x01\x00\x11\xa1DnameNcustomreadonly
   s> makefile('rb', None)
   s>     HTTP/1.1 200 OK\r\n
   s>     Server: testing stub value\r\n
   s>     Date: $HTTP_DATE$\r\n
-  s>     Content-Type: application/mercurial-exp-framing-0002\r\n
+  s>     Content-Type: application/mercurial-exp-framing-0003\r\n
   s>     Transfer-Encoding: chunked\r\n
   s>     \r\n
-  s>     *\r\n (glob)
+  s>     25\r\n
   s>     \x1d\x00\x00\x01\x00\x02\x01Bcustomreadonly bytes response
   s>     \r\n
   s>     25\r\n
@@ -484,36 +481,35 @@ 
   >     accept: $MEDIATYPE
   >     content-type: $MEDIATYPE
   >     user-agent: test
-  >     frame 1 1 stream-begin command-name have-args listkeys
-  >     frame 3 1 0 command-name have-args listkeys
-  >     frame 3 1 0 command-argument eoa \x09\x00\x09\x00namespacebookmarks
-  >     frame 1 1 0 command-argument eoa \x09\x00\x0a\x00namespacenamespaces
+  >     frame 1 1 stream-begin command-request new|more \xa2Dargs\xa1Inamespace
+  >     frame 3 1 0 command-request new|more \xa2Dargs\xa1Inamespace
+  >     frame 3 1 0 command-request continuation JnamespacesDnameHlistkeys
+  >     frame 1 1 0 command-request continuation IbookmarksDnameHlistkeys
   > EOF
   using raw connection to peer
   s>     POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
   s>     Accept-Encoding: identity\r\n
-  s>     accept: application/mercurial-exp-framing-0002\r\n
-  s>     content-type: application/mercurial-exp-framing-0002\r\n
+  s>     accept: application/mercurial-exp-framing-0003\r\n
+  s>     content-type: application/mercurial-exp-framing-0003\r\n
   s>     user-agent: test\r\n
-  s>     content-length: 93\r\n
+  s>     content-length: 115\r\n
   s>     host: $LOCALIP:$HGPORT\r\n (glob)
   s>     \r\n
-  s>     \x08\x00\x00\x01\x00\x01\x01\x12listkeys\x08\x00\x00\x03\x00\x01\x00\x12listkeys\x16\x00\x00\x03\x00\x01\x00"	\x00	\x00namespacebookmarks\x17\x00\x00\x01\x00\x01\x00"	\x00\n
-  s>     \x00namespacenamespaces
+  s>     \x11\x00\x00\x01\x00\x01\x01\x15\xa2Dargs\xa1Inamespace\x11\x00\x00\x03\x00\x01\x00\x15\xa2Dargs\xa1Inamespace\x19\x00\x00\x03\x00\x01\x00\x12JnamespacesDnameHlistkeys\x18\x00\x00\x01\x00\x01\x00\x12IbookmarksDnameHlistkeys
   s> makefile('rb', None)
   s>     HTTP/1.1 200 OK\r\n
   s>     Server: testing stub value\r\n
   s>     Date: $HTTP_DATE$\r\n
-  s>     Content-Type: application/mercurial-exp-framing-0002\r\n
+  s>     Content-Type: application/mercurial-exp-framing-0003\r\n
   s>     Transfer-Encoding: chunked\r\n
   s>     \r\n
+  s>     26\r\n
+  s>     \x1e\x00\x00\x03\x00\x02\x01Bbookmarks	\n
+  s>     namespaces	\n
+  s>     phases	
+  s>     \r\n
   s>     8\r\n
-  s>     \x00\x00\x00\x03\x00\x02\x01B
-  s>     \r\n
-  s>     26\r\n
-  s>     \x1e\x00\x00\x01\x00\x02\x00Bbookmarks	\n
-  s>     namespaces	\n
-  s>     phases	
+  s>     \x00\x00\x00\x01\x00\x02\x00B
   s>     \r\n
   s>     0\r\n
   s>     \r\n
@@ -540,18 +536,18 @@ 
   >     accept: $MEDIATYPE
   >     content-type: $MEDIATYPE
   >     user-agent: test
-  >     frame 1 1 stream-begin command-name eos unbundle
+  >     frame 1 1 stream-begin command-request new cbor:{b'name': b'unbundle'}
   > EOF
   using raw connection to peer
   s>     POST /api/exp-http-v2-0001/ro/multirequest HTTP/1.1\r\n
   s>     Accept-Encoding: identity\r\n
-  s>     accept: application/mercurial-exp-framing-0002\r\n
-  s>     content-type: application/mercurial-exp-framing-0002\r\n
+  s>     accept: application/mercurial-exp-framing-0003\r\n
+  s>     content-type: application/mercurial-exp-framing-0003\r\n
   s>     user-agent: test\r\n
-  s>     content-length: 16\r\n
+  s>     content-length: 23\r\n
   s>     host: $LOCALIP:$HGPORT\r\n (glob)
   s>     \r\n
-  s>     \x08\x00\x00\x01\x00\x01\x01\x11unbundle
+  s>     \x0f\x00\x00\x01\x00\x01\x01\x11\xa1DnameHunbundle
   s> makefile('rb', None)
   s>     HTTP/1.1 403 Forbidden\r\n
   s>     Server: testing stub value\r\n
diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py
--- a/mercurial/wireprotoserver.py
+++ b/mercurial/wireprotoserver.py
@@ -36,7 +36,7 @@ 
 HGTYPE = 'application/mercurial-0.1'
 HGTYPE2 = 'application/mercurial-0.2'
 HGERRTYPE = 'application/hg-error'
-FRAMINGTYPE = b'application/mercurial-exp-framing-0002'
+FRAMINGTYPE = b'application/mercurial-exp-framing-0003'
 
 HTTPV2 = wireprototypes.HTTPV2
 SSHV1 = wireprototypes.SSHV1
diff --git a/mercurial/wireprotoframing.py b/mercurial/wireprotoframing.py
--- a/mercurial/wireprotoframing.py
+++ b/mercurial/wireprotoframing.py
@@ -39,42 +39,34 @@ 
     b'encoded': STREAM_FLAG_ENCODING_APPLIED,
 }
 
-FRAME_TYPE_COMMAND_NAME = 0x01
-FRAME_TYPE_COMMAND_ARGUMENT = 0x02
+FRAME_TYPE_COMMAND_REQUEST = 0x01
 FRAME_TYPE_COMMAND_DATA = 0x03
 FRAME_TYPE_BYTES_RESPONSE = 0x04
 FRAME_TYPE_ERROR_RESPONSE = 0x05
 FRAME_TYPE_TEXT_OUTPUT = 0x06
 FRAME_TYPE_PROGRESS = 0x07
 FRAME_TYPE_STREAM_SETTINGS = 0x08
 
 FRAME_TYPES = {
-    b'command-name': FRAME_TYPE_COMMAND_NAME,
-    b'command-argument': FRAME_TYPE_COMMAND_ARGUMENT,
+    b'command-request': FRAME_TYPE_COMMAND_REQUEST,
     b'command-data': FRAME_TYPE_COMMAND_DATA,
     b'bytes-response': FRAME_TYPE_BYTES_RESPONSE,
     b'error-response': FRAME_TYPE_ERROR_RESPONSE,
     b'text-output': FRAME_TYPE_TEXT_OUTPUT,
     b'progress': FRAME_TYPE_PROGRESS,
     b'stream-settings': FRAME_TYPE_STREAM_SETTINGS,
 }
 
-FLAG_COMMAND_NAME_EOS = 0x01
-FLAG_COMMAND_NAME_HAVE_ARGS = 0x02
-FLAG_COMMAND_NAME_HAVE_DATA = 0x04
+FLAG_COMMAND_REQUEST_NEW = 0x01
+FLAG_COMMAND_REQUEST_CONTINUATION = 0x02
+FLAG_COMMAND_REQUEST_MORE_FRAMES = 0x04
+FLAG_COMMAND_REQUEST_EXPECT_DATA = 0x08
 
-FLAGS_COMMAND = {
-    b'eos': FLAG_COMMAND_NAME_EOS,
-    b'have-args': FLAG_COMMAND_NAME_HAVE_ARGS,
-    b'have-data': FLAG_COMMAND_NAME_HAVE_DATA,
-}
-
-FLAG_COMMAND_ARGUMENT_CONTINUATION = 0x01
-FLAG_COMMAND_ARGUMENT_EOA = 0x02
-
-FLAGS_COMMAND_ARGUMENT = {
-    b'continuation': FLAG_COMMAND_ARGUMENT_CONTINUATION,
-    b'eoa': FLAG_COMMAND_ARGUMENT_EOA,
+FLAGS_COMMAND_REQUEST = {
+    b'new': FLAG_COMMAND_REQUEST_NEW,
+    b'continuation': FLAG_COMMAND_REQUEST_CONTINUATION,
+    b'more': FLAG_COMMAND_REQUEST_MORE_FRAMES,
+    b'have-data': FLAG_COMMAND_REQUEST_EXPECT_DATA,
 }
 
 FLAG_COMMAND_DATA_CONTINUATION = 0x01
@@ -103,17 +95,16 @@ 
 
 # Maps frame types to their available flags.
 FRAME_TYPE_FLAGS = {
-    FRAME_TYPE_COMMAND_NAME: FLAGS_COMMAND,
-    FRAME_TYPE_COMMAND_ARGUMENT: FLAGS_COMMAND_ARGUMENT,
+    FRAME_TYPE_COMMAND_REQUEST: FLAGS_COMMAND_REQUEST,
     FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA,
     FRAME_TYPE_BYTES_RESPONSE: FLAGS_BYTES_RESPONSE,
     FRAME_TYPE_ERROR_RESPONSE: FLAGS_ERROR_RESPONSE,
     FRAME_TYPE_TEXT_OUTPUT: {},
     FRAME_TYPE_PROGRESS: {},
     FRAME_TYPE_STREAM_SETTINGS: {},
 }
 
-ARGUMENT_FRAME_HEADER = struct.Struct(r'<HH')
+ARGUMENT_RECORD_HEADER = struct.Struct(r'<HH')
 
 @attr.s(slots=True)
 class frameheader(object):
@@ -269,43 +260,48 @@ 
     return frame(h.requestid, h.streamid, h.streamflags, h.typeid, h.flags,
                  payload)
 
-def createcommandframes(stream, requestid, cmd, args, datafh=None):
+def createcommandframes(stream, requestid, cmd, args, datafh=None,
+                        maxframesize=DEFAULT_MAX_FRAME_SIZE):
     """Create frames necessary to transmit a request to run a command.
 
     This is a generator of bytearrays. Each item represents a frame
     ready to be sent over the wire to a peer.
     """
-    flags = 0
+    data = {b'name': cmd}
     if args:
-        flags |= FLAG_COMMAND_NAME_HAVE_ARGS
-    if datafh:
-        flags |= FLAG_COMMAND_NAME_HAVE_DATA
+        data[b'args'] = args
 
-    if not flags:
-        flags |= FLAG_COMMAND_NAME_EOS
+    data = cbor.dumps(data, canonical=True)
 
-    yield stream.makeframe(requestid=requestid, typeid=FRAME_TYPE_COMMAND_NAME,
-                           flags=flags, payload=cmd)
+    offset = 0
+
+    while True:
+        flags = 0
 
-    for i, k in enumerate(sorted(args)):
-        v = args[k]
-        last = i == len(args) - 1
+        # Must set new or continuation flag.
+        if not offset:
+            flags |= FLAG_COMMAND_REQUEST_NEW
+        else:
+            flags |= FLAG_COMMAND_REQUEST_CONTINUATION
 
-        # TODO handle splitting of argument values across frames.
-        payload = bytearray(ARGUMENT_FRAME_HEADER.size + len(k) + len(v))
-        offset = 0
-        ARGUMENT_FRAME_HEADER.pack_into(payload, offset, len(k), len(v))
-        offset += ARGUMENT_FRAME_HEADER.size
-        payload[offset:offset + len(k)] = k
-        offset += len(k)
-        payload[offset:offset + len(v)] = v
+        # Data frames is set on all frames.
+        if datafh:
+            flags |= FLAG_COMMAND_REQUEST_EXPECT_DATA
 
-        flags = FLAG_COMMAND_ARGUMENT_EOA if last else 0
+        payload = data[offset:offset + maxframesize]
+        offset += len(payload)
+
+        if len(payload) == maxframesize and offset < len(data):
+            flags |= FLAG_COMMAND_REQUEST_MORE_FRAMES
+
         yield stream.makeframe(requestid=requestid,
-                               typeid=FRAME_TYPE_COMMAND_ARGUMENT,
+                               typeid=FRAME_TYPE_COMMAND_REQUEST,
                                flags=flags,
                                payload=payload)
 
+        if not (flags & FLAG_COMMAND_REQUEST_MORE_FRAMES):
+            break
+
     if datafh:
         while True:
             data = datafh.read(DEFAULT_MAX_FRAME_SIZE)
@@ -673,72 +669,120 @@ 
 
     def _makeruncommandresult(self, requestid):
         entry = self._receivingcommands[requestid]
+
+        if not entry['requestdone']:
+            self._state = 'errored'
+            raise error.ProgrammingError('should not be called without '
+                                         'requestdone set')
+
         del self._receivingcommands[requestid]
 
         if self._receivingcommands:
             self._state = 'command-receiving'
         else:
             self._state = 'idle'
 
+        # Decode the payloads as CBOR.
+        entry['payload'].seek(0)
+        request = cbor.load(entry['payload'])
+
+        if b'name' not in request:
+            self._state = 'errored'
+            return self._makeerrorresult(
+                _('command request missing "name" field'))
+
+        if b'args' not in request:
+            request[b'args'] = {}
+
         assert requestid not in self._activecommands
         self._activecommands.add(requestid)
 
         return 'runcommand', {
             'requestid': requestid,
-            'command': entry['command'],
-            'args': entry['args'],
+            'command': request[b'name'],
+            'args': request[b'args'],
             'data': entry['data'].getvalue() if entry['data'] else None,
         }
 
     def _makewantframeresult(self):
         return 'wantframe', {
             'state': self._state,
         }
 
+    def _validatecommandrequestframe(self, frame):
+        new = frame.flags & FLAG_COMMAND_REQUEST_NEW
+        continuation = frame.flags & FLAG_COMMAND_REQUEST_CONTINUATION
+
+        if new and continuation:
+            self._state = 'errored'
+            return self._makeerrorresult(
+                _('received command request frame with both new and '
+                  'continuation flags set'))
+
+        if not new and not continuation:
+            self._state = 'errored'
+            return self._makeerrorresult(
+                _('received command request frame with neither new nor '
+                  'continuation flags set'))
+
     def _onframeidle(self, frame):
         # The only frame type that should be received in this state is a
         # command request.
-        if frame.typeid != FRAME_TYPE_COMMAND_NAME:
+        if frame.typeid != FRAME_TYPE_COMMAND_REQUEST:
             self._state = 'errored'
             return self._makeerrorresult(
-                _('expected command frame; got %d') % frame.typeid)
+                _('expected command request frame; got %d') % frame.typeid)
+
+        res = self._validatecommandrequestframe(frame)
+        if res:
+            return res
 
         if frame.requestid in self._receivingcommands:
             self._state = 'errored'
             return self._makeerrorresult(
                 _('request with ID %d already received') % frame.requestid)
 
         if frame.requestid in self._activecommands:
             self._state = 'errored'
-            return self._makeerrorresult((
-                _('request with ID %d is already active') % frame.requestid))
+            return self._makeerrorresult(
+                _('request with ID %d is already active') % frame.requestid)
+
+        new = frame.flags & FLAG_COMMAND_REQUEST_NEW
+        moreframes = frame.flags & FLAG_COMMAND_REQUEST_MORE_FRAMES
+        expectingdata = frame.flags & FLAG_COMMAND_REQUEST_EXPECT_DATA
 
-        expectingargs = bool(frame.flags & FLAG_COMMAND_NAME_HAVE_ARGS)
-        expectingdata = bool(frame.flags & FLAG_COMMAND_NAME_HAVE_DATA)
+        if not new:
+            self._state = 'errored'
+            return self._makeerrorresult(
+                _('received command request frame without new flag set'))
+
+        payload = util.bytesio()
+        payload.write(frame.payload)
 
         self._receivingcommands[frame.requestid] = {
-            'command': frame.payload,
-            'args': {},
+            'payload': payload,
             'data': None,
-            'expectingargs': expectingargs,
-            'expectingdata': expectingdata,
+            'requestdone': not moreframes,
+            'expectingdata': bool(expectingdata),
         }
 
-        if frame.flags & FLAG_COMMAND_NAME_EOS:
+        # This is the final frame for this request. Dispatch it.
+        if not moreframes and not expectingdata:
             return self._makeruncommandresult(frame.requestid)
 
-        if expectingargs or expectingdata:
-            self._state = 'command-receiving'
-            return self._makewantframeresult()
-        else:
-            self._state = 'errored'
-            return self._makeerrorresult(_('missing frame flags on '
-                                           'command frame'))
+        assert moreframes or expectingdata
+        self._state = 'command-receiving'
+        return self._makewantframeresult()
 
     def _onframecommandreceiving(self, frame):
-        # It could be a new command request. Process it as such.
-        if frame.typeid == FRAME_TYPE_COMMAND_NAME:
-            return self._onframeidle(frame)
+        if frame.typeid == FRAME_TYPE_COMMAND_REQUEST:
+            # Process new command requests as such.
+            if frame.flags & FLAG_COMMAND_REQUEST_NEW:
+                return self._onframeidle(frame)
+
+            res = self._validatecommandrequestframe(frame)
+            if res:
+                return res
 
         # All other frames should be related to a command that is currently
         # receiving but is not active.
@@ -756,14 +800,30 @@ 
 
         entry = self._receivingcommands[frame.requestid]
 
-        if frame.typeid == FRAME_TYPE_COMMAND_ARGUMENT:
-            if not entry['expectingargs']:
+        if frame.typeid == FRAME_TYPE_COMMAND_REQUEST:
+            moreframes = frame.flags & FLAG_COMMAND_REQUEST_MORE_FRAMES
+            expectingdata = bool(frame.flags & FLAG_COMMAND_REQUEST_EXPECT_DATA)
+
+            if entry['requestdone']:
+                self._state = 'errored'
+                return self._makeerrorresult(
+                    _('received command request frame when request frames '
+                      'were supposedly done'))
+
+            if expectingdata != entry['expectingdata']:
                 self._state = 'errored'
-                return self._makeerrorresult(_(
-                    'received command argument frame for request that is not '
-                    'expecting arguments: %d') % frame.requestid)
+                return self._makeerrorresult(
+                    _('mismatch between expect data flag and previous frame'))
+
+            entry['payload'].write(frame.payload)
 
-            return self._handlecommandargsframe(frame, entry)
+            if not moreframes:
+                entry['requestdone'] = True
+
+            if not moreframes and not expectingdata:
+                return self._makeruncommandresult(frame.requestid)
+
+            return self._makewantframeresult()
 
         elif frame.typeid == FRAME_TYPE_COMMAND_DATA:
             if not entry['expectingdata']:
@@ -776,50 +836,10 @@ 
                 entry['data'] = util.bytesio()
 
             return self._handlecommanddataframe(frame, entry)
-
-    def _handlecommandargsframe(self, frame, entry):
-        # The frame and state of command should have already been validated.
-        assert frame.typeid == FRAME_TYPE_COMMAND_ARGUMENT
-
-        offset = 0
-        namesize, valuesize = ARGUMENT_FRAME_HEADER.unpack_from(frame.payload)
-        offset += ARGUMENT_FRAME_HEADER.size
-
-        # The argument name MUST fit inside the frame.
-        argname = bytes(frame.payload[offset:offset + namesize])
-        offset += namesize
-
-        if len(argname) != namesize:
+        else:
             self._state = 'errored'
-            return self._makeerrorresult(_('malformed argument frame: '
-                                           'partial argument name'))
-
-        argvalue = bytes(frame.payload[offset:])
-
-        # Argument value spans multiple frames. Record our active state
-        # and wait for the next frame.
-        if frame.flags & FLAG_COMMAND_ARGUMENT_CONTINUATION:
-            raise error.ProgrammingError('not yet implemented')
-
-        # Common case: the argument value is completely contained in this
-        # frame.
-
-        if len(argvalue) != valuesize:
-            self._state = 'errored'
-            return self._makeerrorresult(_('malformed argument frame: '
-                                           'partial argument value'))
-
-        entry['args'][argname] = argvalue
-
-        if frame.flags & FLAG_COMMAND_ARGUMENT_EOA:
-            if entry['expectingdata']:
-                # TODO signal request to run a command once we don't
-                # buffer data frames.
-                return self._makewantframeresult()
-            else:
-                return self._makeruncommandresult(frame.requestid)
-        else:
-            return self._makewantframeresult()
+            return self._makeerrorresult(_(
+                'received unexpected frame type: %d') % frame.typeid)
 
     def _handlecommanddataframe(self, frame, entry):
         assert frame.typeid == FRAME_TYPE_COMMAND_DATA
diff --git a/mercurial/help/internals/wireprotocol.txt b/mercurial/help/internals/wireprotocol.txt
--- a/mercurial/help/internals/wireprotocol.txt
+++ b/mercurial/help/internals/wireprotocol.txt
@@ -561,63 +561,48 @@ 
 
 This frame contains a request to run a command.
 
-The name of the command to run constitutes the entirety of the frame
-payload.
+The payload consists of a CBOR map defining the command request. The
+bytestring keys of that map are:
+
+name
+   Name of the command that should be executed (bytestring).
+args
+   Map of bytestring keys to various value types containing the named
+   arguments to this command.
+
+   Each command defines its own set of argument names and their expected
+   types.
 
 This frame type MUST ONLY be sent from clients to servers: it is illegal
 for a server to send this frame to a client.
 
 The following flag values are defined for this type:
 
 0x01
-   End of command data. When set, the client will not send any command
-   arguments or additional command data. When set, the command has been
-   fully issued and the server has the full context to process the command.
-   The next frame issued by the client is not part of this command.
+   New command request. When set, this frame represents the beginning
+   of a new request to run a command. The ``Request ID`` attached to this
+   frame MUST NOT be active.
 0x02
-   Command argument frames expected. When set, the client will send
-   *Command Argument* frames containing command argument data.
+   Command request continuation. When set, this frame is a continuation
+   from a previous command request frame for its ``Request ID``. This
+   flag is set when the CBOR data for a command request does not fit
+   in a single frame.
 0x04
-   Command data frames expected. When set, the client will send
-   *Command Data* frames containing a raw stream of data for this
-   command.
-
-The ``0x01`` flag is mutually exclusive with both the ``0x02`` and ``0x04``
-flags.
-
-Command Argument (``0x02``)
----------------------------
-
-This frame contains a named argument for a command.
-
-The frame type MUST ONLY be sent from clients to servers: it is illegal
-for a server to send this frame to a client.
+   Additional frames expected. When set, the command request didn't fit
+   into a single frame and additional CBOR data follows in a subsequent
+   frame.
+0x08
+   Command data frames expected. When set, command data frames are
+   expected to follow the final command request frame for this request.
 
-The payload consists of:
-
-* A 16-bit little endian integer denoting the length of the
-  argument name.
-* A 16-bit little endian integer denoting the length of the
-  argument value.
-* N bytes of ASCII data containing the argument name.
-* N bytes of binary data containing the argument value.
-
-The payload MUST hold the entirety of the 32-bit header and the
-argument name. The argument value MAY span multiple frames. If this
-occurs, the appropriate frame flag should be set to indicate this.
+``0x01`` MUST be set on the initial command request frame for a
+``Request ID``.
 
-The following flag values are defined for this type:
+``0x01`` or ``0x02`` MUST be set to indicate this frame's role in
+a series of command request frames.
 
-0x01
-   Argument data continuation. When set, the data for this argument did
-   not fit in a single frame and the next frame will contain additional
-   argument data.
-
-0x02
-   End of arguments data. When set, the client will not send any more
-   command arguments for the command this frame is associated with.
-   The next frame issued by the client will be command data or
-   belong to a separate request.
+If command data frames are to be sent, ``0x10`` MUST be set on ALL
+command request frames.
 
 Command Data (``0x03``)
 -----------------------
@@ -903,8 +888,8 @@ 
 
 A client can request that a remote run a command by sending it
 frames defining that command. This logical stream is composed of
-1 ``Command Request`` frame, 0 or more ``Command Argument`` frames,
-and 0 or more ``Command Data`` frames.
+1 or more ``Command Request`` frames and and 0 or more ``Command Data``
+frames.
 
 All frames composing a single command request MUST be associated with
 the same ``Request ID``.
@@ -928,14 +913,17 @@ 
 TODO think about whether we should express dependencies between commands
 to avoid roundtrip latency.
 
-Argument frames are the recommended mechanism for transferring fixed
-sets of parameters to a command. Data frames are appropriate for
-transferring variable data. A similar comparison would be to HTTP:
-argument frames are headers and the message body is data frames.
+A command is defined by a command name, 0 or more command arguments,
+and optional command data.
+
+Arguments are the recommended mechanism for transferring fixed sets of
+parameters to a command. Data is appropriate for transferring variable
+data. Thinking in terms of HTTP, arguments would be headers and data
+would be the message body.
 
 It is recommended for servers to delay the dispatch of a command
-until all argument frames for that command have been received. Servers
-MAY impose limits on the maximum argument size.
+until all argument have been received. Servers MAY impose limits on the
+maximum argument size.
 TODO define failure mechanism.
 
 Servers MAY dispatch to commands immediately once argument data