pw_transfer: Add separate session_id to chunk

This partially reverts pigweed:89260 by renaming field number 1 in the
chunk proto back to transfer_id. The session_id field is introduced as
an entirely new field, so that its presence can be used to detect the
protocol version that a transfer is running.

Change-Id: I9765edc6fa5a263de1366176c2e08e8e5a1bd0f0
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/94063
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Alexei Frolov <frolv@google.com>
diff --git a/pw_transfer/chunk.cc b/pw_transfer/chunk.cc
index 8ccea66..5a018a1 100644
--- a/pw_transfer/chunk.cc
+++ b/pw_transfer/chunk.cc
@@ -16,6 +16,7 @@
 
 #include "pw_assert/check.h"
 #include "pw_protobuf/decoder.h"
+#include "pw_protobuf/serialized_size.h"
 #include "pw_status/try.h"
 #include "pw_transfer/transfer.pwpb.h"
 
@@ -26,22 +27,35 @@
 Result<uint32_t> Chunk::ExtractSessionId(ConstByteSpan message) {
   protobuf::Decoder decoder(message);
 
+  uint32_t session_id = 0;
+
   while (decoder.Next().ok()) {
     ProtoChunk::Fields field =
         static_cast<ProtoChunk::Fields>(decoder.FieldNumber());
 
     switch (field) {
-      case ProtoChunk::Fields::SESSION_ID: {
-        uint32_t session_id;
+      case ProtoChunk::Fields::TRANSFER_ID:
+        // Interpret a legacy transfer_id field as a session ID, but don't
+        // return immediately. Instead, check to see if the message also
+        // contains a newer session_id field.
+        PW_TRY(decoder.ReadUint32(&session_id));
+        break;
+
+      case ProtoChunk::Fields::SESSION_ID:
+        // A session_id field always takes precedence over transfer_id, so
+        // return it immediately when encountered.
         PW_TRY(decoder.ReadUint32(&session_id));
         return session_id;
-      }
 
       default:
         continue;
     }
   }
 
+  if (session_id != 0) {
+    return session_id;
+  }
+
   return Status::DataLoss();
 }
 
@@ -68,7 +82,19 @@
         static_cast<ProtoChunk::Fields>(decoder.FieldNumber());
 
     switch (field) {
+      case ProtoChunk::Fields::TRANSFER_ID:
+        // transfer_id is a legacy field. session_id will always take precedence
+        // over it, so it should only be read if session_id has not yet been
+        // encountered.
+        if (chunk.session_id_ == 0) {
+          PW_TRY(decoder.ReadUint32(&chunk.session_id_));
+        }
+        break;
+
       case ProtoChunk::Fields::SESSION_ID:
+        // The existence of a session_id field indicates that a newer protocol
+        // is running.
+        chunk.protocol_version_ = ProtocolVersion::kVersionTwo;
         PW_TRY(decoder.ReadUint32(&chunk.session_id_));
         break;
 
@@ -149,7 +175,21 @@
 
   ProtoChunk::MemoryEncoder encoder(buffer);
 
-  encoder.WriteSessionId(session_id_).IgnoreError();
+  // Write the payload first to avoid clobbering it if it shares the same buffer
+  // as the encode buffer.
+  if (has_payload()) {
+    encoder.WriteData(payload_).IgnoreError();
+  }
+
+  if (protocol_version_ >= ProtocolVersion::kVersionTwo) {
+    if (session_id_ != 0) {
+      encoder.WriteSessionId(session_id_).IgnoreError();
+    }
+
+    if (resource_id_.has_value()) {
+      encoder.WriteResourceId(resource_id_.value()).IgnoreError();
+    }
+  }
 
   if (type_.has_value()) {
     encoder.WriteType(static_cast<ProtoChunk::Type>(type_.value()))
@@ -160,7 +200,11 @@
     encoder.WriteWindowEndOffset(window_end_offset_).IgnoreError();
   }
 
-  if (protocol_version_ == ProtocolVersion::kLegacy) {
+  // Encode additional fields from the legacy protocol.
+  if (ShouldEncodeLegacyFields()) {
+    // The legacy protocol uses the transfer_id field instead of session_id.
+    encoder.WriteTransferId(session_id_).IgnoreError();
+
     // In the legacy protocol, the pending_bytes field must be set alongside
     // window_end_offset, as some transfer implementations require it.
     encoder.WritePendingBytes(window_end_offset_ - offset_).IgnoreError();
@@ -178,10 +222,6 @@
     encoder.WriteOffset(offset_).IgnoreError();
   }
 
-  if (has_payload()) {
-    encoder.WriteData(payload_).IgnoreError();
-  }
-
   if (remaining_bytes_.has_value()) {
     encoder.WriteRemainingBytes(remaining_bytes_.value()).IgnoreError();
   }
@@ -190,14 +230,79 @@
     encoder.WriteStatus(status_.value().code()).IgnoreError();
   }
 
-  if (resource_id_.has_value()) {
-    encoder.WriteResourceId(resource_id_.value()).IgnoreError();
-  }
-
   PW_TRY(encoder.status());
   return ConstByteSpan(encoder);
 }
 
-Status DecodeChunk(ConstByteSpan, Chunk&) { return Status::Unimplemented(); }
+size_t Chunk::EncodedSize() const {
+  size_t size = 0;
+
+  if (session_id_ != 0) {
+    if (protocol_version_ >= ProtocolVersion::kVersionTwo) {
+      size += protobuf::SizeOfVarintField(ProtoChunk::Fields::SESSION_ID,
+                                          session_id_);
+    }
+
+    if (ShouldEncodeLegacyFields()) {
+      size += protobuf::SizeOfVarintField(ProtoChunk::Fields::TRANSFER_ID,
+                                          session_id_);
+    }
+  }
+
+  if (protocol_version_ >= ProtocolVersion::kVersionTwo) {
+    if (resource_id_.has_value()) {
+      size += protobuf::SizeOfVarintField(ProtoChunk::Fields::RESOURCE_ID,
+                                          resource_id_.value());
+    }
+  }
+
+  if (offset_ != 0) {
+    size += protobuf::SizeOfVarintField(ProtoChunk::Fields::OFFSET, offset_);
+  }
+
+  if (window_end_offset_ != 0) {
+    size += protobuf::SizeOfVarintField(ProtoChunk::Fields::WINDOW_END_OFFSET,
+                                        window_end_offset_);
+
+    if (ShouldEncodeLegacyFields()) {
+      size += protobuf::SizeOfVarintField(ProtoChunk::Fields::PENDING_BYTES,
+                                          window_end_offset_ - offset_);
+    }
+  }
+
+  if (type_.has_value()) {
+    size += protobuf::SizeOfVarintField(ProtoChunk::Fields::TYPE,
+                                        static_cast<uint32_t>(type_.value()));
+  }
+
+  if (has_payload()) {
+    size += protobuf::SizeOfDelimitedField(ProtoChunk::Fields::DATA,
+                                           payload_.size());
+  }
+
+  if (max_chunk_size_bytes_.has_value()) {
+    size +=
+        protobuf::SizeOfVarintField(ProtoChunk::Fields::MAX_CHUNK_SIZE_BYTES,
+                                    max_chunk_size_bytes_.value());
+  }
+
+  if (min_delay_microseconds_.has_value()) {
+    size +=
+        protobuf::SizeOfVarintField(ProtoChunk::Fields::MIN_DELAY_MICROSECONDS,
+                                    min_delay_microseconds_.value());
+  }
+
+  if (remaining_bytes_.has_value()) {
+    size += protobuf::SizeOfVarintField(ProtoChunk::Fields::REMAINING_BYTES,
+                                        remaining_bytes_.value());
+  }
+
+  if (status_.has_value()) {
+    size += protobuf::SizeOfVarintField(ProtoChunk::Fields::STATUS,
+                                        status_.value().code());
+  }
+
+  return size;
+}
 
 }  // namespace pw::transfer::internal
diff --git a/pw_transfer/client_test.cc b/pw_transfer/client_test.cc
index 8ea1a43..ab57e8a 100644
--- a/pw_transfer/client_test.cc
+++ b/pw_transfer/client_test.cc
@@ -90,7 +90,7 @@
   EXPECT_EQ(c0.session_id(), 3u);
   EXPECT_EQ(c0.offset(), 0u);
   EXPECT_EQ(c0.window_end_offset(), 64u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   context_.server().SendServerStream<Transfer::Read>(
       EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kTransferData)
@@ -133,7 +133,7 @@
   EXPECT_EQ(c0.session_id(), 4u);
   EXPECT_EQ(c0.offset(), 0u);
   EXPECT_EQ(c0.window_end_offset(), 64u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   constexpr ConstByteSpan data(kData32);
   context_.server().SendServerStream<Transfer::Read>(
@@ -222,7 +222,7 @@
   EXPECT_EQ(c0.session_id(), 5u);
   EXPECT_EQ(c0.offset(), 0u);
   EXPECT_EQ(c0.window_end_offset(), 32u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 }
 
 TEST_F(ReadTransferMaxBytes32, SetsPendingBytesFromWriterLimit) {
@@ -239,7 +239,7 @@
   EXPECT_EQ(c0.session_id(), 5u);
   EXPECT_EQ(c0.offset(), 0u);
   EXPECT_EQ(c0.window_end_offset(), 16u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 }
 
 TEST_F(ReadTransferMaxBytes32, MultiParameters) {
@@ -639,7 +639,7 @@
   EXPECT_EQ(c0.session_id(), 12u);
   EXPECT_EQ(c0.offset(), 0u);
   EXPECT_EQ(c0.window_end_offset(), 64u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Wait for the timeout to expire without doing anything. The client should
   // resend its initial parameters chunk.
@@ -650,7 +650,7 @@
   EXPECT_EQ(c.session_id(), 12u);
   EXPECT_EQ(c.offset(), 0u);
   EXPECT_EQ(c.window_end_offset(), 64u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Transfer has not yet completed.
   EXPECT_EQ(transfer_status, Status::Unknown());
@@ -696,7 +696,7 @@
   EXPECT_EQ(c0.session_id(), 13u);
   EXPECT_EQ(c0.offset(), 0u);
   EXPECT_EQ(c0.window_end_offset(), 64u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   constexpr ConstByteSpan data(kData32);
 
@@ -719,7 +719,7 @@
   EXPECT_EQ(c.session_id(), 13u);
   EXPECT_EQ(c.offset(), 16u);
   EXPECT_EQ(c.window_end_offset(), 64u);
-  EXPECT_EQ(c.legacy_type(), Chunk::Type::kParametersRetransmit);
+  EXPECT_EQ(c.type(), Chunk::Type::kParametersRetransmit);
 
   // Transfer has not yet completed.
   EXPECT_EQ(transfer_status, Status::Unknown());
@@ -765,7 +765,7 @@
   EXPECT_EQ(c0.session_id(), 14u);
   EXPECT_EQ(c0.offset(), 0u);
   EXPECT_EQ(c0.window_end_offset(), 64u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   for (unsigned retry = 1; retry <= kTestRetries; ++retry) {
     // Wait for the timeout to expire without doing anything. The client should
@@ -789,7 +789,7 @@
 
   Chunk c4 = DecodeChunk(payloads.back());
   EXPECT_EQ(c4.session_id(), 14u);
-  EXPECT_EQ(c4.legacy_type(), Chunk::Type::kTransferCompletion);
+  EXPECT_EQ(c4.type(), Chunk::Type::kTransferCompletion);
   ASSERT_TRUE(c4.status().has_value());
   EXPECT_EQ(c4.status().value(), Status::DeadlineExceeded());
 
@@ -938,7 +938,7 @@
   Chunk c0 = DecodeChunk(payloads[0]);
   EXPECT_EQ(c0.session_id(), 3u);
   EXPECT_EQ(c0.resource_id(), 3u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Send transfer parameters. Client should send a data chunk and the final
   // chunk.
@@ -995,7 +995,7 @@
   Chunk c0 = DecodeChunk(payloads[0]);
   EXPECT_EQ(c0.session_id(), 4u);
   EXPECT_EQ(c0.resource_id(), 4u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Send transfer parameters with a chunk size smaller than the data.
 
@@ -1062,7 +1062,7 @@
   Chunk c0 = DecodeChunk(payloads[0]);
   EXPECT_EQ(c0.session_id(), 5u);
   EXPECT_EQ(c0.resource_id(), 5u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Send transfer parameters with a nonzero offset, requesting a seek.
   // Client should send a data chunk and the final chunk.
@@ -1142,7 +1142,7 @@
   Chunk c0 = DecodeChunk(payloads[0]);
   EXPECT_EQ(c0.session_id(), 6u);
   EXPECT_EQ(c0.resource_id(), 6u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Send transfer parameters with a nonzero offset, requesting a seek.
   context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
@@ -1158,7 +1158,7 @@
 
   Chunk c1 = DecodeChunk(payloads[1]);
   EXPECT_EQ(c1.session_id(), 6u);
-  EXPECT_EQ(c1.legacy_type(), Chunk::Type::kTransferCompletion);
+  EXPECT_EQ(c1.type(), Chunk::Type::kTransferCompletion);
   ASSERT_TRUE(c1.status().has_value());
   EXPECT_EQ(c1.status().value(), Status::Unimplemented());
 
@@ -1184,7 +1184,7 @@
   Chunk c0 = DecodeChunk(payloads[0]);
   EXPECT_EQ(c0.session_id(), 7u);
   EXPECT_EQ(c0.resource_id(), 7u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Send an error from the server.
   context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
@@ -1215,7 +1215,7 @@
   Chunk c0 = DecodeChunk(payloads[0]);
   EXPECT_EQ(c0.session_id(), 9u);
   EXPECT_EQ(c0.resource_id(), 9u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Send an invalid transfer parameters chunk with 0 pending bytes.
   context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
@@ -1258,7 +1258,7 @@
   Chunk c0 = DecodeChunk(payloads.back());
   EXPECT_EQ(c0.session_id(), 10u);
   EXPECT_EQ(c0.resource_id(), 10u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Wait for the timeout to expire without doing anything. The client should
   // resend the initial transmit chunk.
@@ -1268,7 +1268,7 @@
   Chunk c = DecodeChunk(payloads.back());
   EXPECT_EQ(c.session_id(), 10u);
   EXPECT_EQ(c.resource_id(), 10u);
-  EXPECT_EQ(c.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c.type(), Chunk::Type::kTransferStart);
 
   // Transfer has not yet completed.
   EXPECT_EQ(transfer_status, Status::Unknown());
@@ -1299,7 +1299,7 @@
   Chunk c0 = DecodeChunk(payloads.back());
   EXPECT_EQ(c0.session_id(), 11u);
   EXPECT_EQ(c0.resource_id(), 11u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Send the first parameters chunk.
   rpc::test::WaitForPackets(context_.output(), 2, [this] {
@@ -1372,7 +1372,7 @@
   Chunk c0 = DecodeChunk(payloads.back());
   EXPECT_EQ(c0.session_id(), 12u);
   EXPECT_EQ(c0.resource_id(), 12u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Send the first parameters chunk, requesting all the data. The client should
   // respond with one data chunk and a remaining_bytes = 0 chunk.
@@ -1456,7 +1456,7 @@
   Chunk c0 = DecodeChunk(payloads.back());
   EXPECT_EQ(c0.session_id(), 13u);
   EXPECT_EQ(c0.resource_id(), 13u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   for (unsigned retry = 1; retry <= kTestRetries; ++retry) {
     // Wait for the timeout to expire without doing anything. The client should
@@ -1467,7 +1467,7 @@
     Chunk c = DecodeChunk(payloads.back());
     EXPECT_EQ(c.session_id(), 13u);
     EXPECT_EQ(c.resource_id(), 13u);
-    EXPECT_EQ(c.legacy_type(), Chunk::Type::kTransferStart);
+    EXPECT_EQ(c.type(), Chunk::Type::kTransferStart);
 
     // Transfer has not yet completed.
     EXPECT_EQ(transfer_status, Status::Unknown());
@@ -1516,7 +1516,7 @@
   Chunk c0 = DecodeChunk(payloads.back());
   EXPECT_EQ(c0.session_id(), 14u);
   EXPECT_EQ(c0.resource_id(), 14u);
-  EXPECT_EQ(c0.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(c0.type(), Chunk::Type::kTransferStart);
 
   // Send the first parameters chunk.
   rpc::test::WaitForPackets(context_.output(), 2, [this] {
@@ -1583,7 +1583,7 @@
   Chunk chunk = DecodeChunk(payloads.back());
   EXPECT_EQ(chunk.session_id(), 15u);
   EXPECT_EQ(chunk.resource_id(), 15u);
-  EXPECT_EQ(chunk.legacy_type(), Chunk::Type::kTransferStart);
+  EXPECT_EQ(chunk.type(), Chunk::Type::kTransferStart);
 
   client_.CancelTransfer(15);
   transfer_thread_.WaitUntilEventIsProcessed();
@@ -1592,7 +1592,7 @@
   ASSERT_EQ(payloads.size(), 2u);
   chunk = DecodeChunk(payloads.back());
   EXPECT_EQ(chunk.session_id(), 15u);
-  ASSERT_EQ(chunk.legacy_type(), Chunk::Type::kTransferCompletion);
+  ASSERT_EQ(chunk.type(), Chunk::Type::kTransferCompletion);
   EXPECT_EQ(chunk.status().value(), Status::Cancelled());
 
   EXPECT_EQ(transfer_status, Status::Cancelled());
diff --git a/pw_transfer/context.cc b/pw_transfer/context.cc
index be49f88..dae00cc 100644
--- a/pw_transfer/context.cc
+++ b/pw_transfer/context.cc
@@ -363,23 +363,16 @@
 }
 
 void Context::TransmitNextChunk(bool retransmit_requested) {
-  ByteSpan buffer = thread_->encode_buffer();
-
-  // Begin by doing a partial encode of all the metadata fields, leaving the
-  // buffer with usable space for the chunk data at the end.
-  transfer::Chunk::MemoryEncoder encoder{buffer};
-  encoder.WriteSessionId(session_id_).IgnoreError();
-  encoder.WriteOffset(offset_).IgnoreError();
-
-  // TODO(frolv): Type field presence is currently meaningful, so this type must
-  // be serialized. Once all users of transfer always set chunk types, the field
-  // can be made non-optional and this write can be removed as TRANSFER_DATA has
-  // the default proto value of 0.
-  encoder.WriteType(transfer::Chunk::Type::TRANSFER_DATA).IgnoreError();
+  Chunk chunk(ProtocolVersion::kLegacy, Chunk::Type::kTransferData);
+  chunk.set_session_id(session_id_);
+  chunk.set_offset(offset_);
 
   // Reserve space for the data proto field overhead and use the remainder of
   // the buffer for the chunk data.
-  size_t reserved_size = encoder.size() + 1 /* data key */ + 5 /* data size */;
+  size_t reserved_size =
+      chunk.EncodedSize() + 1 /* data key */ + 5 /* data size */;
+
+  ByteSpan buffer = thread_->encode_buffer();
 
   ByteSpan data_buffer = buffer.subspan(reserved_size);
   size_t max_bytes_to_send =
@@ -392,7 +385,7 @@
   Result<ByteSpan> data = reader().Read(data_buffer);
   if (data.status().IsOutOfRange()) {
     // No more data to read.
-    encoder.WriteRemainingBytes(0).IgnoreError();
+    chunk.set_remaining_bytes(0);
     window_end_offset_ = offset_;
 
     PW_LOG_DEBUG("Transfer %u sending final chunk with remaining_bytes=0",
@@ -420,7 +413,7 @@
                  static_cast<unsigned>(offset_),
                  static_cast<unsigned>(data.value().size()));
 
-    encoder.WriteData(data.value()).IgnoreError();
+    chunk.set_payload(data.value());
     last_chunk_offset_ = offset_;
     offset_ += data.value().size();
   } else {
@@ -431,14 +424,15 @@
     return;
   }
 
-  if (!encoder.status().ok()) {
+  Result<ConstByteSpan> encoded_chunk = chunk.Encode(buffer);
+  if (!encoded_chunk.ok()) {
     PW_LOG_ERROR("Transfer %u failed to encode transmit chunk",
                  static_cast<unsigned>(session_id_));
     Finish(Status::Internal());
     return;
   }
 
-  if (const Status status = rpc_writer_->Write(encoder); !status.ok()) {
+  if (const Status status = rpc_writer_->Write(*encoded_chunk); !status.ok()) {
     PW_LOG_ERROR("Transfer %u failed to send transmit chunk, status %u",
                  static_cast<unsigned>(session_id_),
                  status.code());
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java
index 40b9a1e..7f1b702 100644
--- a/pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java
@@ -250,7 +250,7 @@
 
   final Chunk.Builder newChunk(Chunk.Type type) {
     // TODO(frolv): Properly set the session ID after it is configured by the server.
-    return Chunk.newBuilder().setSessionId(getId()).setType(type);
+    return Chunk.newBuilder().setTransferId(getId()).setType(type);
   }
 
   /** Sends a chunk. Returns true if sent, false if sending failed and the transfer was aborted. */
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java
index 5683e24..4321e56 100644
--- a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java
@@ -205,7 +205,7 @@
 
   private static String chunkToString(Chunk chunk) {
     StringBuilder str = new StringBuilder();
-    str.append("sessionId:").append(chunk.getSessionId()).append(" ");
+    str.append("sessionId:").append(chunk.getTransferId()).append(" ");
     str.append("windowEndOffset:").append(chunk.getWindowEndOffset()).append(" ");
     str.append("offset:").append(chunk.getOffset()).append(" ");
     // Don't include the actual data; it's too much.
@@ -243,13 +243,13 @@
 
     @Override
     public final void onNext(Chunk chunk) {
-      Transfer<?> transfer = transfers.get(chunk.getSessionId());
+      Transfer<?> transfer = transfers.get(chunk.getTransferId());
       if (transfer != null) {
         logger.atFinest().log("Received chunk: %s", chunkToString(chunk));
         workDispatcher.accept(() -> transfer.handleChunk(chunk));
       } else {
         logger.atWarning().log(
-            "Ignoring unrecognized transfer session ID %d", chunk.getSessionId());
+            "Ignoring unrecognized transfer session ID %d", chunk.getTransferId());
       }
     }
 
diff --git a/pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java b/pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java
index 97a1a1b..a54d218 100644
--- a/pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java
+++ b/pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java
@@ -881,7 +881,7 @@
   }
 
   private static Chunk.Builder newChunk(Chunk.Type type, int resourceId) {
-    return Chunk.newBuilder().setType(type).setSessionId(resourceId);
+    return Chunk.newBuilder().setType(type).setTransferId(resourceId);
   }
 
   private static Chunk initialWriteChunk(int sessionId, int resourceId, int size) {
diff --git a/pw_transfer/public/pw_transfer/internal/chunk.h b/pw_transfer/public/pw_transfer/internal/chunk.h
index 6c10898..72ce600 100644
--- a/pw_transfer/public/pw_transfer/internal/chunk.h
+++ b/pw_transfer/public/pw_transfer/internal/chunk.h
@@ -23,7 +23,7 @@
 
 class Chunk {
  public:
-  enum Type {
+  enum class Type {
     kTransferData = 0,
     kTransferStart = 1,
     kParametersRetransmit = 2,
@@ -52,8 +52,14 @@
         .set_status(status);
   }
 
+  // Encodes the chunk to the specified buffer, returning a span of the
+  // serialized data on success.
   Result<ConstByteSpan> Encode(ByteSpan buffer) const;
 
+  // Returns the size of the serialized chunk based on the fields currently set
+  // within the chunk object.
+  size_t EncodedSize() const;
+
   constexpr Chunk& set_session_id(uint32_t session_id) {
     session_id_ = session_id;
     return *this;
@@ -104,8 +110,14 @@
 
   constexpr uint32_t session_id() const { return session_id_; }
 
-  constexpr uint32_t resource_id() const {
-    return resource_id_.has_value() ? resource_id_.value() : session_id_;
+  constexpr std::optional<uint32_t> resource_id() const {
+    if (is_legacy()) {
+      // In the legacy protocol, resource_id and session_id are the same (i.e.
+      // transfer_id).
+      return session_id_;
+    }
+
+    return resource_id_;
   }
 
   constexpr uint32_t window_end_offset() const { return window_end_offset_; }
@@ -129,11 +141,25 @@
     return protocol_version_ == ProtocolVersion::kLegacy;
   }
 
-  // Legacy protocol chunks may not have a type, but newer versions always will.
-  constexpr std::optional<Type> legacy_type() const { return type_; }
   constexpr Type type() const {
-    PW_ASSERT(!is_legacy());
-    return type_.value();
+    // Legacy protocol chunks may not have a type, but newer versions always
+    // will. Try to deduce the type of a legacy chunk without one set.
+    if (!is_legacy() || type_.has_value()) {
+      return type_.value();
+    }
+
+    // The type-less legacy transfer protocol doesn't support handshakes or
+    // continuation parameters. Therefore, there are only three possible chunk
+    // types: start, data, and retransmit.
+    if (IsInitialChunk()) {
+      return Type::kTransferStart;
+    }
+
+    if (has_payload()) {
+      return Type::kTransferData;
+    }
+
+    return Type::kParametersRetransmit;
   }
 
   // Returns true if this parameters chunk is requesting that the transmitter
@@ -143,13 +169,13 @@
       return true;
     }
 
-    return type_.value() == Chunk::Type::kParametersRetransmit ||
-           type_.value() == Chunk::Type::kTransferStart;
+    return type_.value() == Type::kParametersRetransmit ||
+           type_.value() == Type::kTransferStart;
   }
 
   constexpr bool IsInitialChunk() const {
     if (protocol_version_ >= ProtocolVersion::kVersionTwo) {
-      return type_ == kTransferStart;
+      return type_ == Type::kTransferStart;
     }
 
     // In legacy versions of the transfer protocol, the chunk type is not always
@@ -179,6 +205,18 @@
 
   constexpr Chunk() : Chunk(ProtocolVersion::kUnknown, std::nullopt) {}
 
+  // Returns true if this chunk should write legacy protocol fields to the
+  // serialized message.
+  //
+  // The first chunk of a transfer (type TRANSFER_START) is a special case: as
+  // we do not yet know what version of the protocol the other end is speaking,
+  // every legacy field must be encoded alongside newer ones to ensure that the
+  // chunk is processable. Following a response, the common protocol version
+  // will be determined and fields omitted as necessary.
+  constexpr bool ShouldEncodeLegacyFields() const {
+    return is_legacy() || type_ == Chunk::Type::kTransferStart;
+  }
+
   uint32_t session_id_;
   std::optional<uint32_t> resource_id_;
   uint32_t window_end_offset_;
diff --git a/pw_transfer/py/pw_transfer/client.py b/pw_transfer/py/pw_transfer/client.py
index 0c50d9b..27c5459 100644
--- a/pw_transfer/py/pw_transfer/client.py
+++ b/pw_transfer/py/pw_transfer/client.py
@@ -230,11 +230,11 @@
         """
 
         try:
-            transfer = transfers[chunk.session_id]
+            transfer = transfers[chunk.transfer_id]
         except KeyError:
             _LOG.error(
                 'TransferManager received chunk for unknown transfer %d',
-                chunk.session_id)
+                chunk.transfer_id)
             # TODO(frolv): What should be done here, if anything?
             return
 
diff --git a/pw_transfer/py/pw_transfer/transfer.py b/pw_transfer/py/pw_transfer/transfer.py
index 53c2c39..0004522 100644
--- a/pw_transfer/py/pw_transfer/transfer.py
+++ b/pw_transfer/py/pw_transfer/transfer.py
@@ -202,7 +202,7 @@
         """Sends an error chunk to the server and finishes the transfer."""
         self._send_chunk(
             transfer_pb2.Chunk(
-                session_id=self.id,
+                transfer_id=self.id,
                 status=error.value,
                 type=transfer_pb2.Chunk.Type.TRANSFER_COMPLETION))
         self.finish(error)
@@ -246,7 +246,7 @@
     def _initial_chunk(self) -> transfer_pb2.Chunk:
         # TODO(frolv): session_id should not be set here, but assigned by the
         # server during an initial handshake.
-        return transfer_pb2.Chunk(session_id=self.id,
+        return transfer_pb2.Chunk(transfer_id=self.id,
                                   resource_id=self.id,
                                   type=transfer_pb2.Chunk.Type.TRANSFER_START)
 
@@ -353,7 +353,7 @@
 
     def _next_chunk(self) -> transfer_pb2.Chunk:
         """Returns the next Chunk message to send in the data transfer."""
-        chunk = transfer_pb2.Chunk(session_id=self.id,
+        chunk = transfer_pb2.Chunk(transfer_id=self.id,
                                    offset=self._offset,
                                    type=transfer_pb2.Chunk.Type.TRANSFER_DATA)
         max_bytes_in_chunk = min(self._max_chunk_size,
@@ -444,7 +444,7 @@
                 # No more data to read. Acknowledge receipt and finish.
                 self._send_chunk(
                     transfer_pb2.Chunk(
-                        session_id=self.id,
+                        transfer_id=self.id,
                         status=Status.OK.value,
                         type=transfer_pb2.Chunk.Type.TRANSFER_COMPLETION))
                 self.finish(Status.OK)
@@ -510,7 +510,7 @@
         self._pending_bytes = self._max_bytes_to_receive
         self._window_end_offset = self._offset + self._max_bytes_to_receive
 
-        chunk = transfer_pb2.Chunk(session_id=self.id,
+        chunk = transfer_pb2.Chunk(transfer_id=self.id,
                                    pending_bytes=self._pending_bytes,
                                    window_end_offset=self._window_end_offset,
                                    max_chunk_size_bytes=self._max_chunk_size,
diff --git a/pw_transfer/py/tests/transfer_test.py b/pw_transfer/py/tests/transfer_test.py
index 5d29afb..4c6ea5a 100644
--- a/pw_transfer/py/tests/transfer_test.py
+++ b/pw_transfer/py/tests/transfer_test.py
@@ -105,7 +105,7 @@
         self._enqueue_server_responses(
             _Method.READ,
             ((transfer_pb2.Chunk(
-                session_id=3, offset=0, data=b'abc', remaining_bytes=0), ), ),
+                transfer_id=3, offset=0, data=b'abc', remaining_bytes=0), ), ),
         )
 
         data = manager.read(3)
@@ -122,9 +122,9 @@
             _Method.READ,
             ((
                 transfer_pb2.Chunk(
-                    session_id=3, offset=0, data=b'abc', remaining_bytes=3),
+                    transfer_id=3, offset=0, data=b'abc', remaining_bytes=3),
                 transfer_pb2.Chunk(
-                    session_id=3, offset=3, data=b'def', remaining_bytes=0),
+                    transfer_id=3, offset=3, data=b'def', remaining_bytes=0),
             ), ),
         )
 
@@ -142,9 +142,9 @@
             _Method.READ,
             ((
                 transfer_pb2.Chunk(
-                    session_id=3, offset=0, data=b'abc', remaining_bytes=3),
+                    transfer_id=3, offset=0, data=b'abc', remaining_bytes=3),
                 transfer_pb2.Chunk(
-                    session_id=3, offset=3, data=b'def', remaining_bytes=0),
+                    transfer_id=3, offset=3, data=b'def', remaining_bytes=0),
             ), ),
         )
 
@@ -169,22 +169,26 @@
             _Method.READ,
             (
                 (
-                    transfer_pb2.Chunk(
-                        session_id=3, offset=0, data=b'123',
-                        remaining_bytes=6),
+                    transfer_pb2.Chunk(transfer_id=3,
+                                       offset=0,
+                                       data=b'123',
+                                       remaining_bytes=6),
 
                     # Incorrect offset; expecting 3.
-                    transfer_pb2.Chunk(
-                        session_id=3, offset=1, data=b'456',
-                        remaining_bytes=3),
+                    transfer_pb2.Chunk(transfer_id=3,
+                                       offset=1,
+                                       data=b'456',
+                                       remaining_bytes=3),
                 ),
                 (
-                    transfer_pb2.Chunk(
-                        session_id=3, offset=3, data=b'456',
-                        remaining_bytes=3),
-                    transfer_pb2.Chunk(
-                        session_id=3, offset=6, data=b'789',
-                        remaining_bytes=0),
+                    transfer_pb2.Chunk(transfer_id=3,
+                                       offset=3,
+                                       data=b'456',
+                                       remaining_bytes=3),
+                    transfer_pb2.Chunk(transfer_id=3,
+                                       offset=6,
+                                       data=b'789',
+                                       remaining_bytes=0),
                 ),
             ))
 
@@ -206,7 +210,8 @@
             (
                 (),  # Send nothing in response to the initial parameters.
                 (transfer_pb2.Chunk(
-                    session_id=3, offset=0, data=b'xyz', remaining_bytes=0), ),
+                    transfer_id=3, offset=0, data=b'xyz',
+                    remaining_bytes=0), ),
             ))
 
         data = manager.read(3)
@@ -238,7 +243,7 @@
 
         self._enqueue_server_responses(
             _Method.READ,
-            ((transfer_pb2.Chunk(session_id=31,
+            ((transfer_pb2.Chunk(transfer_id=31,
                                  status=Status.NOT_FOUND.value), ), ),
         )
 
@@ -269,11 +274,11 @@
         self._enqueue_server_responses(
             _Method.WRITE,
             (
-                (transfer_pb2.Chunk(session_id=4,
+                (transfer_pb2.Chunk(transfer_id=4,
                                     offset=0,
                                     pending_bytes=32,
                                     max_chunk_size_bytes=8), ),
-                (transfer_pb2.Chunk(session_id=4, status=Status.OK.value), ),
+                (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value), ),
             ),
         )
 
@@ -288,12 +293,12 @@
         self._enqueue_server_responses(
             _Method.WRITE,
             (
-                (transfer_pb2.Chunk(session_id=4,
+                (transfer_pb2.Chunk(transfer_id=4,
                                     offset=0,
                                     pending_bytes=32,
                                     max_chunk_size_bytes=8), ),
                 (),
-                (transfer_pb2.Chunk(session_id=4, status=Status.OK.value), ),
+                (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value), ),
             ),
         )
 
@@ -310,15 +315,15 @@
         self._enqueue_server_responses(
             _Method.WRITE,
             (
-                (transfer_pb2.Chunk(session_id=4,
+                (transfer_pb2.Chunk(transfer_id=4,
                                     offset=0,
                                     pending_bytes=8,
                                     max_chunk_size_bytes=8), ),
-                (transfer_pb2.Chunk(session_id=4,
+                (transfer_pb2.Chunk(transfer_id=4,
                                     offset=8,
                                     pending_bytes=8,
                                     max_chunk_size_bytes=8), ),
-                (transfer_pb2.Chunk(session_id=4, status=Status.OK.value), ),
+                (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value), ),
             ),
         )
 
@@ -335,15 +340,15 @@
         self._enqueue_server_responses(
             _Method.WRITE,
             (
-                (transfer_pb2.Chunk(session_id=4,
+                (transfer_pb2.Chunk(transfer_id=4,
                                     offset=0,
                                     pending_bytes=8,
                                     max_chunk_size_bytes=8), ),
-                (transfer_pb2.Chunk(session_id=4,
+                (transfer_pb2.Chunk(transfer_id=4,
                                     offset=8,
                                     pending_bytes=8,
                                     max_chunk_size_bytes=8), ),
-                (transfer_pb2.Chunk(session_id=4, status=Status.OK.value), ),
+                (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value), ),
             ),
         )
 
@@ -368,27 +373,27 @@
         self._enqueue_server_responses(
             _Method.WRITE,
             (
-                (transfer_pb2.Chunk(session_id=4,
+                (transfer_pb2.Chunk(transfer_id=4,
                                     offset=0,
                                     pending_bytes=8,
                                     max_chunk_size_bytes=8), ),
-                (transfer_pb2.Chunk(session_id=4,
+                (transfer_pb2.Chunk(transfer_id=4,
                                     offset=8,
                                     pending_bytes=8,
                                     max_chunk_size_bytes=8), ),
                 (
                     transfer_pb2.Chunk(
-                        session_id=4,
+                        transfer_id=4,
                         offset=4,  # rewind
                         pending_bytes=8,
                         max_chunk_size_bytes=8), ),
                 (
                     transfer_pb2.Chunk(
-                        session_id=4,
+                        transfer_id=4,
                         offset=12,
                         pending_bytes=16,  # update max size
                         max_chunk_size_bytes=16), ),
-                (transfer_pb2.Chunk(session_id=4, status=Status.OK.value), ),
+                (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value), ),
             ),
         )
 
@@ -406,17 +411,17 @@
         self._enqueue_server_responses(
             _Method.WRITE,
             (
-                (transfer_pb2.Chunk(session_id=4,
+                (transfer_pb2.Chunk(transfer_id=4,
                                     offset=0,
                                     pending_bytes=8,
                                     max_chunk_size_bytes=8), ),
                 (
                     transfer_pb2.Chunk(
-                        session_id=4,
+                        transfer_id=4,
                         offset=100,  # larger offset than data
                         pending_bytes=8,
                         max_chunk_size_bytes=8), ),
-                (transfer_pb2.Chunk(session_id=4, status=Status.OK.value), ),
+                (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value), ),
             ),
         )
 
@@ -433,7 +438,7 @@
 
         self._enqueue_server_responses(
             _Method.WRITE,
-            ((transfer_pb2.Chunk(session_id=21,
+            ((transfer_pb2.Chunk(transfer_id=21,
                                  status=Status.UNAVAILABLE.value), ), ),
         )
 
@@ -468,16 +473,16 @@
         self.assertEqual(
             self._sent_chunks,
             [
-                transfer_pb2.Chunk(session_id=22,
+                transfer_pb2.Chunk(transfer_id=22,
                                    resource_id=22,
                                    type=transfer_pb2.Chunk.Type.TRANSFER_START
                                    ),  # initial chunk
                 transfer_pb2.Chunk(
-                    session_id=22,
+                    transfer_id=22,
                     resource_id=22,
                     type=transfer_pb2.Chunk.Type.TRANSFER_START),  # retry 1
                 transfer_pb2.Chunk(
-                    session_id=22,
+                    transfer_id=22,
                     resource_id=22,
                     type=transfer_pb2.Chunk.Type.TRANSFER_START),  # retry 2
             ])
@@ -495,14 +500,14 @@
 
         self._enqueue_server_responses(_Method.WRITE, [[
             transfer_pb2.Chunk(
-                session_id=22, pending_bytes=10, max_chunk_size_bytes=5)
+                transfer_id=22, pending_bytes=10, max_chunk_size_bytes=5)
         ]])
 
         with self.assertRaises(pw_transfer.Error) as context:
             manager.write(22, b'0123456789')
 
         last_data_chunk = transfer_pb2.Chunk(
-            session_id=22,
+            transfer_id=22,
             data=b'56789',
             offset=5,
             remaining_bytes=0,
@@ -512,10 +517,10 @@
             self._sent_chunks,
             [
                 transfer_pb2.Chunk(
-                    session_id=22,
+                    transfer_id=22,
                     resource_id=22,
                     type=transfer_pb2.Chunk.Type.TRANSFER_START),
-                transfer_pb2.Chunk(session_id=22,
+                transfer_pb2.Chunk(transfer_id=22,
                                    data=b'01234',
                                    type=transfer_pb2.Chunk.Type.TRANSFER_DATA),
                 last_data_chunk,  # last chunk
@@ -533,7 +538,7 @@
 
         self._enqueue_server_responses(
             _Method.WRITE,
-            ((transfer_pb2.Chunk(session_id=23, pending_bytes=0), ), ),
+            ((transfer_pb2.Chunk(transfer_id=23, pending_bytes=0), ), ),
         )
 
         with self.assertRaises(pw_transfer.Error) as context:
diff --git a/pw_transfer/transfer.proto b/pw_transfer/transfer.proto
index 8f88904..e4bb5a8 100644
--- a/pw_transfer/transfer.proto
+++ b/pw_transfer/transfer.proto
@@ -41,11 +41,13 @@
   // stable depending on the implementation. Sent in every request to identify
   // the transfer target.
   //
+  // LEGACY FIELD ONLY. Split into resource_id and session_id in transfer v2.
+  //
   //  Read → ID of transfer
   //  Read ← ID of transfer
   // Write → ID of transfer
   // Write ← ID of transfer
-  uint32 session_id = 1;
+  uint32 transfer_id = 1;
 
   // Used by the receiver to indicate how many bytes it can accept. The
   // transmitter sends this much data, divided into chunks no larger than
@@ -173,6 +175,23 @@
   // Write ← Chunk type (start/parameters).
   optional Type type = 10;
 
-  // Not currently used. Will inherit session_id's behavior in the future.
-  uint32 resource_id = 11;
+  // Unique identifier for the source or destination of transfer data. May be
+  // stable or ephemeral depending on the implementation. Only sent during the
+  // initial handshake phase of a version 2 or higher transfer.
+  //
+  //  Read → ID of transferable resource
+  //  Read ← ID of transferable resource
+  // Write → ID of transferable resource
+  // Write ← ID of transferable resource
+  optional uint32 resource_id = 11;
+
+  // Unique identifier for a specific transfer session. Assigned by a transfer
+  // service during the initial handshake phase, and persists for the remainder
+  // of that transfer operation.
+  //
+  //  Read → ID of transfer session
+  //  Read ← ID of transfer session
+  // Write → ID of transfer session
+  // Write ← ID of transfer session
+  optional uint32 session_id = 12;
 }
diff --git a/pw_transfer/transfer_test.cc b/pw_transfer/transfer_test.cc
index 1591fac..868dc6c 100644
--- a/pw_transfer/transfer_test.cc
+++ b/pw_transfer/transfer_test.cc
@@ -1012,7 +1012,7 @@
   chunk = DecodeChunk(ctx_.responses().back());
   EXPECT_EQ(chunk.session_id(), 7u);
   EXPECT_EQ(chunk.window_end_offset(), 32u);
-  EXPECT_EQ(chunk.legacy_type(), Chunk::Type::kParametersContinue);
+  EXPECT_EQ(chunk.type(), Chunk::Type::kParametersContinue);
 
   ctx_.SendClientStream<64>(
       EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kTransferData)
@@ -1068,7 +1068,7 @@
   EXPECT_EQ(chunk.session_id(), 7u);
   EXPECT_EQ(chunk.offset(), 12u);
   EXPECT_EQ(chunk.window_end_offset(), 32u);
-  EXPECT_EQ(chunk.legacy_type(), Chunk::Type::kParametersRetransmit);
+  EXPECT_EQ(chunk.type(), Chunk::Type::kParametersRetransmit);
 }
 
 TEST_F(WriteTransfer, TransmitterExtendsWindow_TerminatesWithInvalid) {
diff --git a/pw_transfer/ts/client.ts b/pw_transfer/ts/client.ts
index fd3ca50..2deb453 100644
--- a/pw_transfer/ts/client.ts
+++ b/pw_transfer/ts/client.ts
@@ -86,7 +86,9 @@
     progressCallback?: ProgressCallback
   ): Promise<Uint8Array> {
     if (resourceId in this.readTransfers) {
-      throw new Error(`Read transfer for resource ${resourceId} already exists`);
+      throw new Error(
+        `Read transfer for resource ${resourceId} already exists`
+      );
     }
     const transfer = new ReadTransfer(
       resourceId,
@@ -250,10 +252,10 @@
    * is invoked.
    */
   private async handleChunk(transfers: TransferDict, chunk: Chunk) {
-    const transfer = transfers[chunk.getSessionId()];
+    const transfer = transfers[chunk.getTransferId()];
     if (transfer === undefined) {
       console.error(
-        `TransferManager received chunk for unknown transfer ${chunk.getSessionId()}`
+        `TransferManager received chunk for unknown transfer ${chunk.getTransferId()}`
       );
       return;
     }
diff --git a/pw_transfer/ts/transfer.ts b/pw_transfer/ts/transfer.ts
index 55b6ccc..8264f0a 100644
--- a/pw_transfer/ts/transfer.ts
+++ b/pw_transfer/ts/transfer.ts
@@ -137,7 +137,7 @@
   protected sendError(error: Status): void {
     const chunk = new Chunk();
     chunk.setStatus(error);
-    chunk.setSessionId(this.id);
+    chunk.setTransferId(this.id);
     chunk.setType(Chunk.Type.TRANSFER_COMPLETION);
     this.sendChunk(chunk);
     this.finish(error);
@@ -262,7 +262,7 @@
     this.windowEndOffset = this.offset + this.maxBytesToReceive;
 
     const chunk = new Chunk();
-    chunk.setSessionId(this.id);
+    chunk.setTransferId(this.id);
     chunk.setPendingBytes(this.pendingBytes);
     chunk.setMaxChunkSizeBytes(this.maxChunkSize);
     chunk.setOffset(this.offset);
@@ -303,7 +303,7 @@
       if (chunk.getRemainingBytes() === 0) {
         // No more data to read. Acknowledge receipt and finish.
         const endChunk = new Chunk();
-        endChunk.setSessionId(this.id);
+        endChunk.setTransferId(this.id);
         endChunk.setStatus(Status.OK);
         endChunk.setType(Chunk.Type.TRANSFER_COMPLETION);
         this.sendChunk(endChunk);
@@ -405,7 +405,7 @@
     // TODO(frolv): The session ID should not be set here but assigned by the
     // server during an initial handshake.
     const chunk = new Chunk();
-    chunk.setSessionId(this.id);
+    chunk.setTransferId(this.id);
     chunk.setResourceId(this.id);
     chunk.setType(Chunk.Type.TRANSFER_START);
     return chunk;
@@ -516,7 +516,7 @@
   /** Returns the next Chunk message to send in the data transfer. */
   private nextChunk(): Chunk {
     const chunk = new Chunk();
-    chunk.setSessionId(this.id);
+    chunk.setTransferId(this.id);
     chunk.setOffset(this.offset);
     chunk.setType(Chunk.Type.TRANSFER_DATA);
 
diff --git a/pw_transfer/ts/transfer_test.ts b/pw_transfer/ts/transfer_test.ts
index 16325bf..584df42 100644
--- a/pw_transfer/ts/transfer_test.ts
+++ b/pw_transfer/ts/transfer_test.ts
@@ -118,7 +118,7 @@
     remainingBytes: number
   ): Chunk {
     const chunk = new Chunk();
-    chunk.setSessionId(sessionId);
+    chunk.setTransferId(sessionId);
     chunk.setOffset(offset);
     chunk.setData(textEncoder.encode(data));
     chunk.setRemainingBytes(remainingBytes);
@@ -230,7 +230,7 @@
 
     const chunk = new Chunk();
     chunk.setStatus(Status.NOT_FOUND);
-    chunk.setSessionId(31);
+    chunk.setTransferId(31);
     enqueueServerResponses(service.method('Read')!, [[chunk]]);
 
     await manager
@@ -263,13 +263,13 @@
     const manager = new Manager(service, DEFAULT_TIMEOUT_S);
 
     const chunk = new Chunk();
-    chunk.setSessionId(4);
+    chunk.setTransferId(4);
     chunk.setOffset(0);
     chunk.setPendingBytes(32);
     chunk.setMaxChunkSizeBytes(8);
 
     const completeChunk = new Chunk();
-    completeChunk.setSessionId(4);
+    completeChunk.setTransferId(4);
     completeChunk.setStatus(Status.OK);
 
     enqueueServerResponses(service.method('Write')!, [
@@ -286,13 +286,13 @@
     const manager = new Manager(service, DEFAULT_TIMEOUT_S);
 
     const chunk = new Chunk();
-    chunk.setSessionId(4);
+    chunk.setTransferId(4);
     chunk.setOffset(0);
     chunk.setPendingBytes(32);
     chunk.setMaxChunkSizeBytes(8);
 
     const completeChunk = new Chunk();
-    completeChunk.setSessionId(4);
+    completeChunk.setTransferId(4);
     completeChunk.setStatus(Status.OK);
 
     enqueueServerResponses(service.method('Write')!, [
@@ -311,19 +311,19 @@
     const manager = new Manager(service, DEFAULT_TIMEOUT_S);
 
     const chunk = new Chunk();
-    chunk.setSessionId(4);
+    chunk.setTransferId(4);
     chunk.setOffset(0);
     chunk.setPendingBytes(8);
     chunk.setMaxChunkSizeBytes(8);
 
     const chunk2 = new Chunk();
-    chunk2.setSessionId(4);
+    chunk2.setTransferId(4);
     chunk2.setOffset(8);
     chunk2.setPendingBytes(8);
     chunk2.setMaxChunkSizeBytes(8);
 
     const completeChunk = new Chunk();
-    completeChunk.setSessionId(4);
+    completeChunk.setTransferId(4);
     completeChunk.setStatus(Status.OK);
 
     enqueueServerResponses(service.method('Write')!, [
@@ -343,7 +343,7 @@
     const manager = new Manager(service, DEFAULT_TIMEOUT_S);
 
     const chunk = new Chunk();
-    chunk.setSessionId(4);
+    chunk.setTransferId(4);
     chunk.setOffset(0);
     chunk.setPendingBytes(8);
     chunk.setMaxChunkSizeBytes(4);
@@ -351,42 +351,42 @@
     chunk.setWindowEndOffset(8);
 
     const chunk2 = new Chunk();
-    chunk2.setSessionId(4);
+    chunk2.setTransferId(4);
     chunk2.setOffset(4);
     chunk2.setPendingBytes(8);
     chunk2.setType(Chunk.Type.PARAMETERS_CONTINUE);
     chunk2.setWindowEndOffset(12);
 
     const chunk3 = new Chunk();
-    chunk3.setSessionId(4);
+    chunk3.setTransferId(4);
     chunk3.setOffset(8);
     chunk3.setPendingBytes(8);
     chunk3.setType(Chunk.Type.PARAMETERS_CONTINUE);
     chunk3.setWindowEndOffset(16);
 
     const chunk4 = new Chunk();
-    chunk4.setSessionId(4);
+    chunk4.setTransferId(4);
     chunk4.setOffset(12);
     chunk4.setPendingBytes(8);
     chunk4.setType(Chunk.Type.PARAMETERS_CONTINUE);
     chunk4.setWindowEndOffset(20);
 
     const chunk5 = new Chunk();
-    chunk5.setSessionId(4);
+    chunk5.setTransferId(4);
     chunk5.setOffset(16);
     chunk5.setPendingBytes(8);
     chunk5.setType(Chunk.Type.PARAMETERS_CONTINUE);
     chunk5.setWindowEndOffset(24);
 
     const chunk6 = new Chunk();
-    chunk6.setSessionId(4);
+    chunk6.setTransferId(4);
     chunk6.setOffset(20);
     chunk6.setPendingBytes(8);
     chunk6.setType(Chunk.Type.PARAMETERS_CONTINUE);
     chunk6.setWindowEndOffset(28);
 
     const completeChunk = new Chunk();
-    completeChunk.setSessionId(4);
+    completeChunk.setTransferId(4);
     completeChunk.setStatus(Status.OK);
 
     enqueueServerResponses(service.method('Write')!, [
@@ -415,19 +415,19 @@
     const manager = new Manager(service, DEFAULT_TIMEOUT_S);
 
     const chunk = new Chunk();
-    chunk.setSessionId(4);
+    chunk.setTransferId(4);
     chunk.setOffset(0);
     chunk.setPendingBytes(8);
     chunk.setMaxChunkSizeBytes(8);
 
     const chunk2 = new Chunk();
-    chunk2.setSessionId(4);
+    chunk2.setTransferId(4);
     chunk2.setOffset(8);
     chunk2.setPendingBytes(8);
     chunk2.setMaxChunkSizeBytes(8);
 
     const completeChunk = new Chunk();
-    completeChunk.setSessionId(4);
+    completeChunk.setTransferId(4);
     completeChunk.setStatus(Status.OK);
 
     enqueueServerResponses(service.method('Write')!, [
@@ -461,31 +461,31 @@
     const manager = new Manager(service, DEFAULT_TIMEOUT_S);
 
     const chunk1 = new Chunk();
-    chunk1.setSessionId(4);
+    chunk1.setTransferId(4);
     chunk1.setOffset(0);
     chunk1.setPendingBytes(8);
     chunk1.setMaxChunkSizeBytes(8);
 
     const chunk2 = new Chunk();
-    chunk2.setSessionId(4);
+    chunk2.setTransferId(4);
     chunk2.setOffset(8);
     chunk2.setPendingBytes(8);
     chunk2.setMaxChunkSizeBytes(8);
 
     const chunk3 = new Chunk();
-    chunk3.setSessionId(4);
+    chunk3.setTransferId(4);
     chunk3.setOffset(4); // Rewind
     chunk3.setPendingBytes(8);
     chunk3.setMaxChunkSizeBytes(8);
 
     const chunk4 = new Chunk();
-    chunk4.setSessionId(4);
+    chunk4.setTransferId(4);
     chunk4.setOffset(12); // Rewind
     chunk4.setPendingBytes(16);
     chunk4.setMaxChunkSizeBytes(16);
 
     const completeChunk = new Chunk();
-    completeChunk.setSessionId(4);
+    completeChunk.setTransferId(4);
     completeChunk.setStatus(Status.OK);
 
     enqueueServerResponses(service.method('Write')!, [
@@ -508,19 +508,19 @@
     const manager = new Manager(service, DEFAULT_TIMEOUT_S);
 
     const chunk1 = new Chunk();
-    chunk1.setSessionId(4);
+    chunk1.setTransferId(4);
     chunk1.setOffset(0);
     chunk1.setPendingBytes(8);
     chunk1.setMaxChunkSizeBytes(8);
 
     const chunk2 = new Chunk();
-    chunk2.setSessionId(4);
+    chunk2.setTransferId(4);
     chunk2.setOffset(100); // larger offset than data
     chunk2.setPendingBytes(8);
     chunk2.setMaxChunkSizeBytes(8);
 
     const completeChunk = new Chunk();
-    completeChunk.setSessionId(4);
+    completeChunk.setTransferId(4);
     completeChunk.setStatus(Status.OK);
 
     enqueueServerResponses(service.method('Write')!, [
@@ -544,7 +544,7 @@
     const manager = new Manager(service, DEFAULT_TIMEOUT_S);
 
     const chunk = new Chunk();
-    chunk.setSessionId(21);
+    chunk.setTransferId(21);
     chunk.setStatus(Status.UNAVAILABLE);
 
     enqueueServerResponses(service.method('Write')!, [[chunk]]);
@@ -564,7 +564,7 @@
     const manager = new Manager(service, DEFAULT_TIMEOUT_S);
 
     const chunk = new Chunk();
-    chunk.setSessionId(21);
+    chunk.setTransferId(21);
     chunk.setStatus(Status.NOT_FOUND);
 
     enqueueServerError(service.method('Write')!, Status.NOT_FOUND);
@@ -599,7 +599,7 @@
     const manager = new Manager(service, DEFAULT_TIMEOUT_S, 4, 2);
 
     const chunk = new Chunk();
-    chunk.setSessionId(22);
+    chunk.setTransferId(22);
     chunk.setPendingBytes(10);
     chunk.setMaxChunkSizeBytes(5);
 
@@ -612,15 +612,15 @@
       })
       .catch(error => {
         const expectedChunk1 = new Chunk();
-        expectedChunk1.setSessionId(22);
+        expectedChunk1.setTransferId(22);
         expectedChunk1.setResourceId(22);
         expectedChunk1.setType(Chunk.Type.TRANSFER_START);
         const expectedChunk2 = new Chunk();
-        expectedChunk2.setSessionId(22);
+        expectedChunk2.setTransferId(22);
         expectedChunk2.setData(textEncoder.encode('01234'));
         expectedChunk2.setType(Chunk.Type.TRANSFER_DATA);
         const lastChunk = new Chunk();
-        lastChunk.setSessionId(22);
+        lastChunk.setTransferId(22);
         lastChunk.setData(textEncoder.encode('56789'));
         lastChunk.setOffset(5);
         lastChunk.setRemainingBytes(0);
@@ -645,7 +645,7 @@
     const manager = new Manager(service, DEFAULT_TIMEOUT_S);
 
     const chunk = new Chunk();
-    chunk.setSessionId(23);
+    chunk.setTransferId(23);
     chunk.setPendingBytes(0);
 
     enqueueServerResponses(service.method('Write')!, [[chunk]]);