refactor(sdk)!: remove v2 bridges and legacy message wire format (#3904)

Removes the old bridge APIs such as `StoreConsumerOffset2` and
`DeleteConsumerOffset2`, additionally promotes to the client side
encoding the new message wire format, rather than relying on server-side
conversion.

---------

Co-authored-by: Piotr Gankiewicz <piotr.gankiewicz@gmail.com>
diff --git a/Cargo.lock b/Cargo.lock
index e5964e9..a08a49a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1950,7 +1950,7 @@
 
 [[package]]
 name = "bench-dashboard-frontend"
-version = "0.8.0-edge.2"
+version = "0.8.0-edge.3"
 dependencies = [
  "bench-dashboard-shared",
  "bench-report",
@@ -1980,7 +1980,7 @@
 
 [[package]]
 name = "bench-report"
-version = "0.4.0-edge.2"
+version = "0.4.0-edge.3"
 dependencies = [
  "charming",
  "colored",
@@ -4848,35 +4848,6 @@
 ]
 
 [[package]]
-name = "ext-trait"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d772df1c1a777963712fb68e014235e80863d6a91a85c4e06ba2d16243a310e5"
-dependencies = [
- "ext-trait-proc_macros",
-]
-
-[[package]]
-name = "ext-trait-proc_macros"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ab7934152eaf26aa5aa9f7371408ad5af4c31357073c9e84c3b9d7f11ad639a"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
-name = "extension-traits"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a296e5a895621edf9fa8329c83aa1cb69a964643e36cf54d8d7a69b789089537"
-dependencies = [
- "ext-trait",
-]
-
-[[package]]
 name = "fastbloom"
 version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6622,7 +6593,7 @@
 
 [[package]]
 name = "iggy"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
 dependencies = [
  "async-broadcast",
  "async-dropper",
@@ -6656,7 +6627,7 @@
 
 [[package]]
 name = "iggy-bench"
-version = "0.6.0-edge.3"
+version = "0.6.0-edge.4"
 dependencies = [
  "async-trait",
  "bench-report",
@@ -6685,7 +6656,7 @@
 
 [[package]]
 name = "iggy-bench-dashboard-server"
-version = "0.8.0-edge.2"
+version = "0.8.0-edge.3"
 dependencies = [
  "actix-cors",
  "actix-files",
@@ -6713,7 +6684,7 @@
 
 [[package]]
 name = "iggy-cli"
-version = "0.14.0-edge.3"
+version = "0.14.0-edge.4"
 dependencies = [
  "anyhow",
  "apple-native-keyring-store",
@@ -6747,7 +6718,7 @@
 
 [[package]]
 name = "iggy-connectors"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "axum",
@@ -6799,7 +6770,7 @@
 
 [[package]]
 name = "iggy-mcp"
-version = "0.5.0-edge.2"
+version = "0.5.0-edge.3"
 dependencies = [
  "axum",
  "axum-server",
@@ -6833,7 +6804,7 @@
 
 [[package]]
 name = "iggy_binary_protocol"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
 dependencies = [
  "aligned-vec",
  "bytemuck",
@@ -6846,7 +6817,7 @@
 
 [[package]]
 name = "iggy_common"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
 dependencies = [
  "aes-gcm",
  "async-broadcast",
@@ -6862,7 +6833,6 @@
  "derive_more",
  "humantime",
  "iggy_binary_protocol",
- "lending-iterator",
  "papaya",
  "ring",
  "secrecy",
@@ -6882,7 +6852,7 @@
 
 [[package]]
 name = "iggy_connector_clickhouse_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
 dependencies = [
  "async-trait",
  "bytes",
@@ -6900,7 +6870,7 @@
 
 [[package]]
 name = "iggy_connector_delta_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
 dependencies = [
  "async-trait",
  "chrono",
@@ -6917,7 +6887,7 @@
 
 [[package]]
 name = "iggy_connector_doris_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
 dependencies = [
  "async-trait",
  "base64",
@@ -6937,7 +6907,7 @@
 
 [[package]]
 name = "iggy_connector_elasticsearch_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "base64",
@@ -6955,7 +6925,7 @@
 
 [[package]]
 name = "iggy_connector_elasticsearch_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "dashmap",
@@ -6973,7 +6943,7 @@
 
 [[package]]
 name = "iggy_connector_http_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "base64",
@@ -6995,7 +6965,7 @@
 
 [[package]]
 name = "iggy_connector_iceberg_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "arrow-json 57.3.1",
  "async-trait",
@@ -7014,7 +6984,7 @@
 
 [[package]]
 name = "iggy_connector_influxdb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "axum",
@@ -7035,7 +7005,7 @@
 
 [[package]]
 name = "iggy_connector_influxdb_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "ahash 0.8.12",
  "async-trait",
@@ -7061,7 +7031,7 @@
 
 [[package]]
 name = "iggy_connector_meilisearch_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "base64",
@@ -7079,7 +7049,7 @@
 
 [[package]]
 name = "iggy_connector_mongodb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "humantime",
@@ -7095,7 +7065,7 @@
 
 [[package]]
 name = "iggy_connector_postgres_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "dashmap",
@@ -7114,7 +7084,7 @@
 
 [[package]]
 name = "iggy_connector_postgres_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "base64",
@@ -7136,7 +7106,7 @@
 
 [[package]]
 name = "iggy_connector_quickwit_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "dashmap",
@@ -7150,7 +7120,7 @@
 
 [[package]]
 name = "iggy_connector_random_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "dashmap",
@@ -7167,7 +7137,7 @@
 
 [[package]]
 name = "iggy_connector_s3_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "base64",
@@ -7188,7 +7158,7 @@
 
 [[package]]
 name = "iggy_connector_sdk"
-version = "0.4.0-edge.2"
+version = "0.4.0-edge.3"
 dependencies = [
  "anyhow",
  "apache-avro",
@@ -7224,7 +7194,7 @@
 
 [[package]]
 name = "iggy_connector_stdout_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "dashmap",
@@ -7236,7 +7206,7 @@
 
 [[package]]
 name = "iggy_connector_surrealdb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 dependencies = [
  "async-trait",
  "base64",
@@ -7851,31 +7821,6 @@
 ]
 
 [[package]]
-name = "lending-iterator"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc07588c853b50689205fb5c00498aa681d89828e0ce8cbd965ebc7a5d8ae260"
-dependencies = [
- "extension-traits",
- "lending-iterator-proc_macros",
- "macro_rules_attribute",
- "never-say-never",
- "nougat",
- "polonius-the-crab",
-]
-
-[[package]]
-name = "lending-iterator-proc_macros"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5445dd1c0deb1e97b8a16561d17fc686ca83e8411128fb036e9668a72d51b1d"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
 name = "lexical-core"
 version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8295,22 +8240,6 @@
 ]
 
 [[package]]
-name = "macro_rules_attribute"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf0c9b980bf4f3a37fd7b1c066941dd1b1d0152ce6ee6e8fe8c49b9f6810d862"
-dependencies = [
- "macro_rules_attribute-proc_macro",
- "paste",
-]
-
-[[package]]
-name = "macro_rules_attribute-proc_macro"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d"
-
-[[package]]
 name = "matchers"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8730,12 +8659,6 @@
 checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
 
 [[package]]
-name = "never-say-never"
-version = "6.6.666"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf5a574dadd7941adeaa71823ecba5e28331b8313fb2e1c6a5c7e5981ea53ad6"
-
-[[package]]
 name = "new_debug_unreachable"
 version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -8844,27 +8767,6 @@
 ]
 
 [[package]]
-name = "nougat"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97b57b9ced431322f054fc673f1d3c7fa52d80efd9df74ad2fc759f044742510"
-dependencies = [
- "macro_rules_attribute",
- "nougat-proc_macros",
-]
-
-[[package]]
-name = "nougat-proc_macros"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c84f77a45e99a2f9b492695d99e1c23844619caa5f3e57647cffacad773ca257"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 1.0.109",
-]
-
-[[package]]
 name = "ntapi"
 version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -9885,12 +9787,6 @@
 ]
 
 [[package]]
-name = "polonius-the-crab"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2a69ee997a6282f8462abf1e0d8c38c965e968799e912b3bed8c9e8a28c2f9f"
-
-[[package]]
 name = "polyval"
 version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -11838,7 +11734,7 @@
 
 [[package]]
 name = "server"
-version = "0.9.0-edge.4"
+version = "0.9.0-edge.5"
 dependencies = [
  "ahash 0.8.12",
  "argon2",
diff --git a/Cargo.toml b/Cargo.toml
index 784e3ac..eff5c66 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -199,11 +199,11 @@
 iceberg = "0.9.1"
 iceberg-catalog-rest = "0.9.1"
 iceberg-storage-opendal = "0.9.1"
-iggy = { path = "core/sdk", version = "0.11.0-edge.3" }
-iggy-cli = { path = "core/cli", version = "0.14.0-edge.3" }
-iggy_binary_protocol = { path = "core/binary_protocol", version = "0.11.0-edge.3" }
-iggy_common = { path = "core/common", version = "0.11.0-edge.3" }
-iggy_connector_sdk = { path = "core/connectors/sdk", version = "0.4.0-edge.2" }
+iggy = { path = "core/sdk", version = "0.11.0-edge.4" }
+iggy-cli = { path = "core/cli", version = "0.14.0-edge.4" }
+iggy_binary_protocol = { path = "core/binary_protocol", version = "0.11.0-edge.4" }
+iggy_common = { path = "core/common", version = "0.11.0-edge.4" }
+iggy_connector_sdk = { path = "core/connectors/sdk", version = "0.4.0-edge.3" }
 indexmap = "2.14.0"
 integration = { path = "core/integration" }
 ipnet = "2.12.0"
@@ -213,7 +213,6 @@
 keyring-core = "1.0.0"
 lazy_static = "1.5.0"
 left-right = "0.11"
-lending-iterator = "0.1.7"
 libc = "0.2.188"
 log = "0.4.33"
 lz4_flex = "0.14.0"
diff --git a/bdd/go/go.mod b/bdd/go/go.mod
index 8bbd9df..1106967 100644
--- a/bdd/go/go.mod
+++ b/bdd/go/go.mod
@@ -25,7 +25,9 @@
 	github.com/hashicorp/go-memdb v1.3.5 // indirect
 	github.com/hashicorp/golang-lru v1.0.2 // indirect
 	github.com/klauspost/compress v1.19.2 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
 	github.com/spf13/pflag v1.0.10 // indirect
+	github.com/zeebo/xxh3 v1.1.0 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
 	golang.org/x/mod v0.36.0 // indirect
 	golang.org/x/net v0.56.0 // indirect
diff --git a/bdd/go/go.sum b/bdd/go/go.sum
index 4a53ed4..3767365 100644
--- a/bdd/go/go.sum
+++ b/bdd/go/go.sum
@@ -43,6 +43,8 @@
 github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
 github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
 github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -71,6 +73,10 @@
 github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
 github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
+github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
+github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
 go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
 golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
diff --git a/bdd/python/uv.lock b/bdd/python/uv.lock
index a5ebae6..e7014a8 100644
--- a/bdd/python/uv.lock
+++ b/bdd/python/uv.lock
@@ -8,7 +8,7 @@
 
 [[package]]
 name = "apache-iggy"
-version = "0.9.0.dev3"
+version = "0.9.0.dev4"
 source = { directory = "../../foreign/python" }
 
 [package.metadata]
diff --git a/core/ai/mcp/Cargo.toml b/core/ai/mcp/Cargo.toml
index 0d061d1..10e920b 100644
--- a/core/ai/mcp/Cargo.toml
+++ b/core/ai/mcp/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy-mcp"
-version = "0.5.0-edge.2"
+version = "0.5.0-edge.3"
 description = "MCP Server for Iggy message streaming platform"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/bench/Cargo.toml b/core/bench/Cargo.toml
index d9e3a57..922af4d 100644
--- a/core/bench/Cargo.toml
+++ b/core/bench/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy-bench"
-version = "0.6.0-edge.3"
+version = "0.6.0-edge.4"
 edition = "2024"
 license = "Apache-2.0"
 repository = "https://github.com/apache/iggy"
diff --git a/core/bench/dashboard/frontend/Cargo.toml b/core/bench/dashboard/frontend/Cargo.toml
index 0cc8969..c76ae74 100644
--- a/core/bench/dashboard/frontend/Cargo.toml
+++ b/core/bench/dashboard/frontend/Cargo.toml
@@ -18,7 +18,7 @@
 [package]
 name = "bench-dashboard-frontend"
 license = "Apache-2.0"
-version = "0.8.0-edge.2"
+version = "0.8.0-edge.3"
 edition = "2024"
 publish = false
 
diff --git a/core/bench/dashboard/server/Cargo.toml b/core/bench/dashboard/server/Cargo.toml
index a57782a..82ca242 100644
--- a/core/bench/dashboard/server/Cargo.toml
+++ b/core/bench/dashboard/server/Cargo.toml
@@ -18,7 +18,7 @@
 [package]
 name = "iggy-bench-dashboard-server"
 license = "Apache-2.0"
-version = "0.8.0-edge.2"
+version = "0.8.0-edge.3"
 edition = "2024"
 publish = false
 
diff --git a/core/bench/report/Cargo.toml b/core/bench/report/Cargo.toml
index 5aa1065..88b7dac 100644
--- a/core/bench/report/Cargo.toml
+++ b/core/bench/report/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "bench-report"
-version = "0.4.0-edge.2"
+version = "0.4.0-edge.3"
 edition = "2024"
 description = "Benchmark report and chart generation library for iggy-bench binary and iggy-benchmarks-dashboard web app"
 license = "Apache-2.0"
diff --git a/core/binary_protocol/Cargo.toml b/core/binary_protocol/Cargo.toml
index a52840d..09fee0d 100644
--- a/core/binary_protocol/Cargo.toml
+++ b/core/binary_protocol/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_binary_protocol"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
 description = "Wire protocol types and codec for the Iggy binary protocol. Shared between server and SDK."
 edition = "2024"
 rust-version.workspace = true
diff --git a/core/binary_protocol/src/batch.rs b/core/binary_protocol/src/batch.rs
new file mode 100644
index 0000000..995abd1
--- /dev/null
+++ b/core/binary_protocol/src/batch.rs
@@ -0,0 +1,663 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! The message batch: the one layout a `SendMessages` body, the replicated
+//! prepare, the persisted segment record, and the poll reply all share.
+//!
+//! ```text
+//! [batch header: 256 bytes][blob: message frames]
+//! frame = [header: 48 bytes][payload][user_headers]
+//! ```
+//!
+//! Producers encode this batch directly on the wire (after the routing
+//! metadata section); the server stamps `partition_id`, `base_offset`, and
+//! `base_timestamp` at ingestion/persistence and replicates the bytes
+//! verbatim; polls serve the stored records back. There is no other message
+//! encoding.
+
+use crate::WireError;
+use std::hash::Hasher;
+use twox_hash::XxHash3_64;
+
+/// Size of the batch header. The remainder past the fields below is reserved
+/// and must be zero.
+pub const BATCH_HEADER_SIZE: usize = 256;
+
+/// Size of a message frame header inside the blob.
+pub const BATCH_MESSAGE_HEADER_SIZE: usize = 48;
+
+/// Byte offset of `batch_checksum` inside the batch header.
+pub const BATCH_CHECKSUM_OFFSET: usize = 40;
+
+/// Byte offset of `message_count` inside the batch header.
+pub const BATCH_MESSAGE_COUNT_OFFSET: usize = 48;
+
+/// Byte offset where the reserved region of the batch header starts.
+pub const BATCH_RESERVED_OFFSET: usize = 52;
+
+/// Upper bound on a message's `timestamp_delta`: the field is a `u32`
+/// microsecond delta against the batch `origin_timestamp`, so a single batch
+/// spans at most ~71.6 minutes of producer clock.
+pub const MAX_TIMESTAMP_DELTA_MICROS: u64 = u32::MAX as u64;
+
+/// The batch header.
+///
+/// A producer encodes it with `partition_id`, `base_offset`, and
+/// `base_timestamp` zero; the server owns those three fields and stamps them
+/// at ingestion (`partition_id`) and persistence (`base_offset`,
+/// `base_timestamp`, plus the `batch_checksum` recompute).
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct BatchHeader {
+    pub partition_id: u64,
+    pub base_offset: u64,
+    pub base_timestamp: u64,
+    pub origin_timestamp: u64,
+    /// Total batch size: `BATCH_HEADER_SIZE` + blob length.
+    pub batch_length: u64,
+    pub batch_checksum: u64,
+    pub message_count: u32,
+}
+
+impl BatchHeader {
+    #[must_use]
+    pub const fn new(
+        partition_id: u64,
+        origin_timestamp: u64,
+        batch_length: u64,
+        message_count: u32,
+    ) -> Self {
+        Self {
+            partition_id,
+            base_offset: 0,
+            base_timestamp: 0,
+            origin_timestamp,
+            batch_length,
+            batch_checksum: 0,
+            message_count,
+        }
+    }
+
+    /// # Errors
+    /// [`WireError::UnexpectedEof`] on a short buffer;
+    /// [`WireError::Validation`] on a `batch_length` smaller than the header
+    /// or nonzero reserved bytes.
+    pub fn decode(bytes: &[u8]) -> Result<Self, WireError> {
+        if bytes.len() < BATCH_HEADER_SIZE {
+            return Err(WireError::UnexpectedEof {
+                offset: 0,
+                need: BATCH_HEADER_SIZE,
+                have: bytes.len(),
+            });
+        }
+
+        let batch_length = read_u64(bytes, 32);
+        if batch_length < BATCH_HEADER_SIZE as u64 {
+            return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+                "batch length must cover the batch header",
+            )));
+        }
+
+        // Every encoder zeroes the reserved region. Admitting nonzero bytes
+        // would let unchecksummed data ride the header to disk and replicas.
+        if bytes[BATCH_RESERVED_OFFSET..BATCH_HEADER_SIZE]
+            .iter()
+            .any(|&reserved_byte| reserved_byte != 0)
+        {
+            return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+                "batch header reserved bytes must be zero",
+            )));
+        }
+
+        Ok(Self {
+            partition_id: read_u64(bytes, 0),
+            base_offset: read_u64(bytes, 8),
+            base_timestamp: read_u64(bytes, 16),
+            origin_timestamp: read_u64(bytes, 24),
+            batch_length,
+            batch_checksum: read_u64(bytes, BATCH_CHECKSUM_OFFSET),
+            message_count: read_u32(bytes, BATCH_MESSAGE_COUNT_OFFSET),
+        })
+    }
+
+    /// # Panics
+    /// Panics if `bytes` is shorter than [`BATCH_HEADER_SIZE`].
+    pub fn encode_into(&self, bytes: &mut [u8]) {
+        assert!(bytes.len() >= BATCH_HEADER_SIZE);
+        bytes[..BATCH_HEADER_SIZE].fill(0);
+        bytes[0..8].copy_from_slice(&self.partition_id.to_le_bytes());
+        bytes[8..16].copy_from_slice(&self.base_offset.to_le_bytes());
+        bytes[16..24].copy_from_slice(&self.base_timestamp.to_le_bytes());
+        bytes[24..32].copy_from_slice(&self.origin_timestamp.to_le_bytes());
+        bytes[32..40].copy_from_slice(&self.batch_length.to_le_bytes());
+        bytes[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]
+            .copy_from_slice(&self.batch_checksum.to_le_bytes());
+        bytes[BATCH_MESSAGE_COUNT_OFFSET..BATCH_MESSAGE_COUNT_OFFSET + 4]
+            .copy_from_slice(&self.message_count.to_le_bytes());
+    }
+
+    /// Total batch size in bytes (header + blob).
+    ///
+    /// # Panics
+    /// Panics if `batch_length` exceeds `usize::MAX`.
+    #[must_use]
+    pub fn total_size(&self) -> usize {
+        usize::try_from(self.batch_length).expect("batch length exceeds usize::MAX")
+    }
+
+    /// # Errors
+    /// [`WireError::Validation`] if `batch_length` does not cover the header
+    /// or exceeds `usize`.
+    pub fn blob_len(&self) -> Result<usize, WireError> {
+        self.batch_length
+            .checked_sub(BATCH_HEADER_SIZE as u64)
+            .and_then(|len| usize::try_from(len).ok())
+            .ok_or(WireError::Validation(std::borrow::Cow::Borrowed(
+                "batch length must cover the batch header",
+            )))
+    }
+
+    #[must_use]
+    pub fn checksum_for_blob(&self, blob: &[u8]) -> u64 {
+        calculate_batch_checksum(self, blob)
+    }
+}
+
+/// A decoded batch borrowing its blob.
+#[derive(Debug, Clone, Copy)]
+pub struct BatchRef<'a> {
+    pub header: BatchHeader,
+    blob: &'a [u8],
+}
+
+impl<'a> BatchRef<'a> {
+    #[must_use]
+    pub const fn new(header: BatchHeader, blob: &'a [u8]) -> Self {
+        Self { header, blob }
+    }
+
+    #[must_use]
+    pub const fn iter(&self) -> BatchIterator<'a> {
+        BatchIterator {
+            blob: self.blob,
+            position: 0,
+        }
+    }
+
+    #[must_use]
+    pub const fn iter_with_offsets(&self) -> BatchIteratorWithOffsets<'a> {
+        BatchIteratorWithOffsets {
+            blob: self.blob,
+            position: 0,
+        }
+    }
+
+    #[must_use]
+    pub const fn blob(&self) -> &'a [u8] {
+        self.blob
+    }
+
+    #[must_use]
+    pub const fn message_count(&self) -> u32 {
+        self.header.message_count
+    }
+}
+
+impl<'a> IntoIterator for &BatchRef<'a> {
+    type Item = BatchMessageView<'a>;
+    type IntoIter = BatchIterator<'a>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.iter()
+    }
+}
+
+/// A message frame header inside the blob.
+#[derive(Debug, Clone, Copy)]
+pub struct BatchMessageHeader {
+    /// `XxHash3_64` over `frame[8..48] || payload || user_headers`.
+    pub checksum: u64,
+    pub id: u128,
+    pub offset_delta: u32,
+    /// Microsecond delta from [`BatchHeader::origin_timestamp`].
+    pub timestamp_delta: u32,
+    pub user_headers_length: u32,
+    pub payload_length: u32,
+}
+
+impl BatchMessageHeader {
+    /// # Errors
+    /// [`WireError::UnexpectedEof`] on a short buffer; [`WireError::Validation`]
+    /// on nonzero reserved bytes.
+    pub fn decode(bytes: &[u8]) -> Result<Self, WireError> {
+        if bytes.len() < BATCH_MESSAGE_HEADER_SIZE {
+            return Err(WireError::UnexpectedEof {
+                offset: 0,
+                need: BATCH_MESSAGE_HEADER_SIZE,
+                have: bytes.len(),
+            });
+        }
+
+        if read_u64(bytes, 40) != 0 {
+            return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+                "message frame reserved bytes must be zero",
+            )));
+        }
+
+        Ok(Self {
+            checksum: read_u64(bytes, 0),
+            id: read_u128(bytes, 8),
+            offset_delta: read_u32(bytes, 24),
+            timestamp_delta: read_u32(bytes, 28),
+            user_headers_length: read_u32(bytes, 32),
+            payload_length: read_u32(bytes, 36),
+        })
+    }
+
+    /// Frame size: header + payload + user headers.
+    #[must_use]
+    pub const fn total_size(&self) -> usize {
+        BATCH_MESSAGE_HEADER_SIZE + self.user_headers_length as usize + self.payload_length as usize
+    }
+}
+
+/// A message frame view borrowing payload and user headers from the blob.
+#[derive(Debug, Clone, Copy)]
+pub struct BatchMessageView<'a> {
+    pub header: BatchMessageHeader,
+    pub user_headers: &'a [u8],
+    pub payload: &'a [u8],
+}
+
+/// Infallible frame walk over a blob whose layout has already been proven by
+/// [`decode_batch_slice_with`]: on an unvalidated blob it stops at the first
+/// malformed frame instead of erroring.
+pub struct BatchIterator<'a> {
+    blob: &'a [u8],
+    position: usize,
+}
+
+impl<'a> Iterator for BatchIterator<'a> {
+    type Item = BatchMessageView<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.position >= self.blob.len() {
+            return None;
+        }
+
+        let header = BatchMessageHeader::decode(&self.blob[self.position..]).ok()?;
+        let start = self.position + BATCH_MESSAGE_HEADER_SIZE;
+        let payload_end = start + header.payload_length as usize;
+        let headers_end = payload_end + header.user_headers_length as usize;
+        let payload = self.blob.get(start..payload_end)?;
+        let user_headers = self.blob.get(payload_end..headers_end)?;
+        self.position += header.total_size();
+        Some(BatchMessageView {
+            header,
+            user_headers,
+            payload,
+        })
+    }
+}
+
+/// A frame view plus its byte range inside the blob.
+#[derive(Debug, Clone, Copy)]
+pub struct BatchMessageViewWithOffsets<'a> {
+    pub message: BatchMessageView<'a>,
+    pub start: usize,
+    pub end: usize,
+}
+
+pub struct BatchIteratorWithOffsets<'a> {
+    blob: &'a [u8],
+    position: usize,
+}
+
+impl<'a> Iterator for BatchIteratorWithOffsets<'a> {
+    type Item = BatchMessageViewWithOffsets<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.position >= self.blob.len() {
+            return None;
+        }
+
+        let start = self.position;
+        let header = BatchMessageHeader::decode(&self.blob[self.position..]).ok()?;
+        let message_start = self.position + BATCH_MESSAGE_HEADER_SIZE;
+        let payload_end = message_start + header.payload_length as usize;
+        let headers_end = payload_end + header.user_headers_length as usize;
+        let payload = self.blob.get(message_start..payload_end)?;
+        let user_headers = self.blob.get(payload_end..headers_end)?;
+        self.position += header.total_size();
+        Some(BatchMessageViewWithOffsets {
+            message: BatchMessageView {
+                header,
+                user_headers,
+                payload,
+            },
+            start,
+            end: self.position,
+        })
+    }
+}
+
+/// How much of a batch record [`decode_batch_slice_with`] proves before returning it.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum BatchIntegrity {
+    /// Re-hash the batch and reject it unless it matches its own `batch_checksum`.
+    Verify,
+    /// Check the framing only, and hand back whatever it describes. The caller is
+    /// accepting bytes that may not be the ones written.
+    LayoutOnly,
+}
+
+/// Decode one batch record (`[256B header][blob]`), verifying the batch
+/// checksum and every per-message checksum.
+///
+/// `body` may extend past the batch: readers walking a stream of records hand
+/// in the rest of the buffer and step by `batch_length`. Callers whose buffer
+/// is meant to BE the batch must reject the surplus themselves.
+///
+/// # Errors
+/// [`WireError`] on a short or self-inconsistent record, and
+/// [`WireError::InvalidBatchChecksum`] / [`WireError::InvalidMessageChecksum`]
+/// on an integrity mismatch.
+pub fn decode_batch_slice(body: &[u8]) -> Result<BatchRef<'_>, WireError> {
+    decode_batch_slice_with(body, BatchIntegrity::Verify)
+}
+
+/// [`decode_batch_slice`] with the integrity level chosen by the caller.
+///
+/// Layout checks are not optional either way: a short or self-inconsistent
+/// record is rejected regardless, because the caller would otherwise index
+/// past it.
+///
+/// # Errors
+/// See [`decode_batch_slice`]; [`BatchIntegrity::LayoutOnly`] skips only the
+/// checksum comparison.
+pub fn decode_batch_slice_with(
+    body: &[u8],
+    integrity: BatchIntegrity,
+) -> Result<BatchRef<'_>, WireError> {
+    let header = BatchHeader::decode(body)?;
+    let blob_len = header.blob_len()?;
+    if body.len() < header.total_size() {
+        return Err(WireError::UnexpectedEof {
+            offset: 0,
+            need: header.total_size(),
+            have: body.len(),
+        });
+    }
+
+    let blob = &body[BATCH_HEADER_SIZE..BATCH_HEADER_SIZE + blob_len];
+    let batch = BatchRef { header, blob };
+    match integrity {
+        BatchIntegrity::Verify => {
+            let expected_checksum = verify_and_recompute_batch_checksum(&batch)?;
+            if header.batch_checksum != expected_checksum {
+                return Err(WireError::InvalidBatchChecksum {
+                    stored: header.batch_checksum,
+                    computed: expected_checksum,
+                    base_offset: header.base_offset,
+                });
+            }
+        }
+        BatchIntegrity::LayoutOnly => validate_batch_layout(&batch)?,
+    }
+
+    Ok(batch)
+}
+
+/// Batch checksum: streaming `XxHash3_64` over the six batch header meta
+/// fields followed by each message's stored 8-byte checksum field in message
+/// order - NOT the message bodies.
+///
+/// Bodies are bound only transitively: each per-message checksum already covers
+/// `frame[8..48] || payload || user_headers`, so hashing the checksum fields
+/// binds every body byte IFF a reader also re-verifies the per-message
+/// checksums. Stamping hashes `N * 8` bytes instead of the whole blob;
+/// validating decoders pay the one body pass as the per-message verify in
+/// [`verify_and_recompute_batch_checksum`], which hashes the checksum-field
+/// bytes in the same order so its recompute matches a compute here.
+///
+/// Assumes a well-formed blob whose frames tile exactly; every compute site
+/// builds the blob and satisfies this.
+#[must_use]
+pub fn calculate_batch_checksum(header: &BatchHeader, blob: &[u8]) -> u64 {
+    let mut hasher = XxHash3_64::new();
+    write_batch_header_fields(&mut hasher, header);
+    let batch = BatchRef {
+        header: *header,
+        blob,
+    };
+    for framed in batch.iter_with_offsets() {
+        hasher.write(&blob[framed.start..framed.start + 8]);
+    }
+    hasher.finish()
+}
+
+fn write_batch_header_fields(hasher: &mut XxHash3_64, header: &BatchHeader) {
+    hasher.write(&header.partition_id.to_le_bytes());
+    hasher.write(&header.base_offset.to_le_bytes());
+    hasher.write(&header.base_timestamp.to_le_bytes());
+    hasher.write(&header.origin_timestamp.to_le_bytes());
+    hasher.write(&header.batch_length.to_le_bytes());
+    hasher.write(&header.message_count.to_le_bytes());
+}
+
+/// Verify every per-message checksum in `batch` and return the recomputed
+/// batch checksum (see [`calculate_batch_checksum`]) from a single frame walk.
+///
+/// The per-message pass is the equal-integrity half of the scheme: the batch
+/// value binds bodies only through the checksum fields, so a validating decode
+/// must re-verify each message here or body corruption that leaves the
+/// checksum field intact would pass. This is the one full-body pass a
+/// validating decode pays; the caller then compares the returned value against
+/// the stored `batch_checksum`.
+///
+/// # Errors
+/// [`WireError::InvalidMessageChecksum`] on the first per-message mismatch;
+/// [`WireError::Validation`] if the frames do not tile `message_count` exactly.
+pub fn verify_and_recompute_batch_checksum(batch: &BatchRef<'_>) -> Result<u64, WireError> {
+    let blob = batch.blob();
+    let mut hasher = XxHash3_64::new();
+    write_batch_header_fields(&mut hasher, &batch.header);
+    let mut verified = 0u32;
+    let mut covered = 0usize;
+    for framed in batch.iter_with_offsets() {
+        // Cover (`frame[8..48] || payload || user_headers`) hashed raw from the
+        // blob, byte-exact with the encoder's, so a flipped body byte fails even
+        // when the stored checksum field is left intact.
+        let stored = framed.message.header.checksum;
+        let expected = XxHash3_64::oneshot(&blob[framed.start + 8..framed.end]);
+        if expected != stored {
+            return Err(WireError::InvalidMessageChecksum {
+                stored,
+                computed: expected,
+                offset: batch
+                    .header
+                    .base_offset
+                    .saturating_add(u64::from(framed.message.header.offset_delta)),
+            });
+        }
+        hasher.write(&blob[framed.start..framed.start + 8]);
+        verified += 1;
+        covered = framed.end;
+    }
+    if verified != batch.message_count() || covered != blob.len() {
+        return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+            "batch frames do not tile message_count exactly",
+        )));
+    }
+    Ok(hasher.finish())
+}
+
+/// Layout-only twin of [`verify_and_recompute_batch_checksum`]: prove the
+/// frames tile `message_count` exactly without touching any checksum.
+///
+/// # Errors
+/// [`WireError::Validation`] if the frames do not tile `message_count` exactly.
+fn validate_batch_layout(batch: &BatchRef<'_>) -> Result<(), WireError> {
+    let blob = batch.blob();
+    let mut counted = 0u32;
+    let mut covered = 0usize;
+    for framed in batch.iter_with_offsets() {
+        counted += 1;
+        covered = framed.end;
+    }
+    if counted != batch.message_count() || covered != blob.len() {
+        return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+            "batch frames do not tile message_count exactly",
+        )));
+    }
+    Ok(())
+}
+
+fn read_u32(bytes: &[u8], offset: usize) -> u32 {
+    u32::from_le_bytes(bytes[offset..offset + 4].try_into().expect("4-byte slice"))
+}
+
+fn read_u64(bytes: &[u8], offset: usize) -> u64 {
+    u64::from_le_bytes(bytes[offset..offset + 8].try_into().expect("8-byte slice"))
+}
+
+fn read_u128(bytes: &[u8], offset: usize) -> u128 {
+    u128::from_le_bytes(
+        bytes[offset..offset + 16]
+            .try_into()
+            .expect("16-byte slice"),
+    )
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[allow(clippy::cast_possible_truncation)]
+    fn frame(id: u128, offset_delta: u32, timestamp_delta: u32, payload: &[u8]) -> Vec<u8> {
+        let mut bytes = vec![0u8; BATCH_MESSAGE_HEADER_SIZE];
+        bytes[8..24].copy_from_slice(&id.to_le_bytes());
+        bytes[24..28].copy_from_slice(&offset_delta.to_le_bytes());
+        bytes[28..32].copy_from_slice(&timestamp_delta.to_le_bytes());
+        bytes[36..40].copy_from_slice(&(payload.len() as u32).to_le_bytes());
+        bytes.extend_from_slice(payload);
+        let checksum = XxHash3_64::oneshot(&bytes[8..]);
+        bytes[0..8].copy_from_slice(&checksum.to_le_bytes());
+        bytes
+    }
+
+    #[allow(clippy::cast_possible_truncation)]
+    fn batch_bytes(frames: &[Vec<u8>]) -> Vec<u8> {
+        let blob: Vec<u8> = frames.concat();
+        let mut header = BatchHeader::new(
+            7,
+            1_000,
+            (BATCH_HEADER_SIZE + blob.len()) as u64,
+            frames.len() as u32,
+        );
+        header.batch_checksum = calculate_batch_checksum(&header, &blob);
+        let mut bytes = vec![0u8; BATCH_HEADER_SIZE];
+        header.encode_into(&mut bytes);
+        bytes.extend_from_slice(&blob);
+        bytes
+    }
+
+    #[test]
+    fn header_roundtrip() {
+        let mut header = BatchHeader::new(1, 2, 300, 4);
+        header.base_offset = 10;
+        header.base_timestamp = 20;
+        header.batch_checksum = 30;
+        let mut bytes = vec![0u8; BATCH_HEADER_SIZE];
+        header.encode_into(&mut bytes);
+        let decoded = BatchHeader::decode(&bytes).unwrap();
+        assert_eq!(decoded, header);
+    }
+
+    #[test]
+    fn decode_verifies_checksums() {
+        let bytes = batch_bytes(&[frame(1, 0, 0, b"first"), frame(2, 1, 5, b"second")]);
+        let batch = decode_batch_slice(&bytes).unwrap();
+        assert_eq!(batch.message_count(), 2);
+        let views: Vec<_> = batch.iter().collect();
+        assert_eq!(views[0].payload, b"first");
+        assert_eq!(views[1].payload, b"second");
+        assert_eq!(views[1].header.offset_delta, 1);
+    }
+
+    #[test]
+    fn decode_rejects_flipped_body_byte() {
+        let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+        let last = bytes.len() - 1;
+        bytes[last] ^= 0xFF;
+        assert!(matches!(
+            decode_batch_slice(&bytes),
+            Err(WireError::InvalidMessageChecksum { .. })
+        ));
+    }
+
+    #[test]
+    fn decode_rejects_flipped_batch_checksum() {
+        let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+        bytes[BATCH_CHECKSUM_OFFSET] ^= 0xFF;
+        assert!(matches!(
+            decode_batch_slice(&bytes),
+            Err(WireError::InvalidBatchChecksum { .. })
+        ));
+    }
+
+    #[test]
+    fn layout_only_accepts_zero_checksums() {
+        let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+        bytes[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8].fill(0);
+        let batch = decode_batch_slice_with(&bytes, BatchIntegrity::LayoutOnly).unwrap();
+        assert_eq!(batch.message_count(), 1);
+    }
+
+    #[test]
+    fn decode_rejects_miscounted_batch() {
+        let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+        bytes[BATCH_MESSAGE_COUNT_OFFSET..BATCH_MESSAGE_COUNT_OFFSET + 4]
+            .copy_from_slice(&2u32.to_le_bytes());
+        assert!(decode_batch_slice_with(&bytes, BatchIntegrity::LayoutOnly).is_err());
+    }
+
+    #[test]
+    fn decode_rejects_truncated_batch() {
+        let bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+        assert!(decode_batch_slice(&bytes[..bytes.len() - 1]).is_err());
+    }
+
+    #[test]
+    fn decode_rejects_nonzero_reserved() {
+        let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+        bytes[BATCH_HEADER_SIZE + 40] = 1;
+        assert!(decode_batch_slice_with(&bytes, BatchIntegrity::LayoutOnly).is_err());
+    }
+
+    #[test]
+    fn decode_rejects_nonzero_header_reserved() {
+        let mut bytes = batch_bytes(&[frame(1, 0, 0, b"payload")]);
+        bytes[BATCH_RESERVED_OFFSET] = 1;
+        assert!(matches!(
+            BatchHeader::decode(&bytes),
+            Err(WireError::Validation(_))
+        ));
+        bytes[BATCH_RESERVED_OFFSET] = 0;
+        bytes[BATCH_HEADER_SIZE - 1] = 1;
+        assert!(BatchHeader::decode(&bytes).is_err());
+    }
+}
diff --git a/core/binary_protocol/src/codes.rs b/core/binary_protocol/src/codes.rs
index 9042029..85f0b97 100644
--- a/core/binary_protocol/src/codes.rs
+++ b/core/binary_protocol/src/codes.rs
@@ -58,8 +58,6 @@
 pub const GET_CONSUMER_OFFSET_CODE: u32 = 120;
 pub const STORE_CONSUMER_OFFSET_CODE: u32 = 121;
 pub const DELETE_CONSUMER_OFFSET_CODE: u32 = 122;
-pub const STORE_CONSUMER_OFFSET_2_CODE: u32 = 123;
-pub const DELETE_CONSUMER_OFFSET_2_CODE: u32 = 124;
 
 // -- Streams --
 pub const GET_STREAM_CODE: u32 = 200;
@@ -137,8 +135,6 @@
         GET_CONSUMER_OFFSET_CODE,
         STORE_CONSUMER_OFFSET_CODE,
         DELETE_CONSUMER_OFFSET_CODE,
-        STORE_CONSUMER_OFFSET_2_CODE,
-        DELETE_CONSUMER_OFFSET_2_CODE,
         GET_STREAM_CODE,
         GET_STREAMS_CODE,
         CREATE_STREAM_CODE,
diff --git a/core/binary_protocol/src/consensus/command.rs b/core/binary_protocol/src/consensus/command.rs
index f358c3c..81e8033 100644
--- a/core/binary_protocol/src/consensus/command.rs
+++ b/core/binary_protocol/src/consensus/command.rs
@@ -21,7 +21,7 @@
 /// VSR message type discriminant.
 #[derive(Default, Debug, EnumSetType)]
 #[repr(u8)]
-pub enum Command2 {
+pub enum Command {
     #[default]
     Reserved = 0,
 
@@ -85,11 +85,11 @@
     ForwardLogoutResult = 29,
 }
 
-// SAFETY: Command2 is #[repr(u8)] with no padding bytes.
-unsafe impl NoUninit for Command2 {}
+// SAFETY: Command is #[repr(u8)] with no padding bytes.
+unsafe impl NoUninit for Command {}
 
-// SAFETY: Command2 is #[repr(u8)]; is_valid_bit_pattern matches all defined discriminants.
-unsafe impl CheckedBitPattern for Command2 {
+// SAFETY: Command is #[repr(u8)]; is_valid_bit_pattern matches all defined discriminants.
+unsafe impl CheckedBitPattern for Command {
     type Bits = u8;
 
     fn is_valid_bit_pattern(bits: &u8) -> bool {
diff --git a/core/binary_protocol/src/consensus/error.rs b/core/binary_protocol/src/consensus/error.rs
index b424660..02fb044 100644
--- a/core/binary_protocol/src/consensus/error.rs
+++ b/core/binary_protocol/src/consensus/error.rs
@@ -15,13 +15,13 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use super::command::Command2;
+use super::command::Command;
 use thiserror::Error;
 
 #[derive(Debug, Clone, Error, PartialEq, Eq)]
 pub enum ConsensusError {
     #[error("invalid command: expected {expected:?}, found {found:?}")]
-    InvalidCommand { expected: Command2, found: Command2 },
+    InvalidCommand { expected: Command, found: Command },
 
     #[error("invalid size: expected {expected:?}, found {found:?}")]
     InvalidSize { expected: u32, found: u32 },
@@ -41,7 +41,7 @@
         }
     )]
     FrameChecksumMismatch {
-        command: Command2,
+        command: Command,
         expected: u128,
         found: u128,
     },
@@ -62,13 +62,13 @@
     PrepareRequestChecksumPaddingNonZero,
 
     #[error("command must be Commit")]
-    CommitInvalidCommand2,
+    CommitInvalidCommand,
 
     #[error("size must be 256, found {0}")]
     CommitInvalidSize(u32),
 
     #[error("command must be Reply")]
-    ReplyInvalidCommand2,
+    ReplyInvalidCommand,
 
     #[error("request_checksum_padding must be 0")]
     ReplyRequestChecksumPaddingNonZero,
@@ -80,5 +80,5 @@
     InvalidBitPattern,
 
     #[error("client-bound command {0:?} cannot be dispatched on inbound path")]
-    ClientBoundCommand(Command2),
+    ClientBoundCommand(Command),
 }
diff --git a/core/binary_protocol/src/consensus/header.rs b/core/binary_protocol/src/consensus/header.rs
index 9dd4d7d..2f205df 100644
--- a/core/binary_protocol/src/consensus/header.rs
+++ b/core/binary_protocol/src/consensus/header.rs
@@ -42,7 +42,7 @@
 //! `checksum` as their view-independent identity, and the three client-facing
 //! headers are sealed on neither side, so SDKs are untouched.
 
-use super::{Command2, ConsensusError, Operation};
+use super::{Command, ConsensusError, Operation};
 use bytemuck::{CheckedBitPattern, NoUninit};
 use std::mem::offset_of;
 
@@ -101,13 +101,13 @@
 /// strict allocators (Miri, jemalloc, arenas). Use
 /// `aligned_vec::AVec<u8, ConstAlign<16>>` for explicit alignment.
 pub trait ConsensusHeader: Sized + CheckedBitPattern + NoUninit {
-    const COMMAND: Command2;
+    const COMMAND: Command;
 
     /// Whether a frame carrying `command` may be typed as this header.
     /// Defaults to an exact match; a header that serves several commands
     /// with one layout (e.g. `RepairDone` / `RangeEvicted`) widens it.
     #[must_use]
-    fn accepts(command: Command2) -> bool {
+    fn accepts(command: Command) -> bool {
         command == Self::COMMAND
     }
 
@@ -135,7 +135,7 @@
     /// Returns `ConsensusError` if the header fields are inconsistent.
     fn validate(&self) -> Result<(), ConsensusError>;
     fn operation(&self) -> Operation;
-    fn command(&self) -> Command2;
+    fn command(&self) -> Command;
     fn size(&self) -> u32;
 
     /// The `checksum` field, whatever this header spends it on.
@@ -205,7 +205,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
     pub reserved_command: [u8; RESERVED_COMMAND_LEN],
@@ -227,7 +227,7 @@
 };
 
 impl ConsensusHeader for GenericHeader {
-    const COMMAND: Command2 = Command2::Reserved;
+    const COMMAND: Command = Command::Reserved;
     const FRAME_SEALED: bool = false;
 
     fn checksum(&self) -> u128 {
@@ -240,7 +240,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn validate(&self) -> Result<(), ConsensusError> {
@@ -263,7 +263,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -333,7 +333,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -374,7 +374,7 @@
             size: 0,
             view: 0,
             release: 0,
-            command: Command2::Reserved,
+            command: Command::Reserved,
             replica: 0,
             reserved_frame: [0; 66],
             client: 0,
@@ -400,7 +400,7 @@
             size: 0,
             view: 0,
             release: 0,
-            command: Command2::Reserved,
+            command: Command::Reserved,
             replica: 0,
             reserved_frame: [0; 66],
             client: 0,
@@ -477,7 +477,7 @@
 }
 
 impl ConsensusHeader for RoutedRequestHeader {
-    const COMMAND: Command2 = Command2::Request;
+    const COMMAND: Command = Command::Request;
     /// The client-wire [`RequestHeader`] this is promoted from is unsealed, and the
     /// promotion copies `checksum` verbatim, so there is nothing here to verify.
     const FRAME_SEALED: bool = false;
@@ -492,7 +492,7 @@
     fn operation(&self) -> Operation {
         self.operation
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -500,9 +500,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::Request {
+        if self.command != Command::Request {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::Request,
+                expected: Command::Request,
                 found: self.command,
             });
         }
@@ -511,7 +511,7 @@
 }
 
 impl ConsensusHeader for RequestHeader {
-    const COMMAND: Command2 = Command2::Request;
+    const COMMAND: Command = Command::Request;
     const FRAME_SEALED: bool = false;
 
     fn checksum(&self) -> u128 {
@@ -524,7 +524,7 @@
     fn operation(&self) -> Operation {
         self.operation
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -532,9 +532,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::Request {
+        if self.command != Command::Request {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::Request,
+                expected: Command::Request,
                 found: self.command,
             });
         }
@@ -554,7 +554,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -602,7 +602,7 @@
             size: 0,
             view: 0,
             release: 0,
-            command: Command2::Reserved,
+            command: Command::Reserved,
             replica: 0,
             reserved_frame: [0; 66],
             request_checksum: 0,
@@ -621,7 +621,7 @@
 }
 
 impl ConsensusHeader for ReplyHeader {
-    const COMMAND: Command2 = Command2::Reply;
+    const COMMAND: Command = Command::Reply;
     const FRAME_SEALED: bool = false;
 
     fn checksum(&self) -> u128 {
@@ -634,7 +634,7 @@
     fn operation(&self) -> Operation {
         self.operation
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -642,8 +642,8 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::Reply {
-            return Err(ConsensusError::ReplyInvalidCommand2);
+        if self.command != Command::Reply {
+            return Err(ConsensusError::ReplyInvalidCommand);
         }
         Ok(())
     }
@@ -713,7 +713,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -781,7 +781,7 @@
             size: HEADER_SIZE as u32,
             view,
             release: 0,
-            command: Command2::Eviction,
+            command: Command::Eviction,
             replica,
             reserved_frame: [0; 66],
             client,
@@ -812,7 +812,7 @@
 }
 
 impl ConsensusHeader for EvictionHeader {
-    const COMMAND: Command2 = Command2::Eviction;
+    const COMMAND: Command = Command::Eviction;
     const FRAME_SEALED: bool = false;
 
     fn checksum(&self) -> u128 {
@@ -826,7 +826,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -835,9 +835,9 @@
 
     #[allow(clippy::cast_possible_truncation)]
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::Eviction {
+        if self.command != Command::Eviction {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::Eviction,
+                expected: Command::Eviction,
                 found: self.command,
             });
         }
@@ -905,7 +905,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -951,7 +951,7 @@
             size: 0,
             view: 0,
             release: 0,
-            command: Command2::Reserved,
+            command: Command::Reserved,
             replica: 0,
             reserved_frame: [0; 66],
             client: 0,
@@ -971,7 +971,7 @@
 }
 
 impl ConsensusHeader for PrepareHeader {
-    const COMMAND: Command2 = Command2::Prepare;
+    const COMMAND: Command = Command::Prepare;
     const FRAME_SEALED: bool = false;
 
     fn checksum(&self) -> u128 {
@@ -984,7 +984,7 @@
     fn operation(&self) -> Operation {
         self.operation
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -992,9 +992,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::Prepare {
+        if self.command != Command::Prepare {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::Prepare,
+                expected: Command::Prepare,
                 found: self.command,
             });
         }
@@ -1069,7 +1069,7 @@
 pub struct RepairPrepareHeader(pub PrepareHeader);
 
 impl ConsensusHeader for RepairPrepareHeader {
-    const COMMAND: Command2 = Command2::RepairPrepare;
+    const COMMAND: Command = Command::RepairPrepare;
     const FRAME_SEALED: bool = false;
 
     fn checksum(&self) -> u128 {
@@ -1082,7 +1082,7 @@
     fn operation(&self) -> Operation {
         self.0.operation
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.0.command
     }
     fn size(&self) -> u32 {
@@ -1090,9 +1090,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.0.command != Command2::RepairPrepare {
+        if self.0.command != Command::RepairPrepare {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::RepairPrepare,
+                expected: Command::RepairPrepare,
                 found: self.0.command,
             });
         }
@@ -1127,7 +1127,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -1160,7 +1160,7 @@
             size: 0,
             view: 0,
             release: 0,
-            command: Command2::Reserved,
+            command: Command::Reserved,
             replica: 0,
             reserved_frame: [0; 66],
             parent: 0,
@@ -1180,7 +1180,7 @@
 impl ConsensusHeader for PrepareOkHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::PrepareOk;
+    const COMMAND: Command = Command::PrepareOk;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -1192,7 +1192,7 @@
     fn operation(&self) -> Operation {
         self.operation
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -1200,9 +1200,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::PrepareOk {
+        if self.command != Command::PrepareOk {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::PrepareOk,
+                expected: Command::PrepareOk,
                 found: self.command,
             });
         }
@@ -1222,7 +1222,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -1245,7 +1245,7 @@
 impl ConsensusHeader for CommitHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::Commit;
+    const COMMAND: Command = Command::Commit;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -1257,7 +1257,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -1265,8 +1265,8 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::Commit {
-            return Err(ConsensusError::CommitInvalidCommand2);
+        if self.command != Command::Commit {
+            return Err(ConsensusError::CommitInvalidCommand);
         }
         if self.size != 256 {
             return Err(ConsensusError::CommitInvalidSize(self.size));
@@ -1287,7 +1287,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -1306,7 +1306,7 @@
 impl ConsensusHeader for StartViewChangeHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::StartViewChange;
+    const COMMAND: Command = Command::StartViewChange;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -1318,7 +1318,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -1326,9 +1326,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::StartViewChange {
+        if self.command != Command::StartViewChange {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::StartViewChange,
+                expected: Command::StartViewChange,
                 found: self.command,
             });
         }
@@ -1351,7 +1351,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -1414,7 +1414,7 @@
 impl ConsensusHeader for DoViewChangeHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::DoViewChange;
+    const COMMAND: Command = Command::DoViewChange;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -1426,7 +1426,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -1434,9 +1434,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::DoViewChange {
+        if self.command != Command::DoViewChange {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::DoViewChange,
+                expected: Command::DoViewChange,
                 found: self.command,
             });
         }
@@ -1525,7 +1525,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -1562,7 +1562,7 @@
 impl ConsensusHeader for StartViewHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::StartView;
+    const COMMAND: Command = Command::StartView;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -1574,7 +1574,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -1582,9 +1582,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::StartView {
+        if self.command != Command::StartView {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::StartView,
+                expected: Command::StartView,
                 found: self.command,
             });
         }
@@ -1637,7 +1637,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -1667,7 +1667,7 @@
 impl ConsensusHeader for RequestStartViewHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::RequestStartView;
+    const COMMAND: Command = Command::RequestStartView;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -1679,7 +1679,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -1687,9 +1687,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::RequestStartView {
+        if self.command != Command::RequestStartView {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::RequestStartView,
+                expected: Command::RequestStartView,
                 found: self.command,
             });
         }
@@ -1719,7 +1719,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -1741,7 +1741,7 @@
 impl ConsensusHeader for RequestPreparesHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::RequestPrepares;
+    const COMMAND: Command = Command::RequestPrepares;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -1753,7 +1753,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -1761,9 +1761,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::RequestPrepares {
+        if self.command != Command::RequestPrepares {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::RequestPrepares,
+                expected: Command::RequestPrepares,
                 found: self.command,
             });
         }
@@ -1792,7 +1792,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -1814,7 +1814,7 @@
 impl ConsensusHeader for RepairRangeReplyHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::RepairDone;
+    const COMMAND: Command = Command::RepairDone;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -1826,13 +1826,13 @@
     // One layout, two commands: `RepairDone` terminates a stream,
     // `RangeEvicted` prefixes it. Without this widening, `try_into_typed`
     // rejects `RangeEvicted` frames before `validate` ever sees them.
-    fn accepts(command: Command2) -> bool {
-        command == Command2::RepairDone || command == Command2::RangeEvicted
+    fn accepts(command: Command) -> bool {
+        command == Command::RepairDone || command == Command::RangeEvicted
     }
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -1840,9 +1840,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::RepairDone && self.command != Command2::RangeEvicted {
+        if self.command != Command::RepairDone && self.command != Command::RangeEvicted {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::RepairDone,
+                expected: Command::RepairDone,
                 found: self.command,
             });
         }
@@ -1874,7 +1874,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -1896,7 +1896,7 @@
 impl ConsensusHeader for RequestStateTransferHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::RequestStateTransfer;
+    const COMMAND: Command = Command::RequestStateTransfer;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -1908,7 +1908,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -1917,9 +1917,9 @@
 
     #[allow(clippy::cast_possible_truncation)]
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::RequestStateTransfer {
+        if self.command != Command::RequestStateTransfer {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::RequestStateTransfer,
+                expected: Command::RequestStateTransfer,
                 found: self.command,
             });
         }
@@ -1956,7 +1956,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -2020,7 +2020,7 @@
 impl ConsensusHeader for StateTransferTargetHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::StateTransferTarget;
+    const COMMAND: Command = Command::StateTransferTarget;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -2032,7 +2032,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -2040,9 +2040,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::StateTransferTarget {
+        if self.command != Command::StateTransferTarget {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::StateTransferTarget,
+                expected: Command::StateTransferTarget,
                 found: self.command,
             });
         }
@@ -2096,7 +2096,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -2121,7 +2121,7 @@
 impl ConsensusHeader for RequestStateChunkHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::RequestStateChunk;
+    const COMMAND: Command = Command::RequestStateChunk;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -2133,7 +2133,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -2142,9 +2142,9 @@
 
     #[allow(clippy::cast_possible_truncation)]
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::RequestStateChunk {
+        if self.command != Command::RequestStateChunk {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::RequestStateChunk,
+                expected: Command::RequestStateChunk,
                 found: self.command,
             });
         }
@@ -2181,7 +2181,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -2205,7 +2205,7 @@
 impl ConsensusHeader for StateChunkHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::StateChunk;
+    const COMMAND: Command = Command::StateChunk;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -2217,7 +2217,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -2225,9 +2225,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::StateChunk {
+        if self.command != Command::StateChunk {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::StateChunk,
+                expected: Command::StateChunk,
                 found: self.command,
             });
         }
@@ -2257,7 +2257,7 @@
 /// peer could inject a `Request` + `Register` directly. Clients cannot
 /// reach this command, because every client frame is typed through
 /// [`RequestHeader`], whose `validate` rejects any command but
-/// [`Command2::Request`].
+/// [`Command::Request`].
 #[derive(Debug, Clone, Copy, PartialEq, Eq, CheckedBitPattern, NoUninit)]
 #[repr(C)]
 pub struct ForwardRegisterHeader {
@@ -2267,7 +2267,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -2299,7 +2299,7 @@
     // believed: a flipped bit there would commit a register under another user.
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::ForwardRegister;
+    const COMMAND: Command = Command::ForwardRegister;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -2311,7 +2311,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -2319,9 +2319,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::ForwardRegister {
+        if self.command != Command::ForwardRegister {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::ForwardRegister,
+                expected: Command::ForwardRegister,
                 found: self.command,
             });
         }
@@ -2345,7 +2345,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -2409,7 +2409,7 @@
 impl ConsensusHeader for ForwardRegisterResultHeader {
     const FRAME_SEALED: bool = true;
 
-    const COMMAND: Command2 = Command2::ForwardRegisterResult;
+    const COMMAND: Command = Command::ForwardRegisterResult;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -2421,7 +2421,7 @@
     fn operation(&self) -> Operation {
         Operation::Reserved
     }
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
     fn size(&self) -> u32 {
@@ -2429,9 +2429,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::ForwardRegisterResult {
+        if self.command != Command::ForwardRegisterResult {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::ForwardRegisterResult,
+                expected: Command::ForwardRegisterResult,
                 found: self.command,
             });
         }
@@ -2476,7 +2476,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -2504,7 +2504,7 @@
 
 impl ConsensusHeader for ForwardLogoutHeader {
     const FRAME_SEALED: bool = true;
-    const COMMAND: Command2 = Command2::ForwardLogout;
+    const COMMAND: Command = Command::ForwardLogout;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -2518,7 +2518,7 @@
         Operation::Reserved
     }
 
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
 
@@ -2527,9 +2527,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::ForwardLogout {
+        if self.command != Command::ForwardLogout {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::ForwardLogout,
+                expected: Command::ForwardLogout,
                 found: self.command,
             });
         }
@@ -2556,7 +2556,7 @@
     pub size: u32,
     pub view: u32,
     pub release: u32,
-    pub command: Command2,
+    pub command: Command,
     pub replica: u8,
     pub reserved_frame: [u8; 66],
 
@@ -2597,7 +2597,7 @@
 
 impl ConsensusHeader for ForwardLogoutResultHeader {
     const FRAME_SEALED: bool = true;
-    const COMMAND: Command2 = Command2::ForwardLogoutResult;
+    const COMMAND: Command = Command::ForwardLogoutResult;
 
     fn checksum(&self) -> u128 {
         self.checksum
@@ -2611,7 +2611,7 @@
         Operation::Reserved
     }
 
-    fn command(&self) -> Command2 {
+    fn command(&self) -> Command {
         self.command
     }
 
@@ -2620,9 +2620,9 @@
     }
 
     fn validate(&self) -> Result<(), ConsensusError> {
-        if self.command != Command2::ForwardLogoutResult {
+        if self.command != Command::ForwardLogoutResult {
             return Err(ConsensusError::InvalidCommand {
-                expected: Command2::ForwardLogoutResult,
+                expected: Command::ForwardLogoutResult,
                 found: self.command,
             });
         }
@@ -2694,14 +2694,14 @@
 #[cfg(test)]
 mod tests {
     use super::{
-        Command2, CommitHeader, ConsensusError, ConsensusHeader, DoViewChangeHeader,
-        EvictionHeader, EvictionReason, ForwardLogoutHeader, ForwardLogoutOutcome,
-        ForwardLogoutResultHeader, ForwardRegisterHeader, ForwardRegisterOutcome,
-        ForwardRegisterResultHeader, GenericHeader, HEADER_SIZE, Operation, PrepareHeader,
-        PrepareOkHeader, RepairPrepareHeader, RepairRangeReplyHeader, ReplyHeader, RequestHeader,
-        RequestPreparesHeader, RequestStartViewHeader, RequestStateChunkHeader,
-        RequestStateTransferHeader, RoutedRequestHeader, StartViewChangeHeader, StartViewHeader,
-        StateChunkHeader, StateTransferTargetHeader,
+        Command, CommitHeader, ConsensusError, ConsensusHeader, DoViewChangeHeader, EvictionHeader,
+        EvictionReason, ForwardLogoutHeader, ForwardLogoutOutcome, ForwardLogoutResultHeader,
+        ForwardRegisterHeader, ForwardRegisterOutcome, ForwardRegisterResultHeader, GenericHeader,
+        HEADER_SIZE, Operation, PrepareHeader, PrepareOkHeader, RepairPrepareHeader,
+        RepairRangeReplyHeader, ReplyHeader, RequestHeader, RequestPreparesHeader,
+        RequestStartViewHeader, RequestStateChunkHeader, RequestStateTransferHeader,
+        RoutedRequestHeader, StartViewChangeHeader, StartViewHeader, StateChunkHeader,
+        StateTransferTargetHeader,
     };
     use aligned_vec::{AVec, ConstAlign};
 
@@ -2832,7 +2832,7 @@
         }
 
         let prepare = PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             checksum: 0xdead_beef,
             ..Default::default()
         };
@@ -2856,14 +2856,14 @@
     fn generic_header_zero_copy() {
         let buf = aligned_zeroed(256);
         let header: &GenericHeader = bytemuck::checked::try_from_bytes(&buf).unwrap();
-        assert_eq!(header.command, Command2::Reserved);
+        assert_eq!(header.command, Command::Reserved);
         assert_eq!(header.size, 0);
     }
 
     #[test]
     fn request_header_zero_copy() {
         let mut buf = aligned_zeroed(256);
-        buf[60] = Command2::Request as u8;
+        buf[60] = Command::Request as u8;
         // client offset = 60 + 1 (replica) + 66 (reserved_frame) = 128.
         // validate rejects client == 0.
         buf[128] = 1;
@@ -2871,7 +2871,7 @@
         // operation is `Reserved`, which validate rejects.
         buf[std::mem::offset_of!(RequestHeader, operation)] = Operation::Register as u8;
         let header: &RequestHeader = bytemuck::checked::try_from_bytes(&buf).unwrap();
-        assert_eq!(header.command, Command2::Request);
+        assert_eq!(header.command, Command::Request);
         assert!(header.validate().is_ok());
     }
 
@@ -2888,7 +2888,7 @@
     #[test]
     fn request_reserved_operation_rejected() {
         let header = RequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             client: 1,
             operation: Operation::Reserved,
             session: 1,
@@ -2901,7 +2901,7 @@
     #[test]
     fn request_register_nonzero_session_rejected() {
         let header = RequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             operation: Operation::Register,
             session: 5,
             request: 0,
@@ -2913,7 +2913,7 @@
     #[test]
     fn request_register_nonzero_request_rejected() {
         let header = RequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             operation: Operation::Register,
             session: 0,
             request: 1,
@@ -2925,7 +2925,7 @@
     #[test]
     fn request_non_register_valid() {
         let header = RequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             operation: Operation::SendMessages,
             client: 0xCAFE,
             session: 10,
@@ -2938,7 +2938,7 @@
     #[test]
     fn request_non_register_zero_session_rejected() {
         let header = RequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             operation: Operation::SendMessages,
             session: 0,
             request: 1,
@@ -2950,7 +2950,7 @@
     #[test]
     fn request_non_register_zero_request_rejected() {
         let header = RequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             operation: Operation::SendMessages,
             session: 10,
             request: 0,
@@ -2962,9 +2962,9 @@
     #[test]
     fn reply_header_zero_copy() {
         let mut buf = aligned_zeroed(256);
-        buf[60] = Command2::Reply as u8;
+        buf[60] = Command::Reply as u8;
         let header: &ReplyHeader = bytemuck::checked::try_from_bytes(&buf).unwrap();
-        assert_eq!(header.command, Command2::Reply);
+        assert_eq!(header.command, Command::Reply);
         assert!(header.validate().is_ok());
     }
 
@@ -3011,7 +3011,7 @@
     #[test]
     fn routed_request_zero_client_rejected() {
         let header = RoutedRequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             operation: Operation::SendMessages,
             session: 10,
             request: 1,
@@ -3023,7 +3023,7 @@
     #[test]
     fn routed_request_reserved_operation_rejected() {
         let header = RoutedRequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             client: 0xCAFE,
             session: 10,
             request: 1,
@@ -3040,7 +3040,7 @@
     #[test]
     fn routed_request_valid_fields_accepted() {
         let header = RoutedRequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             operation: Operation::SendMessages,
             client: 0xCAFE,
             session: 10,
@@ -3056,7 +3056,7 @@
     #[test]
     fn reply_header_nonzero_status_still_validates() {
         let header = ReplyHeader {
-            command: Command2::Reply,
+            command: Command::Reply,
             status: 41,
             ..ReplyHeader::default()
         };
@@ -3154,7 +3154,7 @@
             size: u32::try_from(HEADER_SIZE).expect("HEADER_SIZE fits u32"),
             view: 3,
             release: 0,
-            command: Command2::ForwardRegister,
+            command: Command::ForwardRegister,
             replica: 2,
             reserved_frame: [0; 66],
             client: 0xCAFE,
@@ -3175,7 +3175,7 @@
             size: u32::try_from(HEADER_SIZE).expect("HEADER_SIZE fits u32"),
             view: 3,
             release: 0,
-            command: Command2::ForwardRegisterResult,
+            command: Command::ForwardRegisterResult,
             replica: 0,
             reserved_frame: [0; 66],
             nonce: 0xF00D,
@@ -3193,11 +3193,11 @@
     fn forward_register_round_trips_through_generic_bytes() {
         for (command, bytes) in [
             (
-                Command2::ForwardRegister,
+                Command::ForwardRegister,
                 bytemuck::bytes_of(&forward_register()).to_vec(),
             ),
             (
-                Command2::ForwardRegisterResult,
+                Command::ForwardRegisterResult,
                 bytemuck::bytes_of(&forward_register_result(ForwardRegisterOutcome::Ok)).to_vec(),
             ),
         ] {
@@ -3335,7 +3335,7 @@
             size: u32::try_from(HEADER_SIZE).expect("HEADER_SIZE fits u32"),
             view: 3,
             release: 0,
-            command: Command2::ForwardLogout,
+            command: Command::ForwardLogout,
             replica: 2,
             reserved_frame: [0; 66],
             client: 0xCAFE,
@@ -3356,7 +3356,7 @@
             size: u32::try_from(HEADER_SIZE).expect("HEADER_SIZE fits u32"),
             view: 3,
             release: 0,
-            command: Command2::ForwardLogoutResult,
+            command: Command::ForwardLogoutResult,
             replica: 0,
             reserved_frame: [0; 66],
             nonce: 0xF00D,
@@ -3393,7 +3393,7 @@
             bytes.copy_from_slice(bytemuck::bytes_of(&result));
             let generic = bytemuck::checked::try_from_bytes::<GenericHeader>(&bytes)
                 .expect("forward logout result is a valid generic header");
-            assert_eq!(generic.command, Command2::ForwardLogoutResult);
+            assert_eq!(generic.command, Command::ForwardLogoutResult);
         }
     }
 
diff --git a/core/binary_protocol/src/consensus/mod.rs b/core/binary_protocol/src/consensus/mod.rs
index bac436e..d33f842 100644
--- a/core/binary_protocol/src/consensus/mod.rs
+++ b/core/binary_protocol/src/consensus/mod.rs
@@ -42,7 +42,7 @@
 mod operation;
 mod reply_result;
 
-pub use command::Command2;
+pub use command::Command;
 pub use error::ConsensusError;
 pub use header::{
     CHECKSUM_UNSEALED, CommitHeader, ConsensusHeader, DVC_HEADERS_MAX, DoViewChangeHeader,
diff --git a/core/binary_protocol/src/consensus/operation.rs b/core/binary_protocol/src/consensus/operation.rs
index 026ff9b..691bd9f 100644
--- a/core/binary_protocol/src/consensus/operation.rs
+++ b/core/binary_protocol/src/consensus/operation.rs
@@ -95,8 +95,6 @@
     SendMessages = 160,
     StoreConsumerOffset = 161,
     DeleteConsumerOffset = 162,
-    StoreConsumerOffset2 = 164,
-    DeleteConsumerOffset2 = 165,
 }
 
 impl Operation {
@@ -169,14 +167,7 @@
     /// the SDK, the one place that sees Register replies.
     #[must_use]
     pub const fn is_result_framed(&self) -> bool {
-        self.is_metadata()
-            || matches!(
-                self,
-                Self::StoreConsumerOffset
-                    | Self::StoreConsumerOffset2
-                    | Self::DeleteConsumerOffset
-                    | Self::DeleteConsumerOffset2
-            )
+        self.is_metadata() || matches!(self, Self::StoreConsumerOffset | Self::DeleteConsumerOffset)
     }
 
     /// Data-plane operations routed to the shard owning the partition.
@@ -247,9 +238,7 @@
             | Self::LeaveConsumerGroup
             | Self::SendMessages
             | Self::StoreConsumerOffset
-            | Self::DeleteConsumerOffset
-            | Self::StoreConsumerOffset2
-            | Self::DeleteConsumerOffset2 => match crate::dispatch::lookup_by_operation(*self) {
+            | Self::DeleteConsumerOffset => match crate::dispatch::lookup_by_operation(*self) {
                 Some(meta) => Some(meta.code),
                 None => None,
             },
@@ -300,8 +289,6 @@
             Operation::SendMessages,
             Operation::StoreConsumerOffset,
             Operation::DeleteConsumerOffset,
-            Operation::StoreConsumerOffset2,
-            Operation::DeleteConsumerOffset2,
         ];
         for op in ops {
             let code = op
@@ -367,8 +354,7 @@
         assert!(Operation::TruncatePartition.is_internal());
         assert!(Operation::TruncatePartition.is_metadata());
         assert!(!Operation::TruncatePartition.is_client_allowed());
+        assert!(Operation::StoreConsumerOffset.is_partition());
         assert!(Operation::DeleteConsumerOffset.is_partition());
-        assert!(Operation::StoreConsumerOffset2.is_partition());
-        assert!(Operation::DeleteConsumerOffset2.is_partition());
     }
 }
diff --git a/core/binary_protocol/src/dispatch.rs b/core/binary_protocol/src/dispatch.rs
index 576fadb..715fe71 100644
--- a/core/binary_protocol/src/dispatch.rs
+++ b/core/binary_protocol/src/dispatch.rs
@@ -126,16 +126,6 @@
         "consumer_offset.delete",
         Operation::DeleteConsumerOffset,
     ),
-    CommandMeta::replicated(
-        STORE_CONSUMER_OFFSET_2_CODE,
-        "consumer_offset.store.v2",
-        Operation::StoreConsumerOffset2,
-    ),
-    CommandMeta::replicated(
-        DELETE_CONSUMER_OFFSET_2_CODE,
-        "consumer_offset.delete.v2",
-        Operation::DeleteConsumerOffset2,
-    ),
     // Streams
     CommandMeta::non_replicated(GET_STREAM_CODE, "stream.get"),
     CommandMeta::non_replicated(GET_STREAMS_CODE, "stream.list"),
@@ -233,32 +223,30 @@
         GET_CONSUMER_OFFSET_CODE => 24,
         STORE_CONSUMER_OFFSET_CODE => 25,
         DELETE_CONSUMER_OFFSET_CODE => 26,
-        STORE_CONSUMER_OFFSET_2_CODE => 27,
-        DELETE_CONSUMER_OFFSET_2_CODE => 28,
-        GET_STREAM_CODE => 29,
-        GET_STREAMS_CODE => 30,
-        CREATE_STREAM_CODE => 31,
-        DELETE_STREAM_CODE => 32,
-        UPDATE_STREAM_CODE => 33,
-        PURGE_STREAM_CODE => 34,
-        GET_TOPIC_CODE => 35,
-        GET_TOPICS_CODE => 36,
-        CREATE_TOPIC_CODE => 37,
-        DELETE_TOPIC_CODE => 38,
-        UPDATE_TOPIC_CODE => 39,
-        PURGE_TOPIC_CODE => 40,
-        CREATE_PARTITIONS_CODE => 41,
-        DELETE_PARTITIONS_CODE => 42,
-        DELETE_SEGMENTS_CODE => 43,
-        GET_CONSUMER_GROUP_CODE => 44,
-        GET_CONSUMER_GROUPS_CODE => 45,
-        CREATE_CONSUMER_GROUP_CODE => 46,
-        DELETE_CONSUMER_GROUP_CODE => 47,
-        JOIN_CONSUMER_GROUP_CODE => 48,
-        LEAVE_CONSUMER_GROUP_CODE => 49,
-        SYNC_CONSUMER_GROUP_CODE => 50,
-        LOGIN_REGISTER_WITH_PAT_CODE => 51,
-        DESCRIBE_OPTIONS_CODE => 52,
+        GET_STREAM_CODE => 27,
+        GET_STREAMS_CODE => 28,
+        CREATE_STREAM_CODE => 29,
+        DELETE_STREAM_CODE => 30,
+        UPDATE_STREAM_CODE => 31,
+        PURGE_STREAM_CODE => 32,
+        GET_TOPIC_CODE => 33,
+        GET_TOPICS_CODE => 34,
+        CREATE_TOPIC_CODE => 35,
+        DELETE_TOPIC_CODE => 36,
+        UPDATE_TOPIC_CODE => 37,
+        PURGE_TOPIC_CODE => 38,
+        CREATE_PARTITIONS_CODE => 39,
+        DELETE_PARTITIONS_CODE => 40,
+        DELETE_SEGMENTS_CODE => 41,
+        GET_CONSUMER_GROUP_CODE => 42,
+        GET_CONSUMER_GROUPS_CODE => 43,
+        CREATE_CONSUMER_GROUP_CODE => 44,
+        DELETE_CONSUMER_GROUP_CODE => 45,
+        JOIN_CONSUMER_GROUP_CODE => 46,
+        LEAVE_CONSUMER_GROUP_CODE => 47,
+        SYNC_CONSUMER_GROUP_CODE => 48,
+        LOGIN_REGISTER_WITH_PAT_CODE => 49,
+        DESCRIBE_OPTIONS_CODE => 50,
         _ => return None,
     };
     Some(&COMMAND_TABLE[idx])
@@ -272,21 +260,21 @@
 pub const fn lookup_by_operation(op: Operation) -> Option<&'static CommandMeta> {
     // Indices must match the order of entries in COMMAND_TABLE above.
     let idx = match op {
-        Operation::CreateStream => 31,
-        Operation::UpdateStream => 33,
-        Operation::DeleteStream => 32,
-        Operation::PurgeStream => 34,
-        Operation::CreateTopic => 37,
-        Operation::UpdateTopic => 39,
-        Operation::DeleteTopic => 38,
-        Operation::PurgeTopic => 40,
-        Operation::CreatePartitions => 41,
-        Operation::DeletePartitions => 42,
-        Operation::DeleteSegments => 43,
-        Operation::CreateConsumerGroup => 46,
-        Operation::DeleteConsumerGroup => 47,
-        Operation::JoinConsumerGroup => 48,
-        Operation::LeaveConsumerGroup => 49,
+        Operation::CreateStream => 29,
+        Operation::UpdateStream => 31,
+        Operation::DeleteStream => 30,
+        Operation::PurgeStream => 32,
+        Operation::CreateTopic => 35,
+        Operation::UpdateTopic => 37,
+        Operation::DeleteTopic => 36,
+        Operation::PurgeTopic => 38,
+        Operation::CreatePartitions => 39,
+        Operation::DeletePartitions => 40,
+        Operation::DeleteSegments => 41,
+        Operation::CreateConsumerGroup => 44,
+        Operation::DeleteConsumerGroup => 45,
+        Operation::JoinConsumerGroup => 46,
+        Operation::LeaveConsumerGroup => 47,
         Operation::CreateUser => 9,
         Operation::UpdateUser => 11,
         Operation::DeleteUser => 10,
@@ -297,8 +285,6 @@
         Operation::SendMessages => 22,
         Operation::StoreConsumerOffset => 25,
         Operation::DeleteConsumerOffset => 26,
-        Operation::StoreConsumerOffset2 => 27,
-        Operation::DeleteConsumerOffset2 => 28,
         Operation::CreateTopicWithAssignments
         | Operation::CreatePartitionsWithAssignments
         | Operation::RemoveConsumerGroupMember
@@ -347,8 +333,6 @@
             GET_CONSUMER_OFFSET_CODE,
             STORE_CONSUMER_OFFSET_CODE,
             DELETE_CONSUMER_OFFSET_CODE,
-            STORE_CONSUMER_OFFSET_2_CODE,
-            DELETE_CONSUMER_OFFSET_2_CODE,
             GET_STREAM_CODE,
             GET_STREAMS_CODE,
             CREATE_STREAM_CODE,
@@ -433,8 +417,6 @@
             Operation::SendMessages,
             Operation::StoreConsumerOffset,
             Operation::DeleteConsumerOffset,
-            Operation::StoreConsumerOffset2,
-            Operation::DeleteConsumerOffset2,
         ];
         for op in replicated_ops {
             let meta = lookup_by_operation(op)
diff --git a/core/binary_protocol/src/error.rs b/core/binary_protocol/src/error.rs
index 82d5cb2..208b15c 100644
--- a/core/binary_protocol/src/error.rs
+++ b/core/binary_protocol/src/error.rs
@@ -49,4 +49,23 @@
 
     #[error("validation failed: {0}")]
     Validation(Cow<'static, str>),
+
+    #[error(
+        "invalid batch checksum: stored {stored}, computed {computed}, base offset {base_offset}"
+    )]
+    InvalidBatchChecksum {
+        stored: u64,
+        computed: u64,
+        base_offset: u64,
+    },
+
+    #[error("invalid message checksum: stored {stored}, computed {computed}, offset {offset}")]
+    InvalidMessageChecksum {
+        stored: u64,
+        computed: u64,
+        offset: u64,
+    },
+
+    #[error("message timestamp delta {0} exceeds the batch maximum")]
+    InvalidMessageTimestampDelta(u64),
 }
diff --git a/core/binary_protocol/src/framing.rs b/core/binary_protocol/src/framing.rs
index 9603834..dc1f5ed 100644
--- a/core/binary_protocol/src/framing.rs
+++ b/core/binary_protocol/src/framing.rs
@@ -20,10 +20,6 @@
 //! Encodes and decodes complete request/response frames without any I/O.
 //! The transport layer (TCP, QUIC, WebSocket) reads bytes into a buffer,
 //! then hands the buffer to these types for zero-copy parsing.
-//!
-//! When VSR consensus replaces this framing, the transport layer will
-//! switch to `consensus::header::GenericHeader` (256-byte fixed header)
-//! while the command payload codec stays the same.
 
 use crate::codec::{read_bytes, read_u32_le, read_u64_le};
 use crate::error::WireError;
@@ -34,168 +30,19 @@
 /// Status code for a successful response.
 pub const STATUS_OK: u32 = 0;
 
-/// Decoded request frame. Borrows the payload from the input buffer.
-///
-/// Wire format: `[length:4 LE][code:4 LE][payload:N]`
-/// where `length` = 4 (code size) + N (payload size).
-#[derive(Debug)]
-pub struct RequestFrame<'a> {
-    pub code: u32,
-    pub payload: &'a [u8],
-}
-
-impl<'a> RequestFrame<'a> {
-    /// Size of the frame header: `[length:4][code:4]`.
-    pub const HEADER_SIZE: usize = 8;
-
-    /// Validate a frame length field and return the payload size.
-    ///
-    /// Transport layers that read the length and code fields incrementally
-    /// (e.g. compio completion-based I/O) can use this to validate the length
-    /// before reading the payload, without buffering the entire frame.
-    ///
-    /// # Errors
-    /// Returns `WireError::Validation` if `frame_length < 4` (too small to
-    /// contain even the command code).
-    pub fn payload_length(frame_length: u32) -> Result<u32, WireError> {
-        frame_length
-            .checked_sub(4)
-            .ok_or(WireError::Validation(Cow::Borrowed(
-                "request frame length must be at least 4 (code size)",
-            )))
-    }
-
-    /// Construct a frame from pre-parsed header fields and a payload slice.
-    ///
-    /// Used by transport layers that read the header incrementally (e.g.
-    /// compio completion-based I/O) and then read the payload separately.
-    #[must_use]
-    pub const fn from_parts(code: u32, payload: &'a [u8]) -> Self {
-        Self { code, payload }
-    }
-
-    /// Decode a request frame from a complete buffer.
-    ///
-    /// # Errors
-    /// Returns `WireError::UnexpectedEof` if the buffer is too short.
-    pub fn decode(buf: &'a [u8]) -> Result<(Self, usize), WireError> {
-        let frame_length = read_u32_le(buf, 0)?;
-        let payload_len = Self::payload_length(frame_length)? as usize;
-        let code = read_u32_le(buf, 4)?;
-        let payload = read_bytes(buf, Self::HEADER_SIZE, payload_len)?;
-        let total = Self::HEADER_SIZE + payload_len;
-        Ok((Self { code, payload }, total))
-    }
-
-    /// Encode a request frame into `out`.
-    ///
-    /// Writes `[length:4 LE][code:4 LE][payload]` where length includes
-    /// the 4-byte code field.
-    ///
-    /// # Errors
-    /// Returns `WireError::PayloadTooLarge` if payload exceeds u32 capacity.
-    pub fn encode(code: u32, payload: &[u8], out: &mut BytesMut) -> Result<(), WireError> {
-        let length = payload
-            .len()
-            .checked_add(4)
-            .and_then(|n| u32::try_from(n).ok())
-            .ok_or(WireError::PayloadTooLarge {
-                size: payload.len(),
-                max: u32::MAX as usize - 4,
-            })?;
-        out.reserve(Self::HEADER_SIZE + payload.len());
-        out.put_u32_le(length);
-        out.put_u32_le(code);
-        out.put_slice(payload);
-        Ok(())
-    }
-
-    /// Total encoded size for a given payload length.
-    ///
-    /// Returns `None` if `HEADER_SIZE + payload_len` overflows `usize`.
-    #[must_use]
-    pub const fn encoded_size(payload_len: usize) -> Option<usize> {
-        Self::HEADER_SIZE.checked_add(payload_len)
-    }
-}
-
-/// Decoded response frame. Borrows the payload from the input buffer.
-///
-/// Wire format: `[status:4 LE][length:4 LE][payload:N]`
-/// where `status` = 0 for success, non-zero for error code.
-#[derive(Debug)]
-pub struct ResponseFrame<'a> {
-    pub status: u32,
-    pub payload: &'a [u8],
-}
-
-impl<'a> ResponseFrame<'a> {
-    /// Size of the frame header: `[status:4][length:4]`.
-    pub const HEADER_SIZE: usize = 8;
-
-    /// Decode a response frame from a complete buffer.
-    ///
-    /// # Errors
-    /// Returns `WireError::UnexpectedEof` if the buffer is too short.
-    pub fn decode(buf: &'a [u8]) -> Result<(Self, usize), WireError> {
-        let status = read_u32_le(buf, 0)?;
-        let length = read_u32_le(buf, 4)? as usize;
-        let payload = read_bytes(buf, Self::HEADER_SIZE, length)?;
-        let total = Self::HEADER_SIZE + length;
-        Ok((Self { status, payload }, total))
-    }
-
-    /// Encode a successful response with payload.
-    ///
-    /// # Errors
-    /// Returns `WireError::PayloadTooLarge` if payload exceeds u32 capacity.
-    pub fn encode_ok(payload: &[u8], out: &mut BytesMut) -> Result<(), WireError> {
-        let length = u32::try_from(payload.len()).map_err(|_| WireError::PayloadTooLarge {
-            size: payload.len(),
-            max: u32::MAX as usize,
-        })?;
-        out.reserve(Self::HEADER_SIZE + payload.len());
-        out.put_u32_le(STATUS_OK);
-        out.put_u32_le(length);
-        out.put_slice(payload);
-        Ok(())
-    }
-
-    /// Encode an error response (status code, empty payload).
-    pub fn encode_error(status: NonZeroU32, out: &mut BytesMut) {
-        out.reserve(Self::HEADER_SIZE);
-        out.put_u32_le(status.get());
-        out.put_u32_le(0);
-    }
-
-    /// Returns `true` if this is a success response.
-    #[must_use]
-    pub const fn is_ok(&self) -> bool {
-        self.status == STATUS_OK
-    }
-
-    /// Total encoded size for a given payload length.
-    ///
-    /// Returns `None` if `HEADER_SIZE + payload_len` overflows `usize`.
-    #[must_use]
-    pub const fn encoded_size(payload_len: usize) -> Option<usize> {
-        Self::HEADER_SIZE.checked_add(payload_len)
-    }
-}
-
 /// Decoded request frame with request ID for request-response correlation
 /// and consensus-level duplicate detection (the server framing).
 ///
 /// Wire format: `[length:4 LE][code:4 LE][request_id:8 LE][payload:N]`
 /// where `length` = 4 (code) + 8 (`request_id`) + N (payload).
 #[derive(Debug)]
-pub struct RequestFrame2<'a> {
+pub struct RequestFrame<'a> {
     pub code: u32,
     pub request_id: u64,
     pub payload: &'a [u8],
 }
 
-impl<'a> RequestFrame2<'a> {
+impl<'a> RequestFrame<'a> {
     /// Size of the frame header: `[length:4][code:4][request_id:8]`.
     pub const HEADER_SIZE: usize = 16;
 
@@ -287,13 +134,13 @@
 /// Wire format: `[status:4 LE][length:4 LE][request_id:8 LE][payload:N]`
 /// where `status` = 0 for success, non-zero for error code.
 #[derive(Debug)]
-pub struct ResponseFrame2<'a> {
+pub struct ResponseFrame<'a> {
     pub status: u32,
     pub request_id: u64,
     pub payload: &'a [u8],
 }
 
-impl<'a> ResponseFrame2<'a> {
+impl<'a> ResponseFrame<'a> {
     /// Size of the frame header: `[status:4][length:4][request_id:8]`.
     pub const HEADER_SIZE: usize = 16;
 
@@ -376,152 +223,12 @@
     use super::*;
 
     #[test]
-    fn request_roundtrip() {
-        let payload = b"hello world";
-        let mut buf = BytesMut::with_capacity(RequestFrame::encoded_size(payload.len()).unwrap());
-        RequestFrame::encode(42, payload, &mut buf).unwrap();
-
-        let (frame, consumed) = RequestFrame::decode(&buf).unwrap();
-        assert_eq!(consumed, buf.len());
-        assert_eq!(frame.code, 42);
-        assert_eq!(frame.payload, payload);
-    }
-
-    #[test]
-    fn request_empty_payload() {
-        let mut buf = BytesMut::with_capacity(RequestFrame::HEADER_SIZE);
-        RequestFrame::encode(1, &[], &mut buf).unwrap();
-
-        let (frame, consumed) = RequestFrame::decode(&buf).unwrap();
-        assert_eq!(consumed, 8);
-        assert_eq!(frame.code, 1);
-        assert!(frame.payload.is_empty());
-    }
-
-    #[test]
-    fn request_length_field_includes_code() {
-        let payload = b"test";
-        let mut buf = BytesMut::new();
-        RequestFrame::encode(99, payload, &mut buf).unwrap();
-
-        let length = u32::from_le_bytes(buf[0..4].try_into().unwrap());
-        assert_eq!(length, 4 + 4); // code(4) + payload(4)
-    }
-
-    #[test]
-    fn request_truncated_header() {
-        let buf = [0u8; 7]; // less than HEADER_SIZE
-        assert!(RequestFrame::decode(&buf).is_err());
-    }
-
-    #[test]
-    fn request_truncated_payload() {
-        let mut buf = BytesMut::new();
-        buf.put_u32_le(104); // length = 104 (code + 100 bytes payload)
-        buf.put_u32_le(1); // code
-        buf.put_slice(&[0u8; 50]); // only 50 of 100 bytes
-        assert!(RequestFrame::decode(&buf).is_err());
-    }
-
-    #[test]
-    fn request_length_too_small() {
-        let mut buf = BytesMut::new();
-        buf.put_u32_le(3); // length < 4 (must include code)
-        buf.put_u32_le(1);
-        assert!(RequestFrame::decode(&buf).is_err());
-    }
-
-    #[test]
-    fn payload_length_valid() {
-        assert_eq!(RequestFrame::payload_length(4).unwrap(), 0);
-        assert_eq!(RequestFrame::payload_length(104).unwrap(), 100);
-        assert_eq!(
-            RequestFrame::payload_length(u32::MAX).unwrap(),
-            u32::MAX - 4
-        );
-    }
-
-    #[test]
-    fn payload_length_too_small() {
-        assert!(RequestFrame::payload_length(0).is_err());
-        assert!(RequestFrame::payload_length(1).is_err());
-        assert!(RequestFrame::payload_length(3).is_err());
-    }
-
-    #[test]
-    fn request_encoded_size() {
-        assert_eq!(RequestFrame::encoded_size(0), Some(8));
-        assert_eq!(RequestFrame::encoded_size(100), Some(108));
-        assert_eq!(RequestFrame::encoded_size(usize::MAX), None);
-    }
-
-    #[test]
-    fn response_ok_roundtrip() {
-        let payload = b"response data";
-        let mut buf = BytesMut::with_capacity(ResponseFrame::encoded_size(payload.len()).unwrap());
-        ResponseFrame::encode_ok(payload, &mut buf).unwrap();
-
-        let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
-        assert_eq!(consumed, buf.len());
-        assert!(frame.is_ok());
-        assert_eq!(frame.status, 0);
-        assert_eq!(frame.payload, payload);
-    }
-
-    #[test]
-    fn response_ok_empty_payload() {
-        let mut buf = BytesMut::new();
-        ResponseFrame::encode_ok(&[], &mut buf).unwrap();
-
-        let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
-        assert_eq!(consumed, 8);
-        assert!(frame.is_ok());
-        assert!(frame.payload.is_empty());
-    }
-
-    #[test]
-    fn response_error_roundtrip() {
-        let mut buf = BytesMut::new();
-        ResponseFrame::encode_error(NonZeroU32::new(1001).unwrap(), &mut buf);
-
-        let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
-        assert_eq!(consumed, 8);
-        assert!(!frame.is_ok());
-        assert_eq!(frame.status, 1001);
-        assert!(frame.payload.is_empty());
-    }
-
-    #[test]
-    fn response_truncated_header() {
-        let buf = [0u8; 7];
-        assert!(ResponseFrame::decode(&buf).is_err());
-    }
-
-    #[test]
-    fn response_truncated_payload() {
-        let mut buf = BytesMut::new();
-        buf.put_u32_le(0); // status OK
-        buf.put_u32_le(100); // length = 100
-        buf.put_slice(&[0u8; 50]); // only 50 bytes
-        assert!(ResponseFrame::decode(&buf).is_err());
-    }
-
-    #[test]
-    fn response_encoded_size() {
-        assert_eq!(ResponseFrame::encoded_size(0), Some(8));
-        assert_eq!(ResponseFrame::encoded_size(256), Some(264));
-        assert_eq!(ResponseFrame::encoded_size(usize::MAX), None);
-    }
-
-    // RequestFrame2 tests
-
-    #[test]
     fn request2_roundtrip() {
         let payload = b"hello world";
-        let mut buf = BytesMut::with_capacity(RequestFrame2::encoded_size(payload.len()).unwrap());
-        RequestFrame2::encode(42, 7, payload, &mut buf).unwrap();
+        let mut buf = BytesMut::with_capacity(RequestFrame::encoded_size(payload.len()).unwrap());
+        RequestFrame::encode(42, 7, payload, &mut buf).unwrap();
 
-        let (frame, consumed) = RequestFrame2::decode(&buf).unwrap();
+        let (frame, consumed) = RequestFrame::decode(&buf).unwrap();
         assert_eq!(consumed, buf.len());
         assert_eq!(frame.code, 42);
         assert_eq!(frame.request_id, 7);
@@ -530,10 +237,10 @@
 
     #[test]
     fn request2_empty_payload() {
-        let mut buf = BytesMut::with_capacity(RequestFrame2::HEADER_SIZE);
-        RequestFrame2::encode(1, 99, &[], &mut buf).unwrap();
+        let mut buf = BytesMut::with_capacity(RequestFrame::HEADER_SIZE);
+        RequestFrame::encode(1, 99, &[], &mut buf).unwrap();
 
-        let (frame, consumed) = RequestFrame2::decode(&buf).unwrap();
+        let (frame, consumed) = RequestFrame::decode(&buf).unwrap();
         assert_eq!(consumed, 16);
         assert_eq!(frame.code, 1);
         assert_eq!(frame.request_id, 99);
@@ -544,7 +251,7 @@
     fn request2_length_field_includes_code_and_request_id() {
         let payload = b"test";
         let mut buf = BytesMut::new();
-        RequestFrame2::encode(99, 1, payload, &mut buf).unwrap();
+        RequestFrame::encode(99, 1, payload, &mut buf).unwrap();
 
         let length = u32::from_le_bytes(buf[0..4].try_into().unwrap());
         assert_eq!(length, 4 + 8 + 4); // code(4) + request_id(8) + payload(4)
@@ -553,7 +260,7 @@
     #[test]
     fn request2_truncated_header() {
         let buf = [0u8; 15]; // less than HEADER_SIZE (16)
-        assert!(RequestFrame2::decode(&buf).is_err());
+        assert!(RequestFrame::decode(&buf).is_err());
     }
 
     #[test]
@@ -563,7 +270,7 @@
         buf.put_u32_le(1); // code
         buf.put_u64_le(1); // request_id
         buf.put_slice(&[0u8; 50]); // only 50 of 100 bytes
-        assert!(RequestFrame2::decode(&buf).is_err());
+        assert!(RequestFrame::decode(&buf).is_err());
     }
 
     #[test]
@@ -572,46 +279,46 @@
         buf.put_u32_le(11); // length < 12 (must include code + request_id)
         buf.put_u32_le(1);
         buf.put_u64_le(1);
-        assert!(RequestFrame2::decode(&buf).is_err());
+        assert!(RequestFrame::decode(&buf).is_err());
     }
 
     #[test]
     fn request2_payload_length_valid() {
-        assert_eq!(RequestFrame2::payload_length(12).unwrap(), 0);
-        assert_eq!(RequestFrame2::payload_length(112).unwrap(), 100);
+        assert_eq!(RequestFrame::payload_length(12).unwrap(), 0);
+        assert_eq!(RequestFrame::payload_length(112).unwrap(), 100);
     }
 
     #[test]
     fn request2_payload_length_too_small() {
-        assert!(RequestFrame2::payload_length(0).is_err());
-        assert!(RequestFrame2::payload_length(11).is_err());
+        assert!(RequestFrame::payload_length(0).is_err());
+        assert!(RequestFrame::payload_length(11).is_err());
     }
 
     #[test]
     fn request2_encoded_size() {
-        assert_eq!(RequestFrame2::encoded_size(0), Some(16));
-        assert_eq!(RequestFrame2::encoded_size(100), Some(116));
-        assert_eq!(RequestFrame2::encoded_size(usize::MAX), None);
+        assert_eq!(RequestFrame::encoded_size(0), Some(16));
+        assert_eq!(RequestFrame::encoded_size(100), Some(116));
+        assert_eq!(RequestFrame::encoded_size(usize::MAX), None);
     }
 
     #[test]
     fn request2_from_parts() {
         let payload = b"data";
-        let frame = RequestFrame2::from_parts(5, 42, payload);
+        let frame = RequestFrame::from_parts(5, 42, payload);
         assert_eq!(frame.code, 5);
         assert_eq!(frame.request_id, 42);
         assert_eq!(frame.payload, payload);
     }
 
-    // ResponseFrame2 tests
+    // ResponseFrame tests
 
     #[test]
     fn response2_ok_roundtrip() {
         let payload = b"response data";
-        let mut buf = BytesMut::with_capacity(ResponseFrame2::encoded_size(payload.len()).unwrap());
-        ResponseFrame2::encode_ok(7, payload, &mut buf).unwrap();
+        let mut buf = BytesMut::with_capacity(ResponseFrame::encoded_size(payload.len()).unwrap());
+        ResponseFrame::encode_ok(7, payload, &mut buf).unwrap();
 
-        let (frame, consumed) = ResponseFrame2::decode(&buf).unwrap();
+        let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
         assert_eq!(consumed, buf.len());
         assert!(frame.is_ok());
         assert_eq!(frame.request_id, 7);
@@ -621,9 +328,9 @@
     #[test]
     fn response2_ok_empty_payload() {
         let mut buf = BytesMut::new();
-        ResponseFrame2::encode_ok(42, &[], &mut buf).unwrap();
+        ResponseFrame::encode_ok(42, &[], &mut buf).unwrap();
 
-        let (frame, consumed) = ResponseFrame2::decode(&buf).unwrap();
+        let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
         assert_eq!(consumed, 16);
         assert!(frame.is_ok());
         assert_eq!(frame.request_id, 42);
@@ -633,9 +340,9 @@
     #[test]
     fn response2_error_roundtrip() {
         let mut buf = BytesMut::new();
-        ResponseFrame2::encode_error(NonZeroU32::new(1001).unwrap(), 55, &mut buf);
+        ResponseFrame::encode_error(NonZeroU32::new(1001).unwrap(), 55, &mut buf);
 
-        let (frame, consumed) = ResponseFrame2::decode(&buf).unwrap();
+        let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
         assert_eq!(consumed, 16);
         assert!(!frame.is_ok());
         assert_eq!(frame.status, 1001);
@@ -646,7 +353,7 @@
     #[test]
     fn response2_truncated_header() {
         let buf = [0u8; 15];
-        assert!(ResponseFrame2::decode(&buf).is_err());
+        assert!(ResponseFrame::decode(&buf).is_err());
     }
 
     #[test]
@@ -655,13 +362,13 @@
         buf.put_u32_le(0); // status
         buf.put_u32_le(7); // length < 8 (must include request_id)
         buf.put_u64_le(1); // request_id
-        assert!(ResponseFrame2::decode(&buf).is_err());
+        assert!(ResponseFrame::decode(&buf).is_err());
     }
 
     #[test]
     fn response2_encoded_size() {
-        assert_eq!(ResponseFrame2::encoded_size(0), Some(16));
-        assert_eq!(ResponseFrame2::encoded_size(256), Some(272));
-        assert_eq!(ResponseFrame2::encoded_size(usize::MAX), None);
+        assert_eq!(ResponseFrame::encoded_size(0), Some(16));
+        assert_eq!(ResponseFrame::encoded_size(256), Some(272));
+        assert_eq!(ResponseFrame::encoded_size(usize::MAX), None);
     }
 }
diff --git a/core/binary_protocol/src/lib.rs b/core/binary_protocol/src/lib.rs
index b308e5f..7ec2ddd 100644
--- a/core/binary_protocol/src/lib.rs
+++ b/core/binary_protocol/src/lib.rs
@@ -55,14 +55,13 @@
 //!   [`StartViewHeader`]
 //! - Dispatch: [`GenericHeader`] for type-erased initial parsing
 
+pub mod batch;
 pub mod codec;
 pub mod codes;
 pub mod consensus;
 pub mod dispatch;
 pub mod error;
 pub mod framing;
-pub mod message_layout;
-pub mod message_view;
 pub mod namespace;
 pub mod primitives;
 pub mod requests;
@@ -71,7 +70,7 @@
 
 pub use codec::{WireDecode, WireEncode};
 pub use consensus::{
-    CHECKSUM_UNSEALED, Command2, CommitHeader, ConsensusError, ConsensusHeader, DVC_HEADERS_MAX,
+    CHECKSUM_UNSEALED, Command, CommitHeader, ConsensusError, ConsensusHeader, DVC_HEADERS_MAX,
     DoViewChangeHeader, EvictionHeader, EvictionReason, ForwardLogoutHeader, ForwardLogoutOutcome,
     ForwardLogoutResultHeader, ForwardRegisterHeader, ForwardRegisterOutcome,
     ForwardRegisterResultHeader, GenericHeader, HEADER_SIZE, Operation, PrepareHeader,
@@ -83,10 +82,7 @@
 };
 pub use dispatch::{COMMAND_TABLE, CommandMeta, lookup_by_operation, lookup_command};
 pub use error::WireError;
-pub use framing::{RequestFrame, RequestFrame2, ResponseFrame, ResponseFrame2, STATUS_OK};
-pub use message_view::{
-    WireMessageIterator, WireMessageIteratorMut, WireMessageView, WireMessageViewMut,
-};
+pub use framing::{RequestFrame, ResponseFrame, STATUS_OK};
 pub use primitives::ack_level::AckLevel;
 pub use primitives::consumer::{KIND_CONSUMER_GROUP, WireConsumer};
 pub use primitives::identifier::{MAX_WIRE_NAME_LENGTH, WireIdentifier, WireName};
diff --git a/core/binary_protocol/src/message_layout.rs b/core/binary_protocol/src/message_layout.rs
deleted file mode 100644
index 2858ec5..0000000
--- a/core/binary_protocol/src/message_layout.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-//! 64-byte message frame layout constants.
-//!
-//! This module is the single source of truth for the on-wire message
-//! header layout shared between encoders, decoders, and zero-copy views.
-//!
-//! ```text
-//! [checksum:8][id:16][offset:8][timestamp:8][origin_timestamp:8]
-//! [user_headers_length:4][payload_length:4][reserved:8]
-//! ```
-
-/// Fixed-size message header on the wire (64 bytes).
-pub const WIRE_MESSAGE_HEADER_SIZE: usize = 64;
-
-/// Fixed-size index entry per message (16 bytes).
-///
-/// Layout: `[0x00000000:4][cumulative_size:u32_le:4][0x0000000000000000:8]`
-pub const WIRE_MESSAGE_INDEX_SIZE: usize = 16;
-
-// Field offsets within the 64-byte message header.
-pub const MSG_CHECKSUM_OFFSET: usize = 0;
-pub const MSG_ID_OFFSET: usize = 8;
-pub const MSG_OFFSET_OFFSET: usize = 24;
-pub const MSG_TIMESTAMP_OFFSET: usize = 32;
-pub const MSG_ORIGIN_TIMESTAMP_OFFSET: usize = 40;
-pub const MSG_USER_HEADERS_LEN_OFFSET: usize = 48;
-pub const MSG_PAYLOAD_LEN_OFFSET: usize = 52;
-pub const MSG_RESERVED_OFFSET: usize = 56;
-
-// Compile-time verification that field offsets are contiguous and sum to the header size.
-const _: () = {
-    assert!(MSG_ID_OFFSET == MSG_CHECKSUM_OFFSET + 8);
-    assert!(MSG_OFFSET_OFFSET == MSG_ID_OFFSET + 16);
-    assert!(MSG_TIMESTAMP_OFFSET == MSG_OFFSET_OFFSET + 8);
-    assert!(MSG_ORIGIN_TIMESTAMP_OFFSET == MSG_TIMESTAMP_OFFSET + 8);
-    assert!(MSG_USER_HEADERS_LEN_OFFSET == MSG_ORIGIN_TIMESTAMP_OFFSET + 8);
-    assert!(MSG_PAYLOAD_LEN_OFFSET == MSG_USER_HEADERS_LEN_OFFSET + 4);
-    assert!(MSG_RESERVED_OFFSET == MSG_PAYLOAD_LEN_OFFSET + 4);
-    assert!(MSG_RESERVED_OFFSET + 8 == WIRE_MESSAGE_HEADER_SIZE);
-};
diff --git a/core/binary_protocol/src/message_view.rs b/core/binary_protocol/src/message_view.rs
deleted file mode 100644
index 3159dac..0000000
--- a/core/binary_protocol/src/message_view.rs
+++ /dev/null
@@ -1,617 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-//! Zero-copy view types for message frames on the wire.
-//!
-//! These types borrow an existing buffer and provide typed access to
-//! message header fields and payload data without copying.
-
-use crate::error::WireError;
-use crate::message_layout::{
-    MSG_CHECKSUM_OFFSET, MSG_ID_OFFSET, MSG_OFFSET_OFFSET, MSG_ORIGIN_TIMESTAMP_OFFSET,
-    MSG_PAYLOAD_LEN_OFFSET, MSG_TIMESTAMP_OFFSET, MSG_USER_HEADERS_LEN_OFFSET,
-    WIRE_MESSAGE_HEADER_SIZE,
-};
-use std::borrow::Cow;
-
-// Private helpers for infallible reads on validated buffers
-
-#[inline]
-fn u32_at(buf: &[u8], offset: usize) -> u32 {
-    u32::from_le_bytes(
-        buf[offset..offset + 4]
-            .try_into()
-            .expect("slice is exactly 4 bytes"),
-    )
-}
-
-#[inline]
-fn u64_at(buf: &[u8], offset: usize) -> u64 {
-    u64::from_le_bytes(
-        buf[offset..offset + 8]
-            .try_into()
-            .expect("slice is exactly 8 bytes"),
-    )
-}
-
-#[inline]
-fn u128_at(buf: &[u8], offset: usize) -> u128 {
-    u128::from_le_bytes(
-        buf[offset..offset + 16]
-            .try_into()
-            .expect("slice is exactly 16 bytes"),
-    )
-}
-
-/// Validate a message frame buffer and return `(total_size, payload_len, user_headers_len)`.
-fn validate_frame(buf: &[u8]) -> Result<(usize, usize, usize), WireError> {
-    if buf.len() < WIRE_MESSAGE_HEADER_SIZE {
-        return Err(WireError::UnexpectedEof {
-            offset: 0,
-            need: WIRE_MESSAGE_HEADER_SIZE,
-            have: buf.len(),
-        });
-    }
-
-    let user_headers_len = u32_at(buf, MSG_USER_HEADERS_LEN_OFFSET) as usize;
-    let payload_len = u32_at(buf, MSG_PAYLOAD_LEN_OFFSET) as usize;
-
-    let total = WIRE_MESSAGE_HEADER_SIZE
-        .checked_add(payload_len)
-        .and_then(|s| s.checked_add(user_headers_len))
-        .ok_or(WireError::Validation(Cow::Borrowed(
-            "message frame size overflow",
-        )))?;
-
-    if buf.len() < total {
-        return Err(WireError::UnexpectedEof {
-            offset: 0,
-            need: total,
-            have: buf.len(),
-        });
-    }
-
-    Ok((total, payload_len, user_headers_len))
-}
-
-// WireMessageView - immutable zero-copy frame view
-
-/// Borrowed view over a single message frame in a contiguous buffer.
-///
-/// Validates the buffer on construction; all accessors are infallible.
-pub struct WireMessageView<'a> {
-    buf: &'a [u8],
-    payload_len: usize,
-    user_headers_len: usize,
-}
-
-impl<'a> WireMessageView<'a> {
-    /// Create a view over the first message frame in `buf`.
-    ///
-    /// # Errors
-    /// Returns `WireError` if the buffer is shorter than the header (64 bytes)
-    /// or shorter than the full frame indicated by the length fields.
-    pub fn new(buf: &'a [u8]) -> Result<Self, WireError> {
-        let (total, payload_len, user_headers_len) = validate_frame(buf)?;
-        Ok(Self {
-            buf: &buf[..total],
-            payload_len,
-            user_headers_len,
-        })
-    }
-
-    #[must_use]
-    #[inline]
-    pub fn checksum(&self) -> u64 {
-        u64_at(self.buf, MSG_CHECKSUM_OFFSET)
-    }
-
-    #[must_use]
-    #[inline]
-    pub fn id(&self) -> u128 {
-        u128_at(self.buf, MSG_ID_OFFSET)
-    }
-
-    #[must_use]
-    #[inline]
-    pub fn offset(&self) -> u64 {
-        u64_at(self.buf, MSG_OFFSET_OFFSET)
-    }
-
-    #[must_use]
-    #[inline]
-    pub fn timestamp(&self) -> u64 {
-        u64_at(self.buf, MSG_TIMESTAMP_OFFSET)
-    }
-
-    #[must_use]
-    #[inline]
-    pub fn origin_timestamp(&self) -> u64 {
-        u64_at(self.buf, MSG_ORIGIN_TIMESTAMP_OFFSET)
-    }
-
-    #[must_use]
-    #[inline]
-    pub const fn payload_length(&self) -> usize {
-        self.payload_len
-    }
-
-    #[must_use]
-    #[inline]
-    pub const fn user_headers_length(&self) -> usize {
-        self.user_headers_len
-    }
-
-    #[must_use]
-    pub fn payload(&self) -> &'a [u8] {
-        &self.buf[WIRE_MESSAGE_HEADER_SIZE..WIRE_MESSAGE_HEADER_SIZE + self.payload_len]
-    }
-
-    #[must_use]
-    pub fn user_headers(&self) -> &'a [u8] {
-        let start = WIRE_MESSAGE_HEADER_SIZE + self.payload_len;
-        &self.buf[start..start + self.user_headers_len]
-    }
-
-    #[must_use]
-    #[inline]
-    pub const fn total_size(&self) -> usize {
-        self.buf.len()
-    }
-
-    #[must_use]
-    pub const fn as_bytes(&self) -> &'a [u8] {
-        self.buf
-    }
-}
-
-// WireMessageViewMut - mutable zero-copy frame view
-
-/// Mutable view for in-place header patching (offset, timestamp, checksum).
-pub struct WireMessageViewMut<'a> {
-    buf: &'a mut [u8],
-    payload_len: usize,
-    user_headers_len: usize,
-}
-
-impl<'a> WireMessageViewMut<'a> {
-    /// Create a mutable view over the first message frame in `buf`.
-    ///
-    /// # Errors
-    /// Same validation as [`WireMessageView::new`].
-    pub fn new(buf: &'a mut [u8]) -> Result<Self, WireError> {
-        let (total, payload_len, user_headers_len) = validate_frame(buf)?;
-        let (frame, _) = buf.split_at_mut(total);
-        Ok(Self {
-            buf: frame,
-            payload_len,
-            user_headers_len,
-        })
-    }
-
-    // -- Read accessors (same semantics as WireMessageView) --
-
-    #[must_use]
-    #[inline]
-    pub fn checksum(&self) -> u64 {
-        u64_at(self.buf, MSG_CHECKSUM_OFFSET)
-    }
-
-    #[must_use]
-    #[inline]
-    pub fn id(&self) -> u128 {
-        u128_at(self.buf, MSG_ID_OFFSET)
-    }
-
-    #[must_use]
-    #[inline]
-    pub fn offset(&self) -> u64 {
-        u64_at(self.buf, MSG_OFFSET_OFFSET)
-    }
-
-    #[must_use]
-    #[inline]
-    pub fn timestamp(&self) -> u64 {
-        u64_at(self.buf, MSG_TIMESTAMP_OFFSET)
-    }
-
-    #[must_use]
-    #[inline]
-    pub fn origin_timestamp(&self) -> u64 {
-        u64_at(self.buf, MSG_ORIGIN_TIMESTAMP_OFFSET)
-    }
-
-    #[must_use]
-    #[inline]
-    pub const fn payload_length(&self) -> usize {
-        self.payload_len
-    }
-
-    #[must_use]
-    #[inline]
-    pub const fn user_headers_length(&self) -> usize {
-        self.user_headers_len
-    }
-
-    #[must_use]
-    pub fn payload(&self) -> &[u8] {
-        &self.buf[WIRE_MESSAGE_HEADER_SIZE..WIRE_MESSAGE_HEADER_SIZE + self.payload_len]
-    }
-
-    #[must_use]
-    pub fn user_headers(&self) -> &[u8] {
-        let start = WIRE_MESSAGE_HEADER_SIZE + self.payload_len;
-        &self.buf[start..start + self.user_headers_len]
-    }
-
-    #[must_use]
-    #[inline]
-    pub const fn total_size(&self) -> usize {
-        self.buf.len()
-    }
-
-    // -- Typed setters --
-
-    pub fn set_checksum(&mut self, value: u64) {
-        self.buf[MSG_CHECKSUM_OFFSET..MSG_CHECKSUM_OFFSET + 8]
-            .copy_from_slice(&value.to_le_bytes());
-    }
-
-    pub fn set_id(&mut self, value: u128) {
-        self.buf[MSG_ID_OFFSET..MSG_ID_OFFSET + 16].copy_from_slice(&value.to_le_bytes());
-    }
-
-    pub fn set_offset(&mut self, value: u64) {
-        self.buf[MSG_OFFSET_OFFSET..MSG_OFFSET_OFFSET + 8].copy_from_slice(&value.to_le_bytes());
-    }
-
-    pub fn set_timestamp(&mut self, value: u64) {
-        self.buf[MSG_TIMESTAMP_OFFSET..MSG_TIMESTAMP_OFFSET + 8]
-            .copy_from_slice(&value.to_le_bytes());
-    }
-}
-
-// WireMessageIterator - zero-copy frame iterator
-
-/// Iterates over contiguous message frames, yielding borrowed views.
-pub struct WireMessageIterator<'a> {
-    buf: &'a [u8],
-    pos: usize,
-    remaining: u32,
-}
-
-impl<'a> WireMessageIterator<'a> {
-    #[must_use]
-    pub const fn new(buf: &'a [u8], count: u32) -> Self {
-        Self {
-            buf,
-            pos: 0,
-            remaining: count,
-        }
-    }
-}
-
-impl<'a> Iterator for WireMessageIterator<'a> {
-    type Item = Result<WireMessageView<'a>, WireError>;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        if self.remaining == 0 {
-            return None;
-        }
-        match WireMessageView::new(&self.buf[self.pos..]) {
-            Ok(view) => {
-                self.pos += view.total_size();
-                self.remaining -= 1;
-                Some(Ok(view))
-            }
-            Err(e) => {
-                self.remaining = 0;
-                Some(Err(e))
-            }
-        }
-    }
-}
-
-// WireMessageIteratorMut - mutable frame iterator (cursor-based)
-
-/// Mutable iterator over contiguous message frames.
-///
-/// Cannot implement standard `Iterator` because the yielded mutable view's
-/// lifetime is tied to `&mut self`. Uses a cursor-based API instead.
-pub struct WireMessageIteratorMut<'a> {
-    buf: &'a mut [u8],
-    pos: usize,
-    remaining: u32,
-}
-
-impl<'a> WireMessageIteratorMut<'a> {
-    pub const fn new(buf: &'a mut [u8], count: u32) -> Self {
-        Self {
-            buf,
-            pos: 0,
-            remaining: count,
-        }
-    }
-
-    /// Advance to the next frame and return a mutable view over it.
-    ///
-    /// The returned view borrows `self`, so it must be dropped before
-    /// calling this method again.
-    pub fn next_view_mut(&mut self) -> Option<Result<WireMessageViewMut<'_>, WireError>> {
-        if self.remaining == 0 {
-            return None;
-        }
-
-        // Compute frame size via temporary immutable access (borrow ends at block exit).
-        let (total, available) = {
-            let rest = &self.buf[self.pos..];
-            if rest.len() < WIRE_MESSAGE_HEADER_SIZE {
-                self.remaining = 0;
-                return Some(Err(WireError::UnexpectedEof {
-                    offset: self.pos,
-                    need: WIRE_MESSAGE_HEADER_SIZE,
-                    have: rest.len(),
-                }));
-            }
-            let user_headers_len = u32_at(rest, MSG_USER_HEADERS_LEN_OFFSET) as usize;
-            let payload_len = u32_at(rest, MSG_PAYLOAD_LEN_OFFSET) as usize;
-            let Some(total) = WIRE_MESSAGE_HEADER_SIZE
-                .checked_add(payload_len)
-                .and_then(|s| s.checked_add(user_headers_len))
-            else {
-                self.remaining = 0;
-                return Some(Err(WireError::Validation(Cow::Borrowed(
-                    "message frame size overflow",
-                ))));
-            };
-            (total, rest.len())
-        };
-
-        if available < total {
-            self.remaining = 0;
-            return Some(Err(WireError::UnexpectedEof {
-                offset: self.pos,
-                need: total,
-                have: available,
-            }));
-        }
-
-        let start = self.pos;
-        self.pos += total;
-        self.remaining -= 1;
-
-        Some(WireMessageViewMut::new(&mut self.buf[start..start + total]))
-    }
-}
-
-// Tests
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::message_layout::{
-        MSG_ID_OFFSET, MSG_ORIGIN_TIMESTAMP_OFFSET, MSG_PAYLOAD_LEN_OFFSET,
-        MSG_USER_HEADERS_LEN_OFFSET, WIRE_MESSAGE_HEADER_SIZE,
-    };
-
-    fn make_frame(payload: &[u8], user_headers: &[u8], id: u128, origin_ts: u64) -> Vec<u8> {
-        let total = WIRE_MESSAGE_HEADER_SIZE + payload.len() + user_headers.len();
-        let mut frame = vec![0u8; total];
-        frame[MSG_ID_OFFSET..MSG_ID_OFFSET + 16].copy_from_slice(&id.to_le_bytes());
-        frame[MSG_ORIGIN_TIMESTAMP_OFFSET..MSG_ORIGIN_TIMESTAMP_OFFSET + 8]
-            .copy_from_slice(&origin_ts.to_le_bytes());
-        #[allow(clippy::cast_possible_truncation)]
-        {
-            frame[MSG_USER_HEADERS_LEN_OFFSET..MSG_USER_HEADERS_LEN_OFFSET + 4]
-                .copy_from_slice(&(user_headers.len() as u32).to_le_bytes());
-            frame[MSG_PAYLOAD_LEN_OFFSET..MSG_PAYLOAD_LEN_OFFSET + 4]
-                .copy_from_slice(&(payload.len() as u32).to_le_bytes());
-        }
-        frame[WIRE_MESSAGE_HEADER_SIZE..WIRE_MESSAGE_HEADER_SIZE + payload.len()]
-            .copy_from_slice(payload);
-        frame[WIRE_MESSAGE_HEADER_SIZE + payload.len()..].copy_from_slice(user_headers);
-        frame
-    }
-
-    // -- WireMessageView --
-
-    #[test]
-    fn view_rejects_short_buffer() {
-        let buf = [0u8; WIRE_MESSAGE_HEADER_SIZE - 1];
-        assert!(WireMessageView::new(&buf).is_err());
-    }
-
-    #[test]
-    fn view_rejects_truncated_payload() {
-        let mut buf = vec![0u8; WIRE_MESSAGE_HEADER_SIZE + 5];
-        buf[MSG_PAYLOAD_LEN_OFFSET..MSG_PAYLOAD_LEN_OFFSET + 4]
-            .copy_from_slice(&10u32.to_le_bytes());
-        assert!(WireMessageView::new(&buf).is_err());
-    }
-
-    #[test]
-    fn view_accessors() {
-        let frame = make_frame(b"hello", b"hdr", 42, 999);
-        let view = WireMessageView::new(&frame).unwrap();
-        assert_eq!(view.checksum(), 0);
-        assert_eq!(view.id(), 42);
-        assert_eq!(view.offset(), 0);
-        assert_eq!(view.timestamp(), 0);
-        assert_eq!(view.origin_timestamp(), 999);
-        assert_eq!(view.payload_length(), 5);
-        assert_eq!(view.user_headers_length(), 3);
-        assert_eq!(view.payload(), b"hello");
-        assert_eq!(view.user_headers(), b"hdr");
-        assert_eq!(view.total_size(), WIRE_MESSAGE_HEADER_SIZE + 5 + 3);
-        assert_eq!(view.as_bytes().len(), view.total_size());
-    }
-
-    #[test]
-    fn view_empty_payload_and_headers() {
-        let frame = make_frame(b"", b"", 1, 0);
-        let view = WireMessageView::new(&frame).unwrap();
-        assert_eq!(view.payload(), b"");
-        assert_eq!(view.user_headers(), b"");
-        assert_eq!(view.total_size(), WIRE_MESSAGE_HEADER_SIZE);
-    }
-
-    #[test]
-    fn view_from_larger_buffer() {
-        let mut buf = make_frame(b"pay", b"", 1, 0);
-        buf.extend_from_slice(b"trailing_garbage");
-        let view = WireMessageView::new(&buf).unwrap();
-        assert_eq!(view.total_size(), WIRE_MESSAGE_HEADER_SIZE + 3);
-        assert_eq!(view.as_bytes().len(), WIRE_MESSAGE_HEADER_SIZE + 3);
-    }
-
-    // -- WireMessageViewMut --
-
-    #[test]
-    fn view_mut_set_get_roundtrip() {
-        let mut frame = make_frame(b"data", b"", 1, 0);
-        let mut view = WireMessageViewMut::new(&mut frame).unwrap();
-
-        view.set_checksum(0xDEAD_BEEF);
-        view.set_id(0x1234);
-        view.set_offset(100);
-        view.set_timestamp(200);
-
-        assert_eq!(view.checksum(), 0xDEAD_BEEF);
-        assert_eq!(view.id(), 0x1234);
-        assert_eq!(view.offset(), 100);
-        assert_eq!(view.timestamp(), 200);
-        assert_eq!(view.origin_timestamp(), 0);
-        assert_eq!(view.payload(), b"data");
-    }
-
-    #[test]
-    fn view_mut_set_does_not_corrupt_adjacent_fields() {
-        let mut frame = make_frame(b"x", b"y", 42, 999);
-        {
-            let mut view = WireMessageViewMut::new(&mut frame).unwrap();
-            view.set_offset(0xFFFF_FFFF_FFFF_FFFF);
-        }
-        let view = WireMessageView::new(&frame).unwrap();
-        assert_eq!(view.id(), 42);
-        assert_eq!(view.offset(), 0xFFFF_FFFF_FFFF_FFFF);
-        assert_eq!(view.timestamp(), 0);
-        assert_eq!(view.origin_timestamp(), 999);
-        assert_eq!(view.payload(), b"x");
-        assert_eq!(view.user_headers(), b"y");
-    }
-
-    // -- WireMessageIterator --
-
-    #[test]
-    fn iterator_zero_count() {
-        let buf = [];
-        let mut iter = WireMessageIterator::new(&buf, 0);
-        assert!(iter.next().is_none());
-    }
-
-    #[test]
-    fn iterator_single_message() {
-        let frame = make_frame(b"hello", b"", 1, 100);
-        let mut iter = WireMessageIterator::new(&frame, 1);
-        let view = iter.next().unwrap().unwrap();
-        assert_eq!(view.id(), 1);
-        assert_eq!(view.payload(), b"hello");
-        assert!(iter.next().is_none());
-    }
-
-    #[test]
-    fn iterator_multiple_messages() {
-        let f1 = make_frame(b"aaa", b"", 1, 100);
-        let f2 = make_frame(b"bbb", b"hh", 2, 200);
-        let f3 = make_frame(b"c", b"", 3, 300);
-        let mut buf = Vec::new();
-        buf.extend_from_slice(&f1);
-        buf.extend_from_slice(&f2);
-        buf.extend_from_slice(&f3);
-
-        let views: Vec<_> = WireMessageIterator::new(&buf, 3)
-            .collect::<Result<Vec<_>, _>>()
-            .unwrap();
-        assert_eq!(views.len(), 3);
-        assert_eq!(views[0].id(), 1);
-        assert_eq!(views[0].payload(), b"aaa");
-        assert_eq!(views[1].id(), 2);
-        assert_eq!(views[1].payload(), b"bbb");
-        assert_eq!(views[1].user_headers(), b"hh");
-        assert_eq!(views[2].id(), 3);
-        assert_eq!(views[2].payload(), b"c");
-    }
-
-    #[test]
-    fn iterator_truncated_mid_frame() {
-        let frame = make_frame(b"hello", b"", 1, 0);
-        let truncated = &frame[..frame.len() / 2];
-        let mut iter = WireMessageIterator::new(truncated, 1);
-        assert!(iter.next().unwrap().is_err());
-        assert!(iter.next().is_none());
-    }
-
-    // -- WireMessageIteratorMut --
-
-    #[test]
-    fn iterator_mut_mutate_then_read() {
-        let f1 = make_frame(b"aaa", b"", 1, 100);
-        let f2 = make_frame(b"bbb", b"", 2, 200);
-        let mut buf = Vec::new();
-        buf.extend_from_slice(&f1);
-        buf.extend_from_slice(&f2);
-
-        let mut iter = WireMessageIteratorMut::new(&mut buf, 2);
-        {
-            let mut view = iter.next_view_mut().unwrap().unwrap();
-            view.set_offset(10);
-            view.set_timestamp(1000);
-        }
-        {
-            let mut view = iter.next_view_mut().unwrap().unwrap();
-            view.set_offset(20);
-            view.set_timestamp(2000);
-        }
-        assert!(iter.next_view_mut().is_none());
-
-        let views: Vec<_> = WireMessageIterator::new(&buf, 2)
-            .collect::<Result<Vec<_>, _>>()
-            .unwrap();
-        assert_eq!(views[0].offset(), 10);
-        assert_eq!(views[0].timestamp(), 1000);
-        assert_eq!(views[0].payload(), b"aaa");
-        assert_eq!(views[1].offset(), 20);
-        assert_eq!(views[1].timestamp(), 2000);
-        assert_eq!(views[1].payload(), b"bbb");
-    }
-
-    #[test]
-    fn iterator_mut_count_matches() {
-        let f1 = make_frame(b"a", b"", 1, 0);
-        let f2 = make_frame(b"b", b"", 2, 0);
-        let mut buf = Vec::new();
-        buf.extend_from_slice(&f1);
-        buf.extend_from_slice(&f2);
-
-        let mut iter = WireMessageIteratorMut::new(&mut buf, 2);
-        let mut count = 0;
-        while let Some(Ok(_)) = iter.next_view_mut() {
-            count += 1;
-        }
-        assert_eq!(count, 2);
-    }
-}
diff --git a/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs b/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs
index 1c3c480..0140817 100644
--- a/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs
+++ b/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset.rs
@@ -18,14 +18,18 @@
 use crate::WireError;
 use crate::WireIdentifier;
 use crate::codec::{WireDecode, WireEncode, read_u8, read_u32_le};
+use crate::primitives::ack_level::AckLevel;
 use crate::primitives::consumer::WireConsumer;
 use bytes::{BufMut, BytesMut};
 
 /// `DeleteConsumerOffset` request.
 ///
+/// Adds an `ack` byte: `NoAck` = leader-local fast path, `Quorum` = VSR
+/// pipeline.
+///
 /// Wire format:
 /// ```text
-/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE]
+/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE][ack:1]
 /// ```
 ///
 /// `partition_id` encoding: a u8 flag (1=Some, 0=None) followed by 4 bytes
@@ -36,6 +40,7 @@
     pub stream_id: WireIdentifier,
     pub topic_id: WireIdentifier,
     pub partition_id: Option<u32>,
+    pub ack: AckLevel,
 }
 
 impl WireEncode for DeleteConsumerOffsetRequest {
@@ -45,6 +50,7 @@
             + self.topic_id.encoded_size()
             + 1
             + 4
+            + 1
     }
 
     fn encode(&self, buf: &mut BytesMut) {
@@ -58,6 +64,7 @@
             buf.put_u8(0);
             buf.put_u32_le(0);
         }
+        buf.put_u8(self.ack.as_u8());
     }
 }
 
@@ -79,12 +86,16 @@
         } else {
             None
         };
+        let ack_code = read_u8(buf, pos)?;
+        pos += 1;
+        let ack = AckLevel::from_code(ack_code)?;
         Ok((
             Self {
                 consumer,
                 stream_id,
                 topic_id,
                 partition_id,
+                ack,
             },
             pos,
         ))
@@ -96,12 +107,13 @@
     use super::*;
 
     #[test]
-    fn roundtrip_with_partition() {
+    fn roundtrip_with_partition_quorum() {
         let req = DeleteConsumerOffsetRequest {
             consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
             stream_id: WireIdentifier::numeric(10),
             topic_id: WireIdentifier::numeric(20),
             partition_id: Some(5),
+            ack: AckLevel::Quorum,
         };
         let bytes = req.to_bytes();
         let (decoded, consumed) = DeleteConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -110,12 +122,13 @@
     }
 
     #[test]
-    fn roundtrip_without_partition() {
+    fn roundtrip_without_partition_no_ack() {
         let req = DeleteConsumerOffsetRequest {
             consumer: WireConsumer::consumer_group(WireIdentifier::numeric(3)),
             stream_id: WireIdentifier::numeric(1),
             topic_id: WireIdentifier::numeric(1),
             partition_id: None,
+            ack: AckLevel::NoAck,
         };
         let bytes = req.to_bytes();
         let (decoded, consumed) = DeleteConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -130,6 +143,7 @@
             stream_id: WireIdentifier::named("stream-1").unwrap(),
             topic_id: WireIdentifier::named("topic-1").unwrap(),
             partition_id: Some(0),
+            ack: AckLevel::Quorum,
         };
         let bytes = req.to_bytes();
         let (decoded, consumed) = DeleteConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -138,22 +152,31 @@
     }
 
     #[test]
-    fn partition_none_encodes_zero_bytes() {
+    fn ack_byte_is_last() {
         let req = DeleteConsumerOffsetRequest {
             consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
             stream_id: WireIdentifier::numeric(1),
             topic_id: WireIdentifier::numeric(1),
-            partition_id: None,
+            partition_id: Some(0),
+            ack: AckLevel::NoAck,
         };
         let bytes = req.to_bytes();
-        let partition_offset = req.consumer.encoded_size()
-            + req.stream_id.encoded_size()
-            + req.topic_id.encoded_size();
-        assert_eq!(bytes[partition_offset], 0);
-        assert_eq!(
-            &bytes[partition_offset + 1..partition_offset + 5],
-            &[0, 0, 0, 0]
-        );
+        assert_eq!(*bytes.last().unwrap(), AckLevel::NoAck.as_u8());
+    }
+
+    #[test]
+    fn unknown_ack_rejected() {
+        let req = DeleteConsumerOffsetRequest {
+            consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
+            stream_id: WireIdentifier::numeric(1),
+            topic_id: WireIdentifier::numeric(1),
+            partition_id: Some(0),
+            ack: AckLevel::Quorum,
+        };
+        let mut bytes = req.to_bytes().to_vec();
+        let last = bytes.len() - 1;
+        bytes[last] = 0xFF;
+        assert!(DeleteConsumerOffsetRequest::decode(&bytes).is_err());
     }
 
     #[test]
@@ -163,6 +186,7 @@
             stream_id: WireIdentifier::numeric(1),
             topic_id: WireIdentifier::numeric(1),
             partition_id: Some(1),
+            ack: AckLevel::Quorum,
         };
         let bytes = req.to_bytes();
         for i in 0..bytes.len() {
diff --git a/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset_2.rs b/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset_2.rs
deleted file mode 100644
index ccfa104..0000000
--- a/core/binary_protocol/src/requests/consumer_offsets/delete_consumer_offset_2.rs
+++ /dev/null
@@ -1,199 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use crate::WireError;
-use crate::WireIdentifier;
-use crate::codec::{WireDecode, WireEncode, read_u8, read_u32_le};
-use crate::primitives::ack_level::AckLevel;
-use crate::primitives::consumer::WireConsumer;
-use bytes::{BufMut, BytesMut};
-
-/// `DeleteConsumerOffset` v2 request.
-///
-/// Adds an `ack` byte: `NoAck` = leader-local fast path, `Quorum` = VSR
-/// pipeline.
-///
-/// Wire format:
-/// ```text
-/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE][ack:1]
-/// ```
-///
-/// `partition_id` encoding: a u8 flag (1=Some, 0=None) followed by 4 bytes
-/// for the u32 value (0 when None).
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct DeleteConsumerOffset2Request {
-    pub consumer: WireConsumer,
-    pub stream_id: WireIdentifier,
-    pub topic_id: WireIdentifier,
-    pub partition_id: Option<u32>,
-    pub ack: AckLevel,
-}
-
-impl WireEncode for DeleteConsumerOffset2Request {
-    fn encoded_size(&self) -> usize {
-        self.consumer.encoded_size()
-            + self.stream_id.encoded_size()
-            + self.topic_id.encoded_size()
-            + 1
-            + 4
-            + 1
-    }
-
-    fn encode(&self, buf: &mut BytesMut) {
-        self.consumer.encode(buf);
-        self.stream_id.encode(buf);
-        self.topic_id.encode(buf);
-        if let Some(pid) = self.partition_id {
-            buf.put_u8(1);
-            buf.put_u32_le(pid);
-        } else {
-            buf.put_u8(0);
-            buf.put_u32_le(0);
-        }
-        buf.put_u8(self.ack.as_u8());
-    }
-}
-
-impl WireDecode for DeleteConsumerOffset2Request {
-    fn decode(buf: &[u8]) -> Result<(Self, usize), WireError> {
-        let mut pos = 0;
-        let (consumer, n) = WireConsumer::decode(&buf[pos..])?;
-        pos += n;
-        let (stream_id, n) = WireIdentifier::decode(&buf[pos..])?;
-        pos += n;
-        let (topic_id, n) = WireIdentifier::decode(&buf[pos..])?;
-        pos += n;
-        let partition_flag = read_u8(buf, pos)?;
-        pos += 1;
-        let partition_raw = read_u32_le(buf, pos)?;
-        pos += 4;
-        let partition_id = if partition_flag == 1 {
-            Some(partition_raw)
-        } else {
-            None
-        };
-        let ack_code = read_u8(buf, pos)?;
-        pos += 1;
-        let ack = AckLevel::from_code(ack_code)?;
-        Ok((
-            Self {
-                consumer,
-                stream_id,
-                topic_id,
-                partition_id,
-                ack,
-            },
-            pos,
-        ))
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn roundtrip_with_partition_quorum() {
-        let req = DeleteConsumerOffset2Request {
-            consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
-            stream_id: WireIdentifier::numeric(10),
-            topic_id: WireIdentifier::numeric(20),
-            partition_id: Some(5),
-            ack: AckLevel::Quorum,
-        };
-        let bytes = req.to_bytes();
-        let (decoded, consumed) = DeleteConsumerOffset2Request::decode(&bytes).unwrap();
-        assert_eq!(consumed, bytes.len());
-        assert_eq!(decoded, req);
-    }
-
-    #[test]
-    fn roundtrip_without_partition_no_ack() {
-        let req = DeleteConsumerOffset2Request {
-            consumer: WireConsumer::consumer_group(WireIdentifier::numeric(3)),
-            stream_id: WireIdentifier::numeric(1),
-            topic_id: WireIdentifier::numeric(1),
-            partition_id: None,
-            ack: AckLevel::NoAck,
-        };
-        let bytes = req.to_bytes();
-        let (decoded, consumed) = DeleteConsumerOffset2Request::decode(&bytes).unwrap();
-        assert_eq!(consumed, bytes.len());
-        assert_eq!(decoded, req);
-    }
-
-    #[test]
-    fn roundtrip_named_identifiers() {
-        let req = DeleteConsumerOffset2Request {
-            consumer: WireConsumer::consumer(WireIdentifier::named("my-consumer").unwrap()),
-            stream_id: WireIdentifier::named("stream-1").unwrap(),
-            topic_id: WireIdentifier::named("topic-1").unwrap(),
-            partition_id: Some(0),
-            ack: AckLevel::Quorum,
-        };
-        let bytes = req.to_bytes();
-        let (decoded, consumed) = DeleteConsumerOffset2Request::decode(&bytes).unwrap();
-        assert_eq!(consumed, bytes.len());
-        assert_eq!(decoded, req);
-    }
-
-    #[test]
-    fn ack_byte_is_last() {
-        let req = DeleteConsumerOffset2Request {
-            consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
-            stream_id: WireIdentifier::numeric(1),
-            topic_id: WireIdentifier::numeric(1),
-            partition_id: Some(0),
-            ack: AckLevel::NoAck,
-        };
-        let bytes = req.to_bytes();
-        assert_eq!(*bytes.last().unwrap(), AckLevel::NoAck.as_u8());
-    }
-
-    #[test]
-    fn unknown_ack_rejected() {
-        let req = DeleteConsumerOffset2Request {
-            consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
-            stream_id: WireIdentifier::numeric(1),
-            topic_id: WireIdentifier::numeric(1),
-            partition_id: Some(0),
-            ack: AckLevel::Quorum,
-        };
-        let mut bytes = req.to_bytes().to_vec();
-        let last = bytes.len() - 1;
-        bytes[last] = 0xFF;
-        assert!(DeleteConsumerOffset2Request::decode(&bytes).is_err());
-    }
-
-    #[test]
-    fn truncated_returns_error() {
-        let req = DeleteConsumerOffset2Request {
-            consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
-            stream_id: WireIdentifier::numeric(1),
-            topic_id: WireIdentifier::numeric(1),
-            partition_id: Some(1),
-            ack: AckLevel::Quorum,
-        };
-        let bytes = req.to_bytes();
-        for i in 0..bytes.len() {
-            assert!(
-                DeleteConsumerOffset2Request::decode(&bytes[..i]).is_err(),
-                "expected error for truncation at byte {i}"
-            );
-        }
-    }
-}
diff --git a/core/binary_protocol/src/requests/consumer_offsets/mod.rs b/core/binary_protocol/src/requests/consumer_offsets/mod.rs
index f8d3cee..bde4d02 100644
--- a/core/binary_protocol/src/requests/consumer_offsets/mod.rs
+++ b/core/binary_protocol/src/requests/consumer_offsets/mod.rs
@@ -16,13 +16,9 @@
 // under the License.
 
 pub mod delete_consumer_offset;
-pub mod delete_consumer_offset_2;
 pub mod get_consumer_offset;
 pub mod store_consumer_offset;
-pub mod store_consumer_offset_2;
 
 pub use delete_consumer_offset::DeleteConsumerOffsetRequest;
-pub use delete_consumer_offset_2::DeleteConsumerOffset2Request;
 pub use get_consumer_offset::GetConsumerOffsetRequest;
 pub use store_consumer_offset::StoreConsumerOffsetRequest;
-pub use store_consumer_offset_2::StoreConsumerOffset2Request;
diff --git a/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs b/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs
index 10bfd94..5261d5c 100644
--- a/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs
+++ b/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset.rs
@@ -18,14 +18,18 @@
 use crate::WireError;
 use crate::WireIdentifier;
 use crate::codec::{WireDecode, WireEncode, read_u8, read_u32_le, read_u64_le};
+use crate::primitives::ack_level::AckLevel;
 use crate::primitives::consumer::WireConsumer;
 use bytes::{BufMut, BytesMut};
 
 /// `StoreConsumerOffset` request.
 ///
+/// Adds an `ack` byte: `NoAck` = leader-local fast path, `Quorum` = VSR
+/// pipeline.
+///
 /// Wire format:
 /// ```text
-/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE][offset:8 LE]
+/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE][offset:8 LE][ack:1]
 /// ```
 ///
 /// `partition_id` encoding: a u8 flag (1=Some, 0=None) followed by 4 bytes
@@ -37,6 +41,7 @@
     pub topic_id: WireIdentifier,
     pub partition_id: Option<u32>,
     pub offset: u64,
+    pub ack: AckLevel,
 }
 
 impl WireEncode for StoreConsumerOffsetRequest {
@@ -47,6 +52,7 @@
             + 1
             + 4
             + 8
+            + 1
     }
 
     fn encode(&self, buf: &mut BytesMut) {
@@ -61,6 +67,7 @@
             buf.put_u32_le(0);
         }
         buf.put_u64_le(self.offset);
+        buf.put_u8(self.ack.as_u8());
     }
 }
 
@@ -84,6 +91,9 @@
         };
         let offset = read_u64_le(buf, pos)?;
         pos += 8;
+        let ack_code = read_u8(buf, pos)?;
+        pos += 1;
+        let ack = AckLevel::from_code(ack_code)?;
         Ok((
             Self {
                 consumer,
@@ -91,6 +101,7 @@
                 topic_id,
                 partition_id,
                 offset,
+                ack,
             },
             pos,
         ))
@@ -102,13 +113,14 @@
     use super::*;
 
     #[test]
-    fn roundtrip_with_partition() {
+    fn roundtrip_with_partition_quorum() {
         let req = StoreConsumerOffsetRequest {
             consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
             stream_id: WireIdentifier::numeric(10),
             topic_id: WireIdentifier::numeric(20),
             partition_id: Some(5),
             offset: 12345,
+            ack: AckLevel::Quorum,
         };
         let bytes = req.to_bytes();
         let (decoded, consumed) = StoreConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -117,13 +129,14 @@
     }
 
     #[test]
-    fn roundtrip_without_partition() {
+    fn roundtrip_without_partition_no_ack() {
         let req = StoreConsumerOffsetRequest {
             consumer: WireConsumer::consumer_group(WireIdentifier::numeric(3)),
             stream_id: WireIdentifier::numeric(1),
             topic_id: WireIdentifier::numeric(1),
             partition_id: None,
             offset: u64::MAX,
+            ack: AckLevel::NoAck,
         };
         let bytes = req.to_bytes();
         let (decoded, consumed) = StoreConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -139,6 +152,7 @@
             topic_id: WireIdentifier::named("topic-1").unwrap(),
             partition_id: Some(0),
             offset: 0,
+            ack: AckLevel::Quorum,
         };
         let bytes = req.to_bytes();
         let (decoded, consumed) = StoreConsumerOffsetRequest::decode(&bytes).unwrap();
@@ -147,17 +161,33 @@
     }
 
     #[test]
-    fn offset_encoding() {
+    fn ack_byte_is_last() {
         let req = StoreConsumerOffsetRequest {
             consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
             stream_id: WireIdentifier::numeric(1),
             topic_id: WireIdentifier::numeric(1),
             partition_id: Some(0),
-            offset: 0x0102_0304_0506_0708,
+            offset: 0,
+            ack: AckLevel::NoAck,
         };
         let bytes = req.to_bytes();
-        let (decoded, _) = StoreConsumerOffsetRequest::decode(&bytes).unwrap();
-        assert_eq!(decoded.offset, 0x0102_0304_0506_0708);
+        assert_eq!(*bytes.last().unwrap(), AckLevel::NoAck.as_u8());
+    }
+
+    #[test]
+    fn unknown_ack_rejected() {
+        let req = StoreConsumerOffsetRequest {
+            consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
+            stream_id: WireIdentifier::numeric(1),
+            topic_id: WireIdentifier::numeric(1),
+            partition_id: Some(0),
+            offset: 0,
+            ack: AckLevel::Quorum,
+        };
+        let mut bytes = req.to_bytes().to_vec();
+        let last = bytes.len() - 1;
+        bytes[last] = 0xFF;
+        assert!(StoreConsumerOffsetRequest::decode(&bytes).is_err());
     }
 
     #[test]
@@ -168,6 +198,7 @@
             topic_id: WireIdentifier::numeric(1),
             partition_id: Some(1),
             offset: 100,
+            ack: AckLevel::Quorum,
         };
         let bytes = req.to_bytes();
         for i in 0..bytes.len() {
diff --git a/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset_2.rs b/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset_2.rs
deleted file mode 100644
index 37e89fb..0000000
--- a/core/binary_protocol/src/requests/consumer_offsets/store_consumer_offset_2.rs
+++ /dev/null
@@ -1,211 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use crate::WireError;
-use crate::WireIdentifier;
-use crate::codec::{WireDecode, WireEncode, read_u8, read_u32_le, read_u64_le};
-use crate::primitives::ack_level::AckLevel;
-use crate::primitives::consumer::WireConsumer;
-use bytes::{BufMut, BytesMut};
-
-/// `StoreConsumerOffset` v2 request.
-///
-/// Adds an `ack` byte: `NoAck` = leader-local fast path, `Quorum` = VSR
-/// pipeline.
-///
-/// Wire format:
-/// ```text
-/// [consumer][stream_id][topic_id][partition_flag:1][partition_id:4 LE][offset:8 LE][ack:1]
-/// ```
-///
-/// `partition_id` encoding: a u8 flag (1=Some, 0=None) followed by 4 bytes
-/// for the u32 value (0 when None).
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct StoreConsumerOffset2Request {
-    pub consumer: WireConsumer,
-    pub stream_id: WireIdentifier,
-    pub topic_id: WireIdentifier,
-    pub partition_id: Option<u32>,
-    pub offset: u64,
-    pub ack: AckLevel,
-}
-
-impl WireEncode for StoreConsumerOffset2Request {
-    fn encoded_size(&self) -> usize {
-        self.consumer.encoded_size()
-            + self.stream_id.encoded_size()
-            + self.topic_id.encoded_size()
-            + 1
-            + 4
-            + 8
-            + 1
-    }
-
-    fn encode(&self, buf: &mut BytesMut) {
-        self.consumer.encode(buf);
-        self.stream_id.encode(buf);
-        self.topic_id.encode(buf);
-        if let Some(pid) = self.partition_id {
-            buf.put_u8(1);
-            buf.put_u32_le(pid);
-        } else {
-            buf.put_u8(0);
-            buf.put_u32_le(0);
-        }
-        buf.put_u64_le(self.offset);
-        buf.put_u8(self.ack.as_u8());
-    }
-}
-
-impl WireDecode for StoreConsumerOffset2Request {
-    fn decode(buf: &[u8]) -> Result<(Self, usize), WireError> {
-        let mut pos = 0;
-        let (consumer, n) = WireConsumer::decode(&buf[pos..])?;
-        pos += n;
-        let (stream_id, n) = WireIdentifier::decode(&buf[pos..])?;
-        pos += n;
-        let (topic_id, n) = WireIdentifier::decode(&buf[pos..])?;
-        pos += n;
-        let partition_flag = read_u8(buf, pos)?;
-        pos += 1;
-        let partition_raw = read_u32_le(buf, pos)?;
-        pos += 4;
-        let partition_id = if partition_flag == 1 {
-            Some(partition_raw)
-        } else {
-            None
-        };
-        let offset = read_u64_le(buf, pos)?;
-        pos += 8;
-        let ack_code = read_u8(buf, pos)?;
-        pos += 1;
-        let ack = AckLevel::from_code(ack_code)?;
-        Ok((
-            Self {
-                consumer,
-                stream_id,
-                topic_id,
-                partition_id,
-                offset,
-                ack,
-            },
-            pos,
-        ))
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn roundtrip_with_partition_quorum() {
-        let req = StoreConsumerOffset2Request {
-            consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
-            stream_id: WireIdentifier::numeric(10),
-            topic_id: WireIdentifier::numeric(20),
-            partition_id: Some(5),
-            offset: 12345,
-            ack: AckLevel::Quorum,
-        };
-        let bytes = req.to_bytes();
-        let (decoded, consumed) = StoreConsumerOffset2Request::decode(&bytes).unwrap();
-        assert_eq!(consumed, bytes.len());
-        assert_eq!(decoded, req);
-    }
-
-    #[test]
-    fn roundtrip_without_partition_no_ack() {
-        let req = StoreConsumerOffset2Request {
-            consumer: WireConsumer::consumer_group(WireIdentifier::numeric(3)),
-            stream_id: WireIdentifier::numeric(1),
-            topic_id: WireIdentifier::numeric(1),
-            partition_id: None,
-            offset: u64::MAX,
-            ack: AckLevel::NoAck,
-        };
-        let bytes = req.to_bytes();
-        let (decoded, consumed) = StoreConsumerOffset2Request::decode(&bytes).unwrap();
-        assert_eq!(consumed, bytes.len());
-        assert_eq!(decoded, req);
-    }
-
-    #[test]
-    fn roundtrip_named_identifiers() {
-        let req = StoreConsumerOffset2Request {
-            consumer: WireConsumer::consumer(WireIdentifier::named("my-consumer").unwrap()),
-            stream_id: WireIdentifier::named("stream-1").unwrap(),
-            topic_id: WireIdentifier::named("topic-1").unwrap(),
-            partition_id: Some(0),
-            offset: 0,
-            ack: AckLevel::Quorum,
-        };
-        let bytes = req.to_bytes();
-        let (decoded, consumed) = StoreConsumerOffset2Request::decode(&bytes).unwrap();
-        assert_eq!(consumed, bytes.len());
-        assert_eq!(decoded, req);
-    }
-
-    #[test]
-    fn ack_byte_is_last() {
-        let req = StoreConsumerOffset2Request {
-            consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
-            stream_id: WireIdentifier::numeric(1),
-            topic_id: WireIdentifier::numeric(1),
-            partition_id: Some(0),
-            offset: 0,
-            ack: AckLevel::NoAck,
-        };
-        let bytes = req.to_bytes();
-        assert_eq!(*bytes.last().unwrap(), AckLevel::NoAck.as_u8());
-    }
-
-    #[test]
-    fn unknown_ack_rejected() {
-        let req = StoreConsumerOffset2Request {
-            consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
-            stream_id: WireIdentifier::numeric(1),
-            topic_id: WireIdentifier::numeric(1),
-            partition_id: Some(0),
-            offset: 0,
-            ack: AckLevel::Quorum,
-        };
-        let mut bytes = req.to_bytes().to_vec();
-        let last = bytes.len() - 1;
-        bytes[last] = 0xFF;
-        assert!(StoreConsumerOffset2Request::decode(&bytes).is_err());
-    }
-
-    #[test]
-    fn truncated_returns_error() {
-        let req = StoreConsumerOffset2Request {
-            consumer: WireConsumer::consumer(WireIdentifier::numeric(1)),
-            stream_id: WireIdentifier::numeric(1),
-            topic_id: WireIdentifier::numeric(1),
-            partition_id: Some(1),
-            offset: 100,
-            ack: AckLevel::Quorum,
-        };
-        let bytes = req.to_bytes();
-        for i in 0..bytes.len() {
-            assert!(
-                StoreConsumerOffset2Request::decode(&bytes[..i]).is_err(),
-                "expected error for truncation at byte {i}"
-            );
-        }
-    }
-}
diff --git a/core/binary_protocol/src/requests/messages/mod.rs b/core/binary_protocol/src/requests/messages/mod.rs
index 6b976e0..7658409 100644
--- a/core/binary_protocol/src/requests/messages/mod.rs
+++ b/core/binary_protocol/src/requests/messages/mod.rs
@@ -21,6 +21,4 @@
 
 pub use flush_unsaved_buffer::FlushUnsavedBufferRequest;
 pub use poll_messages::PollMessagesRequest;
-pub use send_messages::{
-    RawMessage, SendMessagesEncoder, SendMessagesHeader, SendMessagesMetadataEncoder,
-};
+pub use send_messages::{RawMessage, SendMessagesEncoder, SendMessagesHeader};
diff --git a/core/binary_protocol/src/requests/messages/send_messages.rs b/core/binary_protocol/src/requests/messages/send_messages.rs
index 559ce9e..4d4007f 100644
--- a/core/binary_protocol/src/requests/messages/send_messages.rs
+++ b/core/binary_protocol/src/requests/messages/send_messages.rs
@@ -15,16 +15,22 @@
 // specific language governing permissions and limitations
 // under the License.
 
-//! Zero-copy encoder for `SendMessages` wire format.
+//! Encoder for the `SendMessages` wire format.
 //!
-//! Messages are written directly to the buffer without intermediate allocation.
+//! The body is the routing metadata section followed by one canonical message
+//! batch (see [`crate::batch`]) - the same record the server replicates,
+//! persists, and serves back to polls.
 
+use crate::batch::{
+    BATCH_HEADER_SIZE, BATCH_MESSAGE_HEADER_SIZE, BatchHeader, MAX_TIMESTAMP_DELTA_MICROS,
+    calculate_batch_checksum,
+};
 use crate::codec::{WireDecode, WireEncode, read_u32_le};
 use crate::error::WireError;
-use crate::message_layout::{WIRE_MESSAGE_HEADER_SIZE, WIRE_MESSAGE_INDEX_SIZE};
 use crate::primitives::identifier::WireIdentifier;
 use crate::primitives::partitioning::WirePartitioning;
 use bytes::{BufMut, BytesMut};
+use twox_hash::XxHash3_64;
 
 /// Borrowed message data for encoding. No allocation needed - the caller
 /// owns the payload and headers buffers.
@@ -36,29 +42,12 @@
 }
 
 impl RawMessage<'_> {
-    fn wire_size(&self) -> usize {
-        WIRE_MESSAGE_HEADER_SIZE + self.payload.len() + self.headers.map_or(0, <[u8]>::len)
-    }
-
-    /// Write the 64-byte message header to `buf`. Payload and `user_headers` are NOT
-    /// written - the caller sends them separately via vectored I/O.
-    pub fn encode_header(&self, buf: &mut BytesMut) {
-        let headers_len = self.headers.map_or(0, <[u8]>::len);
-        buf.put_u64_le(0); // checksum (server-computed)
-        buf.put_u128_le(self.id);
-        buf.put_u64_le(0); // offset (server-assigned)
-        buf.put_u64_le(0); // timestamp (server-assigned)
-        buf.put_u64_le(self.origin_timestamp);
-        #[allow(clippy::cast_possible_truncation)]
-        {
-            buf.put_u32_le(headers_len as u32);
-            buf.put_u32_le(self.payload.len() as u32);
-        }
-        buf.put_u64_le(0); // reserved
+    fn frame_size(&self) -> usize {
+        BATCH_MESSAGE_HEADER_SIZE + self.payload.len() + self.headers.map_or(0, <[u8]>::len)
     }
 }
 
-/// Zero-copy encoder for the `SendMessages` command payload.
+/// Encoder for the `SendMessages` command payload.
 ///
 /// Wire layout:
 /// ```text
@@ -67,9 +56,12 @@
 /// [topic_id:variable]
 /// [partitioning:variable]
 /// [messages_count:u32_le]
-/// [index_array: messages_count * 16 bytes]
-/// [message_data: variable]
+/// [batch: 256-byte batch header + message frames]
 /// ```
+///
+/// The producer leaves `partition_id`, `base_offset`, and `base_timestamp`
+/// zero in the batch header; the server stamps them. Every checksum is
+/// producer-computed and verified at admission.
 pub struct SendMessagesEncoder;
 
 impl SendMessagesEncoder {
@@ -82,18 +74,31 @@
     ) -> usize {
         let metadata_inner =
             stream_id.encoded_size() + topic_id.encoded_size() + partitioning.encoded_size() + 4;
-        let index_total = messages.len() * WIRE_MESSAGE_INDEX_SIZE;
-        let messages_total: usize = messages.iter().map(RawMessage::wire_size).sum();
-        4 + metadata_inner + index_total + messages_total
+        let blob_total: usize = messages.iter().map(RawMessage::frame_size).sum();
+        4 + metadata_inner + BATCH_HEADER_SIZE + blob_total
     }
 
+    /// Encode the full `SendMessages` body into `buf`.
+    ///
+    /// # Errors
+    /// [`WireError::Validation`] on an empty message batch;
+    /// [`WireError::InvalidMessageTimestampDelta`] when a message's
+    /// `origin_timestamp` runs more than [`MAX_TIMESTAMP_DELTA_MICROS`] ahead
+    /// of the batch's earliest one; [`WireError::PayloadTooLarge`] when a
+    /// section length or the batch length overflows its wire field.
     pub fn encode(
         buf: &mut BytesMut,
         stream_id: &WireIdentifier,
         topic_id: &WireIdentifier,
         partitioning: &WirePartitioning,
         messages: &[RawMessage<'_>],
-    ) {
+    ) -> Result<(), WireError> {
+        if messages.is_empty() {
+            return Err(WireError::Validation(std::borrow::Cow::Borrowed(
+                "cannot encode an empty message batch",
+            )));
+        }
+
         let metadata_inner =
             stream_id.encoded_size() + topic_id.encoded_size() + partitioning.encoded_size() + 4;
 
@@ -104,52 +109,83 @@
         topic_id.encode(buf);
         partitioning.encode(buf);
 
-        #[allow(clippy::cast_possible_truncation)]
-        buf.put_u32_le(messages.len() as u32);
+        let message_count =
+            u32::try_from(messages.len()).map_err(|_| WireError::PayloadTooLarge {
+                size: messages.len(),
+                max: u32::MAX as usize,
+            })?;
+        buf.put_u32_le(message_count);
 
-        // Index array: cumulative sizes for each message
-        let mut cumulative_size: u32 = 0;
-        for msg in messages {
+        let origin_timestamp = messages
+            .iter()
+            .map(|message| message.origin_timestamp)
+            .min()
+            .unwrap_or(0);
+
+        // The batch header depends on the frame checksums written below, so
+        // reserve its slot and backpatch once the blob is in place.
+        let header_start = buf.len();
+        buf.resize(header_start + BATCH_HEADER_SIZE, 0);
+
+        let blob_start = buf.len();
+        for (index, message) in messages.iter().enumerate() {
+            let timestamp_delta = message.origin_timestamp - origin_timestamp;
+            if timestamp_delta > MAX_TIMESTAMP_DELTA_MICROS {
+                return Err(WireError::InvalidMessageTimestampDelta(timestamp_delta));
+            }
+            let headers = message.headers.unwrap_or_default();
+            let user_headers_length =
+                u32::try_from(headers.len()).map_err(|_| WireError::PayloadTooLarge {
+                    size: headers.len(),
+                    max: u32::MAX as usize,
+                })?;
+            let payload_length =
+                u32::try_from(message.payload.len()).map_err(|_| WireError::PayloadTooLarge {
+                    size: message.payload.len(),
+                    max: u32::MAX as usize,
+                })?;
+
+            let frame_start = buf.len();
+            buf.put_u64_le(0); // checksum, backpatched below
+            buf.put_u128_le(message.id);
             #[allow(clippy::cast_possible_truncation)]
             {
-                cumulative_size += msg.wire_size() as u32;
+                buf.put_u32_le(index as u32); // offset_delta
+                buf.put_u32_le(timestamp_delta as u32);
             }
-            // bytes 0-3: zero
-            buf.put_u32_le(0);
-            // bytes 4-7: cumulative size
-            buf.put_u32_le(cumulative_size);
-            // bytes 8-15: zero
-            buf.put_u64_le(0);
-        }
-
-        // Message data: header(64) + payload + optional user_headers
-        for msg in messages {
-            let headers_len = msg.headers.map_or(0, <[u8]>::len);
-
-            buf.put_u64_le(0); // checksum (server-computed)
-            buf.put_u128_le(msg.id);
-            buf.put_u64_le(0); // offset (server-assigned)
-            buf.put_u64_le(0); // timestamp (server-assigned)
-            buf.put_u64_le(msg.origin_timestamp);
-            #[allow(clippy::cast_possible_truncation)]
-            {
-                buf.put_u32_le(headers_len as u32);
-                buf.put_u32_le(msg.payload.len() as u32);
-            }
+            buf.put_u32_le(user_headers_length);
+            buf.put_u32_le(payload_length);
             buf.put_u64_le(0); // reserved
-            buf.put_slice(msg.payload);
-            if let Some(headers) = msg.headers {
-                buf.put_slice(headers);
-            }
+            buf.put_slice(message.payload);
+            buf.put_slice(headers);
+
+            let checksum = XxHash3_64::oneshot(&buf[frame_start + 8..]);
+            buf[frame_start..frame_start + 8].copy_from_slice(&checksum.to_le_bytes());
         }
+
+        // Request framing carries the body size in a u32, so a batch past
+        // u32::MAX cannot ride any request frame.
+        let batch_size = BATCH_HEADER_SIZE + (buf.len() - blob_start);
+        if batch_size > u32::MAX as usize {
+            return Err(WireError::PayloadTooLarge {
+                size: batch_size,
+                max: u32::MAX as usize,
+            });
+        }
+        let batch_length = batch_size as u64;
+        let mut batch_header = BatchHeader::new(0, origin_timestamp, batch_length, message_count);
+        batch_header.batch_checksum = calculate_batch_checksum(&batch_header, &buf[blob_start..]);
+        let header_bytes: &mut [u8] = &mut buf[header_start..header_start + BATCH_HEADER_SIZE];
+        batch_header.encode_into(header_bytes);
+        Ok(())
     }
 }
 
 /// Metadata-only decoder for the `SendMessages` command.
 ///
 /// Parses routing metadata (stream, topic, partitioning, message count)
-/// without touching message payloads. The server uses this to extract
-/// routing info before reading message data into pooled buffers.
+/// without touching the batch. The server uses this to resolve the namespace
+/// before validating the batch bytes.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct SendMessagesHeader {
     pub stream_id: WireIdentifier,
@@ -204,145 +240,75 @@
     }
 }
 
-/// Vectored I/O encoder for `SendMessages`.
-///
-/// Writes metadata + index array but stops before message frame data,
-/// enabling the caller to compose `[header_buf | msg_frames...]` via writev.
-pub struct SendMessagesMetadataEncoder;
-
-impl SendMessagesMetadataEncoder {
-    /// Exact size of metadata + indexes (everything except message frame data).
-    #[must_use]
-    pub fn header_size(
-        stream_id: &WireIdentifier,
-        topic_id: &WireIdentifier,
-        partitioning: &WirePartitioning,
-        messages_count: usize,
-    ) -> usize {
-        let metadata_inner =
-            stream_id.encoded_size() + topic_id.encoded_size() + partitioning.encoded_size() + 4;
-        4 + metadata_inner + messages_count * WIRE_MESSAGE_INDEX_SIZE
-    }
-
-    /// Encode `metadata_length` + metadata fields + index array into `buf`.
-    ///
-    /// Does NOT write message frame data - the caller sends that via vectored I/O.
-    pub fn encode_header(
-        buf: &mut BytesMut,
-        stream_id: &WireIdentifier,
-        topic_id: &WireIdentifier,
-        partitioning: &WirePartitioning,
-        messages: &[RawMessage<'_>],
-    ) {
-        let metadata_inner =
-            stream_id.encoded_size() + topic_id.encoded_size() + partitioning.encoded_size() + 4;
-
-        #[allow(clippy::cast_possible_truncation)]
-        buf.put_u32_le(metadata_inner as u32);
-
-        stream_id.encode(buf);
-        topic_id.encode(buf);
-        partitioning.encode(buf);
-
-        #[allow(clippy::cast_possible_truncation)]
-        buf.put_u32_le(messages.len() as u32);
-
-        let mut cumulative_size: u32 = 0;
-        for msg in messages {
-            #[allow(clippy::cast_possible_truncation)]
-            {
-                cumulative_size += msg.wire_size() as u32;
-            }
-            buf.put_u32_le(0);
-            buf.put_u32_le(cumulative_size);
-            buf.put_u64_le(0);
-        }
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::batch::decode_batch_slice;
 
     fn numeric_id(id: u32) -> WireIdentifier {
         WireIdentifier::numeric(id)
     }
 
+    fn encode(
+        stream_id: &WireIdentifier,
+        topic_id: &WireIdentifier,
+        partitioning: &WirePartitioning,
+        messages: &[RawMessage<'_>],
+    ) -> BytesMut {
+        let size = SendMessagesEncoder::encoded_size(stream_id, topic_id, partitioning, messages);
+        let mut buf = BytesMut::with_capacity(size);
+        SendMessagesEncoder::encode(&mut buf, stream_id, topic_id, partitioning, messages)
+            .expect("encodes");
+        assert_eq!(buf.len(), size);
+        buf
+    }
+
     #[test]
-    fn encode_single_message_no_headers() {
+    fn encoded_batch_decodes_and_verifies() {
         let stream_id = numeric_id(1);
         let topic_id = numeric_id(2);
         let partitioning = WirePartitioning::Balanced;
-        let payload = b"hello";
-        let messages = [RawMessage {
-            id: 100,
-            origin_timestamp: 999,
-            headers: None,
-            payload: payload.as_slice(),
-        }];
-
-        let size =
-            SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
-        let mut buf = BytesMut::with_capacity(size);
-        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
-        assert_eq!(buf.len(), size);
-    }
-
-    #[test]
-    fn encode_single_message_with_headers() {
-        let stream_id = numeric_id(1);
-        let topic_id = numeric_id(2);
-        let partitioning = WirePartitioning::PartitionId(5);
-        let payload = b"world";
-        let headers = b"key:val";
-        let messages = [RawMessage {
-            id: 200,
-            origin_timestamp: 1000,
-            headers: Some(headers.as_slice()),
-            payload: payload.as_slice(),
-        }];
-
-        let size =
-            SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
-        let mut buf = BytesMut::with_capacity(size);
-        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
-        assert_eq!(buf.len(), size);
-    }
-
-    #[test]
-    fn encode_multiple_messages() {
-        let stream_id = numeric_id(10);
-        let topic_id = numeric_id(20);
-        let partitioning = WirePartitioning::MessagesKey(b"user-1".to_vec());
         let messages = [
             RawMessage {
-                id: 1,
-                origin_timestamp: 100,
+                id: 100,
+                origin_timestamp: 1_000,
                 headers: None,
-                payload: b"msg-1",
+                payload: b"first",
             },
             RawMessage {
-                id: 2,
-                origin_timestamp: 200,
-                headers: Some(b"h2"),
-                payload: b"msg-2",
-            },
-            RawMessage {
-                id: 3,
-                origin_timestamp: 300,
-                headers: None,
-                payload: b"msg-3",
+                id: 200,
+                origin_timestamp: 2_000,
+                headers: Some(b"hdr"),
+                payload: b"second",
             },
         ];
 
-        let size =
-            SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
-        let mut buf = BytesMut::with_capacity(size);
-        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
+        let buf = encode(&stream_id, &topic_id, &partitioning, &messages);
 
-        assert_eq!(buf.len(), size);
+        let metadata_len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
+        let (header, consumed) = SendMessagesHeader::decode(&buf[4..4 + metadata_len]).unwrap();
+        assert_eq!(consumed, metadata_len);
+        assert_eq!(header.messages_count, 2);
+
+        let batch = decode_batch_slice(&buf[4 + metadata_len..]).unwrap();
+        assert_eq!(batch.header.partition_id, 0);
+        assert_eq!(batch.header.base_offset, 0);
+        assert_eq!(batch.header.base_timestamp, 0);
+        assert_eq!(batch.header.origin_timestamp, 1_000);
+        assert_eq!(batch.header.total_size(), buf.len() - 4 - metadata_len);
+        assert_eq!(batch.message_count(), 2);
+
+        let views: Vec<_> = batch.iter().collect();
+        assert_eq!(views[0].header.id, 100);
+        assert_eq!(views[0].header.offset_delta, 0);
+        assert_eq!(views[0].header.timestamp_delta, 0);
+        assert_eq!(views[0].payload, b"first");
+        assert_eq!(views[0].user_headers, b"");
+        assert_eq!(views[1].header.id, 200);
+        assert_eq!(views[1].header.offset_delta, 1);
+        assert_eq!(views[1].header.timestamp_delta, 1_000);
+        assert_eq!(views[1].payload, b"second");
+        assert_eq!(views[1].user_headers, b"hdr");
     }
 
     #[test]
@@ -357,8 +323,7 @@
             payload: b"x",
         }];
 
-        let mut buf = BytesMut::with_capacity(256);
-        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
+        let buf = encode(&stream_id, &topic_id, &partitioning, &messages);
 
         let metadata_len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
         let expected =
@@ -367,157 +332,50 @@
     }
 
     #[test]
-    fn verify_index_entries() {
+    fn empty_payload_message() {
         let stream_id = numeric_id(1);
-        let topic_id = numeric_id(2);
+        let topic_id = numeric_id(1);
+        let partitioning = WirePartitioning::Balanced;
+        let messages = [RawMessage {
+            id: 7,
+            origin_timestamp: 0,
+            headers: None,
+            payload: b"",
+        }];
+
+        let buf = encode(&stream_id, &topic_id, &partitioning, &messages);
+        let metadata_len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
+        let batch = decode_batch_slice(&buf[4 + metadata_len..]).unwrap();
+        assert_eq!(batch.message_count(), 1);
+    }
+
+    #[test]
+    fn timestamp_delta_overflow_rejected() {
+        let stream_id = numeric_id(1);
+        let topic_id = numeric_id(1);
         let partitioning = WirePartitioning::Balanced;
         let messages = [
             RawMessage {
                 id: 1,
                 origin_timestamp: 0,
                 headers: None,
-                payload: b"aaaa", // wire_size = 64 + 4 = 68
+                payload: b"a",
             },
             RawMessage {
                 id: 2,
-                origin_timestamp: 0,
-                headers: Some(b"hh"), // wire_size = 64 + 3 + 2 = 69
-                payload: b"bbb",
+                origin_timestamp: MAX_TIMESTAMP_DELTA_MICROS + 1,
+                headers: None,
+                payload: b"b",
             },
         ];
 
-        let mut buf = BytesMut::with_capacity(512);
-        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
-        // Index starts after: 4 (metadata_len) + stream_id(6) + topic_id(6) + partitioning(2) + 4 (msg_count)
-        let index_start = 4 + 6 + 6 + 2 + 4;
-
-        // First index entry: cumulative = 68
-        let first_zero = u32::from_le_bytes(buf[index_start..index_start + 4].try_into().unwrap());
-        let first_cum =
-            u32::from_le_bytes(buf[index_start + 4..index_start + 8].try_into().unwrap());
-        assert_eq!(first_zero, 0);
-        assert_eq!(first_cum, 68);
-
-        // Second index entry: cumulative = 68 + 69 = 137
-        let second_start = index_start + WIRE_MESSAGE_INDEX_SIZE;
-        let second_zero =
-            u32::from_le_bytes(buf[second_start..second_start + 4].try_into().unwrap());
-        let second_cum =
-            u32::from_le_bytes(buf[second_start + 4..second_start + 8].try_into().unwrap());
-        assert_eq!(second_zero, 0);
-        assert_eq!(second_cum, 137);
-    }
-
-    #[test]
-    fn verify_message_header_layout() {
-        let stream_id = numeric_id(1);
-        let topic_id = numeric_id(2);
-        let partitioning = WirePartitioning::Balanced;
-        let messages = [RawMessage {
-            id: 42,
-            origin_timestamp: 777,
-            headers: Some(b"hdr"),
-            payload: b"pay",
-        }];
-
-        let mut buf = BytesMut::with_capacity(256);
-        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
-        // Message data starts after: 4 + 6 + 6 + 2 + 4 + 16 (one index entry)
-        let msg_start = 4 + 6 + 6 + 2 + 4 + 16;
-        let msg = &buf[msg_start..];
-
-        // Header layout matches WIRE_MESSAGE_HEADER_SIZE (64 bytes):
-        // checksum(8) + id(16) + offset(8) + timestamp(8) + origin_ts(8) + hdrs_len(4) + payload_len(4) + reserved(8)
-        let checksum = u64::from_le_bytes(msg[0..8].try_into().unwrap());
-        assert_eq!(checksum, 0);
-
-        let id = u128::from_le_bytes(msg[8..24].try_into().unwrap());
-        assert_eq!(id, 42);
-
-        let offset = u64::from_le_bytes(msg[24..32].try_into().unwrap());
-        assert_eq!(offset, 0);
-
-        let timestamp = u64::from_le_bytes(msg[32..40].try_into().unwrap());
-        assert_eq!(timestamp, 0);
-
-        let origin_ts = u64::from_le_bytes(msg[40..48].try_into().unwrap());
-        assert_eq!(origin_ts, 777);
-
-        let headers_len = u32::from_le_bytes(msg[48..52].try_into().unwrap());
-        assert_eq!(headers_len, 3);
-
-        let payload_len = u32::from_le_bytes(msg[52..56].try_into().unwrap());
-        assert_eq!(payload_len, 3);
-
-        let reserved = u64::from_le_bytes(msg[56..64].try_into().unwrap());
-        assert_eq!(reserved, 0);
-
-        // After the 64-byte header: payload then headers
-        assert_eq!(&msg[64..67], b"pay");
-        assert_eq!(&msg[67..70], b"hdr");
-    }
-
-    #[test]
-    fn empty_payload_message() {
-        let stream_id = numeric_id(1);
-        let topic_id = numeric_id(1);
-        let partitioning = WirePartitioning::Balanced;
-        let messages = [RawMessage {
-            id: 0,
-            origin_timestamp: 0,
-            headers: None,
-            payload: b"",
-        }];
-
-        let size =
-            SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
-        let mut buf = BytesMut::with_capacity(size);
-        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-        assert_eq!(buf.len(), size);
-    }
-
-    // -- RawMessage::encode_header --
-
-    #[test]
-    fn raw_message_encode_header_is_64_bytes() {
-        let msg = RawMessage {
-            id: 42,
-            origin_timestamp: 999,
-            headers: Some(b"hdr"),
-            payload: b"pay",
-        };
-        let mut buf = BytesMut::with_capacity(WIRE_MESSAGE_HEADER_SIZE);
-        msg.encode_header(&mut buf);
-        assert_eq!(buf.len(), WIRE_MESSAGE_HEADER_SIZE);
-    }
-
-    #[test]
-    fn raw_message_encode_header_field_values() {
-        let msg = RawMessage {
-            id: 0x1234_5678_9ABC_DEF0_1234_5678_9ABC_DEF0,
-            origin_timestamp: 0xCAFE_BABE,
-            headers: Some(b"hdr"),
-            payload: b"pay",
-        };
-        let mut buf = BytesMut::with_capacity(WIRE_MESSAGE_HEADER_SIZE);
-        msg.encode_header(&mut buf);
-
-        assert_eq!(u64::from_le_bytes(buf[0..8].try_into().unwrap()), 0);
-        assert_eq!(
-            u128::from_le_bytes(buf[8..24].try_into().unwrap()),
-            0x1234_5678_9ABC_DEF0_1234_5678_9ABC_DEF0
-        );
-        assert_eq!(u64::from_le_bytes(buf[24..32].try_into().unwrap()), 0);
-        assert_eq!(u64::from_le_bytes(buf[32..40].try_into().unwrap()), 0);
-        assert_eq!(
-            u64::from_le_bytes(buf[40..48].try_into().unwrap()),
-            0xCAFE_BABE
-        );
-        assert_eq!(u32::from_le_bytes(buf[48..52].try_into().unwrap()), 3);
-        assert_eq!(u32::from_le_bytes(buf[52..56].try_into().unwrap()), 3);
-        assert_eq!(u64::from_le_bytes(buf[56..64].try_into().unwrap()), 0);
+        let mut buf = BytesMut::new();
+        let result =
+            SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
+        assert!(matches!(
+            result,
+            Err(WireError::InvalidMessageTimestampDelta(_))
+        ));
     }
 
     // -- SendMessagesHeader --
@@ -580,171 +438,4 @@
         assert_eq!(header.metadata_length(), 18);
         assert_eq!(header.encoded_size(), 18);
     }
-
-    #[test]
-    fn send_messages_header_cross_validate_with_encoder() {
-        let stream_id = numeric_id(1);
-        let topic_id = numeric_id(2);
-        let partitioning = WirePartitioning::Balanced;
-        let messages = [RawMessage {
-            id: 42,
-            origin_timestamp: 777,
-            headers: Some(b"hdr"),
-            payload: b"pay",
-        }];
-
-        let mut buf = BytesMut::with_capacity(256);
-        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
-        let metadata_len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
-        let (header, consumed) = SendMessagesHeader::decode(&buf[4..4 + metadata_len]).unwrap();
-        assert_eq!(consumed, metadata_len);
-        assert_eq!(header.stream_id, stream_id);
-        assert_eq!(header.topic_id, topic_id);
-        assert_eq!(header.partitioning, partitioning);
-        assert_eq!(header.messages_count, 1);
-    }
-
-    // -- SendMessagesMetadataEncoder --
-
-    #[test]
-    fn metadata_encoder_header_size_matches_encoded() {
-        let stream_id = numeric_id(1);
-        let topic_id = numeric_id(2);
-        let partitioning = WirePartitioning::Balanced;
-        let messages = [
-            RawMessage {
-                id: 1,
-                origin_timestamp: 100,
-                headers: None,
-                payload: b"msg-1",
-            },
-            RawMessage {
-                id: 2,
-                origin_timestamp: 200,
-                headers: Some(b"h"),
-                payload: b"msg-2",
-            },
-        ];
-
-        let expected_size = SendMessagesMetadataEncoder::header_size(
-            &stream_id,
-            &topic_id,
-            &partitioning,
-            messages.len(),
-        );
-        let mut buf = BytesMut::with_capacity(expected_size);
-        SendMessagesMetadataEncoder::encode_header(
-            &mut buf,
-            &stream_id,
-            &topic_id,
-            &partitioning,
-            &messages,
-        );
-        assert_eq!(buf.len(), expected_size);
-    }
-
-    #[test]
-    fn metadata_encoder_concat_matches_full_encoder() {
-        let stream_id = numeric_id(1);
-        let topic_id = numeric_id(2);
-        let partitioning = WirePartitioning::Balanced;
-        let messages = [
-            RawMessage {
-                id: 1,
-                origin_timestamp: 100,
-                headers: None,
-                payload: b"aaa",
-            },
-            RawMessage {
-                id: 2,
-                origin_timestamp: 200,
-                headers: Some(b"hh"),
-                payload: b"bbb",
-            },
-        ];
-
-        let full_size =
-            SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
-        let mut full_buf = BytesMut::with_capacity(full_size);
-        SendMessagesEncoder::encode(
-            &mut full_buf,
-            &stream_id,
-            &topic_id,
-            &partitioning,
-            &messages,
-        );
-
-        let mut vec_buf = BytesMut::with_capacity(full_size);
-        SendMessagesMetadataEncoder::encode_header(
-            &mut vec_buf,
-            &stream_id,
-            &topic_id,
-            &partitioning,
-            &messages,
-        );
-        for msg in &messages {
-            msg.encode_header(&mut vec_buf);
-            vec_buf.put_slice(msg.payload);
-            if let Some(headers) = msg.headers {
-                vec_buf.put_slice(headers);
-            }
-        }
-
-        assert_eq!(vec_buf.len(), full_buf.len());
-        assert_eq!(&vec_buf[..], &full_buf[..]);
-    }
-
-    // -- Cross-validation: encoder -> header decoder -> iterator --
-
-    #[test]
-    fn cross_validation_encoder_to_iterator() {
-        use crate::message_view::WireMessageIterator;
-
-        let stream_id = WireIdentifier::numeric(1);
-        let topic_id = WireIdentifier::numeric(2);
-        let partitioning = WirePartitioning::Balanced;
-        let messages = [
-            RawMessage {
-                id: 100,
-                origin_timestamp: 1000,
-                headers: None,
-                payload: b"first",
-            },
-            RawMessage {
-                id: 200,
-                origin_timestamp: 2000,
-                headers: Some(b"hdr"),
-                payload: b"second",
-            },
-        ];
-
-        let size =
-            SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &messages);
-        let mut buf = BytesMut::with_capacity(size);
-        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &messages);
-
-        let metadata_len = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
-        let (header, _) = SendMessagesHeader::decode(&buf[4..4 + metadata_len]).unwrap();
-        assert_eq!(header.messages_count, 2);
-
-        let data_offset =
-            4 + metadata_len + (header.messages_count as usize) * WIRE_MESSAGE_INDEX_SIZE;
-        let message_data = &buf[data_offset..];
-
-        let views: Vec<_> = WireMessageIterator::new(message_data, header.messages_count)
-            .collect::<Result<Vec<_>, _>>()
-            .unwrap();
-        assert_eq!(views.len(), 2);
-
-        assert_eq!(views[0].id(), 100);
-        assert_eq!(views[0].origin_timestamp(), 1000);
-        assert_eq!(views[0].payload(), b"first");
-        assert_eq!(views[0].user_headers(), b"");
-
-        assert_eq!(views[1].id(), 200);
-        assert_eq!(views[1].origin_timestamp(), 2000);
-        assert_eq!(views[1].payload(), b"second");
-        assert_eq!(views[1].user_headers(), b"hdr");
-    }
 }
diff --git a/core/binary_protocol/src/responses/consumer_offsets/delete_consumer_offset_2.rs b/core/binary_protocol/src/responses/consumer_offsets/delete_consumer_offset_2.rs
deleted file mode 100644
index a145aab..0000000
--- a/core/binary_protocol/src/responses/consumer_offsets/delete_consumer_offset_2.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-/// `DeleteConsumerOffset2` response is empty.
-pub type DeleteConsumerOffset2Response = super::EmptyResponse;
diff --git a/core/binary_protocol/src/responses/consumer_offsets/mod.rs b/core/binary_protocol/src/responses/consumer_offsets/mod.rs
index 6dbaf45..915648c 100644
--- a/core/binary_protocol/src/responses/consumer_offsets/mod.rs
+++ b/core/binary_protocol/src/responses/consumer_offsets/mod.rs
@@ -16,14 +16,10 @@
 // under the License.
 
 mod delete_consumer_offset;
-mod delete_consumer_offset_2;
 pub mod get_consumer_offset;
 mod store_consumer_offset;
-mod store_consumer_offset_2;
 
 pub use super::EmptyResponse;
 pub use delete_consumer_offset::DeleteConsumerOffsetResponse;
-pub use delete_consumer_offset_2::DeleteConsumerOffset2Response;
 pub use get_consumer_offset::ConsumerOffsetResponse;
 pub use store_consumer_offset::StoreConsumerOffsetResponse;
-pub use store_consumer_offset_2::StoreConsumerOffset2Response;
diff --git a/core/binary_protocol/src/responses/consumer_offsets/store_consumer_offset_2.rs b/core/binary_protocol/src/responses/consumer_offsets/store_consumer_offset_2.rs
deleted file mode 100644
index 0f76f60..0000000
--- a/core/binary_protocol/src/responses/consumer_offsets/store_consumer_offset_2.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-/// `StoreConsumerOffset2` response is empty.
-pub type StoreConsumerOffset2Response = super::EmptyResponse;
diff --git a/core/binary_protocol/src/responses/messages/poll_messages.rs b/core/binary_protocol/src/responses/messages/poll_messages.rs
index cf31e44..ae7dce7 100644
--- a/core/binary_protocol/src/responses/messages/poll_messages.rs
+++ b/core/binary_protocol/src/responses/messages/poll_messages.rs
@@ -15,9 +15,18 @@
 // specific language governing permissions and limitations
 // under the License.
 
+//! Decoder for the `PollMessages` response.
+//!
+//! The body after the 16-byte prefix is a stream of canonical batch records
+//! (see [`crate::batch`]) served as stored: each record's header carries the
+//! stamped `base_offset` / `base_timestamp`, and each frame's deltas resolve
+//! against them. A record may be a server-sliced view of a larger stored
+//! batch, so `base_offset + offset_delta` of the first frame is the first
+//! polled offset, not necessarily `base_offset` itself.
+
+use crate::batch::{BatchHeader, BatchIntegrity, BatchIterator, decode_batch_slice_with};
 use crate::codec::{WireDecode, WireEncode, read_u32_le, read_u64_le};
 use crate::error::WireError;
-use crate::message_view::WireMessageIterator;
 use bytes::{BufMut, BytesMut};
 
 /// Size of the `PollMessages` response header: `partition_id(4) + current_offset(8) + count(4)`.
@@ -61,27 +70,120 @@
     }
 }
 
+/// One polled message with its deltas resolved to absolute values.
+#[derive(Debug, Clone, Copy)]
+pub struct PolledMessageView<'a> {
+    /// Stored per-message checksum, passed through as served.
+    pub checksum: u64,
+    pub id: u128,
+    pub offset: u64,
+    /// Broker append time: the flat batch `base_timestamp` (the per-message
+    /// delta applies to `origin_timestamp` only).
+    pub timestamp: u64,
+    pub origin_timestamp: u64,
+    pub payload: &'a [u8],
+    pub user_headers: &'a [u8],
+}
+
+/// Iterator over every message in a stream of served batch records.
+///
+/// Walks records by `batch_length` and flattens their frames. Each record's
+/// layout is proven up front (frames must tile `message_count` exactly), so
+/// framing errors, including frame-level corruption inside a record, surface
+/// as `Some(Err(_))` and end the iteration. Checksums are passed through
+/// unverified, as served.
+pub struct PolledBatchesIterator<'a> {
+    batches: &'a [u8],
+    position: usize,
+    current_header: Option<BatchHeader>,
+    frames: Option<BatchIterator<'a>>,
+    failed: bool,
+}
+
+impl<'a> PolledBatchesIterator<'a> {
+    #[must_use]
+    pub const fn new(batches: &'a [u8]) -> Self {
+        Self {
+            batches,
+            position: 0,
+            current_header: None,
+            frames: None,
+            failed: false,
+        }
+    }
+
+    fn advance_batch(&mut self) -> Result<bool, WireError> {
+        if self.position >= self.batches.len() {
+            return Ok(false);
+        }
+        // Layout-only decode proves the frames tile `message_count` exactly
+        // before any of them are yielded, so a corrupt record errors instead
+        // of silently truncating. Poll replies pass checksums through as
+        // served, so no integrity verification here.
+        let batch =
+            decode_batch_slice_with(&self.batches[self.position..], BatchIntegrity::LayoutOnly)?;
+        self.position += batch.header.total_size();
+        self.frames = Some(batch.iter());
+        self.current_header = Some(batch.header);
+        Ok(true)
+    }
+}
+
+impl<'a> Iterator for PolledBatchesIterator<'a> {
+    type Item = Result<PolledMessageView<'a>, WireError>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.failed {
+            return None;
+        }
+        loop {
+            if let Some(frames) = &mut self.frames
+                && let Some(view) = frames.next()
+            {
+                let header = self.current_header.expect("frames imply a current header");
+                return Some(Ok(PolledMessageView {
+                    checksum: view.header.checksum,
+                    id: view.header.id,
+                    offset: header.base_offset + u64::from(view.header.offset_delta),
+                    timestamp: header.base_timestamp,
+                    origin_timestamp: header.origin_timestamp
+                        + u64::from(view.header.timestamp_delta),
+                    payload: view.payload,
+                    user_headers: view.user_headers,
+                }));
+            }
+            match self.advance_batch() {
+                Ok(true) => {}
+                Ok(false) => return None,
+                Err(error) => {
+                    self.failed = true;
+                    return Some(Err(error));
+                }
+            }
+        }
+    }
+}
+
 /// Borrowed `PollMessages` response. Does not own message data.
 ///
 /// Does NOT implement `WireDecode` (trait returns owned data, we borrow).
 /// Use [`PollMessagesResponse::decode`] instead.
 pub struct PollMessagesResponse<'a> {
     pub header: PollMessagesResponseHeader,
-    pub messages: WireMessageIterator<'a>,
+    pub messages: PolledBatchesIterator<'a>,
 }
 
 impl<'a> PollMessagesResponse<'a> {
     /// Decode from a response payload buffer. Borrows the buffer.
     ///
-    /// Reads the 16-byte header then creates an iterator over the remaining
-    /// message frames. Messages are validated lazily during iteration.
+    /// Reads the 16-byte header then creates an iterator over the batch
+    /// records that follow. Records are validated lazily during iteration.
     ///
     /// # Errors
     /// Returns `WireError` if the buffer is too short for the response header.
     pub fn decode(buf: &'a [u8]) -> Result<Self, WireError> {
         let (header, _) = PollMessagesResponseHeader::decode(buf)?;
-        let messages =
-            WireMessageIterator::new(&buf[POLL_RESPONSE_HEADER_SIZE..], header.messages_count);
+        let messages = PolledBatchesIterator::new(&buf[POLL_RESPONSE_HEADER_SIZE..]);
         Ok(Self { header, messages })
     }
 }
@@ -89,88 +191,140 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::message_layout::{
-        MSG_ID_OFFSET, MSG_ORIGIN_TIMESTAMP_OFFSET, MSG_PAYLOAD_LEN_OFFSET,
-        WIRE_MESSAGE_HEADER_SIZE,
-    };
+    use crate::batch::{BATCH_HEADER_SIZE, BATCH_MESSAGE_HEADER_SIZE, calculate_batch_checksum};
+    use twox_hash::XxHash3_64;
 
-    fn make_frame(payload: &[u8], id: u128, origin_ts: u64) -> Vec<u8> {
-        let total = WIRE_MESSAGE_HEADER_SIZE + payload.len();
-        let mut frame = vec![0u8; total];
-        frame[MSG_ID_OFFSET..MSG_ID_OFFSET + 16].copy_from_slice(&id.to_le_bytes());
-        frame[MSG_ORIGIN_TIMESTAMP_OFFSET..MSG_ORIGIN_TIMESTAMP_OFFSET + 8]
-            .copy_from_slice(&origin_ts.to_le_bytes());
-        #[allow(clippy::cast_possible_truncation)]
-        frame[MSG_PAYLOAD_LEN_OFFSET..MSG_PAYLOAD_LEN_OFFSET + 4]
-            .copy_from_slice(&(payload.len() as u32).to_le_bytes());
-        frame[WIRE_MESSAGE_HEADER_SIZE..].copy_from_slice(payload);
-        frame
+    #[allow(clippy::cast_possible_truncation)]
+    fn frame(id: u128, offset_delta: u32, timestamp_delta: u32, payload: &[u8]) -> Vec<u8> {
+        let mut bytes = vec![0u8; BATCH_MESSAGE_HEADER_SIZE];
+        bytes[8..24].copy_from_slice(&id.to_le_bytes());
+        bytes[24..28].copy_from_slice(&offset_delta.to_le_bytes());
+        bytes[28..32].copy_from_slice(&timestamp_delta.to_le_bytes());
+        bytes[36..40].copy_from_slice(&(payload.len() as u32).to_le_bytes());
+        bytes.extend_from_slice(payload);
+        let checksum = XxHash3_64::oneshot(&bytes[8..]);
+        bytes[0..8].copy_from_slice(&checksum.to_le_bytes());
+        bytes
+    }
+
+    #[allow(clippy::cast_possible_truncation)]
+    fn batch_record(
+        base_offset: u64,
+        base_timestamp: u64,
+        origin_timestamp: u64,
+        frames: &[Vec<u8>],
+    ) -> Vec<u8> {
+        let blob: Vec<u8> = frames.concat();
+        let mut header = BatchHeader::new(
+            1,
+            origin_timestamp,
+            (BATCH_HEADER_SIZE + blob.len()) as u64,
+            frames.len() as u32,
+        );
+        header.base_offset = base_offset;
+        header.base_timestamp = base_timestamp;
+        header.batch_checksum = calculate_batch_checksum(&header, &blob);
+        let mut bytes = vec![0u8; BATCH_HEADER_SIZE];
+        header.encode_into(&mut bytes);
+        bytes.extend_from_slice(&blob);
+        bytes
+    }
+
+    fn response_body(current_offset: u64, count: u32, batches: &[Vec<u8>]) -> Vec<u8> {
+        let mut body = Vec::new();
+        body.extend_from_slice(&7u32.to_le_bytes());
+        body.extend_from_slice(&current_offset.to_le_bytes());
+        body.extend_from_slice(&count.to_le_bytes());
+        for batch in batches {
+            body.extend_from_slice(batch);
+        }
+        body
     }
 
     #[test]
     fn response_header_roundtrip() {
         let header = PollMessagesResponseHeader {
-            partition_id: 7,
-            current_offset: 12345,
-            messages_count: 3,
+            partition_id: 3,
+            current_offset: 42,
+            messages_count: 7,
         };
         let bytes = header.to_bytes();
-        assert_eq!(bytes.len(), POLL_RESPONSE_HEADER_SIZE);
         let (decoded, consumed) = PollMessagesResponseHeader::decode(&bytes).unwrap();
-        assert_eq!(consumed, POLL_RESPONSE_HEADER_SIZE);
+        assert_eq!(consumed, bytes.len());
         assert_eq!(decoded, header);
     }
 
     #[test]
     fn response_header_truncation() {
-        let header = PollMessagesResponseHeader {
-            partition_id: 1,
-            current_offset: 0,
-            messages_count: 0,
-        };
-        let bytes = header.to_bytes();
-        for i in 0..bytes.len() {
-            assert!(
-                PollMessagesResponseHeader::decode(&bytes[..i]).is_err(),
-                "expected error for truncation at byte {i}"
-            );
-        }
+        let bytes = [0u8; POLL_RESPONSE_HEADER_SIZE - 1];
+        assert!(PollMessagesResponseHeader::decode(&bytes).is_err());
     }
 
     #[test]
-    fn response_decode_with_messages() {
-        let frame1 = make_frame(b"hello", 1, 100);
-        let frame2 = make_frame(b"world", 2, 200);
+    fn decodes_messages_across_batches() {
+        let first = batch_record(
+            100,
+            5_000,
+            1_000,
+            &[frame(11, 0, 0, b"a"), frame(12, 1, 10, b"b")],
+        );
+        let second = batch_record(102, 6_000, 2_000, &[frame(13, 0, 0, b"c")]);
+        let body = response_body(102, 3, &[first, second]);
 
-        let mut buf = BytesMut::new();
-        buf.put_u32_le(42);
-        buf.put_u64_le(999);
-        buf.put_u32_le(2);
-        buf.put_slice(&frame1);
-        buf.put_slice(&frame2);
+        let response = PollMessagesResponse::decode(&body).unwrap();
+        assert_eq!(response.header.partition_id, 7);
+        assert_eq!(response.header.current_offset, 102);
+        assert_eq!(response.header.messages_count, 3);
 
-        let resp = PollMessagesResponse::decode(&buf).unwrap();
-        assert_eq!(resp.header.partition_id, 42);
-        assert_eq!(resp.header.current_offset, 999);
-        assert_eq!(resp.header.messages_count, 2);
+        let messages: Vec<_> = response.messages.collect::<Result<Vec<_>, _>>().unwrap();
+        assert_eq!(messages.len(), 3);
 
-        let views: Vec<_> = resp.messages.collect::<Result<Vec<_>, _>>().unwrap();
-        assert_eq!(views.len(), 2);
-        assert_eq!(views[0].id(), 1);
-        assert_eq!(views[0].payload(), b"hello");
-        assert_eq!(views[1].id(), 2);
-        assert_eq!(views[1].payload(), b"world");
+        assert_eq!(messages[0].id, 11);
+        assert_eq!(messages[0].offset, 100);
+        assert_eq!(messages[0].timestamp, 5_000);
+        assert_eq!(messages[0].origin_timestamp, 1_000);
+        assert_eq!(messages[0].payload, b"a");
+
+        assert_eq!(messages[1].id, 12);
+        assert_eq!(messages[1].offset, 101);
+        assert_eq!(messages[1].timestamp, 5_000);
+        assert_eq!(messages[1].origin_timestamp, 1_010);
+        assert_eq!(messages[1].payload, b"b");
+
+        assert_eq!(messages[2].id, 13);
+        assert_eq!(messages[2].offset, 102);
+        assert_eq!(messages[2].timestamp, 6_000);
+        assert_eq!(messages[2].payload, b"c");
     }
 
     #[test]
-    fn response_decode_zero_messages() {
-        let mut buf = BytesMut::new();
-        buf.put_u32_le(1);
-        buf.put_u64_le(0);
-        buf.put_u32_le(0);
+    fn sliced_record_resolves_leading_delta() {
+        // A server-sliced record keeps the stored base_offset; the first
+        // frame's delta positions it inside the original batch.
+        let record = batch_record(50, 9_000, 0, &[frame(1, 3, 0, b"tail")]);
+        let body = response_body(53, 1, &[record]);
 
-        let resp = PollMessagesResponse::decode(&buf).unwrap();
-        assert_eq!(resp.header.messages_count, 0);
-        assert_eq!(resp.messages.count(), 0);
+        let response = PollMessagesResponse::decode(&body).unwrap();
+        let messages: Vec<_> = response.messages.collect::<Result<Vec<_>, _>>().unwrap();
+        assert_eq!(messages.len(), 1);
+        assert_eq!(messages[0].offset, 53);
+    }
+
+    #[test]
+    fn empty_body_yields_no_messages() {
+        let body = response_body(0, 0, &[]);
+        let response = PollMessagesResponse::decode(&body).unwrap();
+        assert_eq!(response.messages.count(), 0);
+    }
+
+    #[test]
+    fn truncated_record_surfaces_error() {
+        let record = batch_record(0, 0, 0, &[frame(1, 0, 0, b"payload")]);
+        let mut body = response_body(0, 1, &[record]);
+        body.truncate(body.len() - 1);
+
+        let response = PollMessagesResponse::decode(&body).unwrap();
+        let result: Result<Vec<_>, _> = response.messages.collect();
+        assert!(result.is_err());
     }
 }
diff --git a/core/binary_protocol/src/version.rs b/core/binary_protocol/src/version.rs
index 670c460..6d26df2 100644
--- a/core/binary_protocol/src/version.rs
+++ b/core/binary_protocol/src/version.rs
@@ -62,7 +62,7 @@
 //!
 //! `ClientVersionInfo` is the leading bytes of the login-register request
 //! *body*, which itself rides inside a 256-byte VSR `RequestHeader` (see
-//! `consensus::header`): `command` = `Command2::Request`, `operation` =
+//! `consensus::header`): `command` = `Command::Request`, `operation` =
 //! `Operation::Register`, client id in `RequestHeader.client`. The client
 //! sends no group; the server derives it. A foreign SDK emits that header,
 //! then the body starting with this prefix, to reach the gate.
diff --git a/core/cli/Cargo.toml b/core/cli/Cargo.toml
index 580ae57..1f9cb09 100644
--- a/core/cli/Cargo.toml
+++ b/core/cli/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy-cli"
-version = "0.14.0-edge.3"
+version = "0.14.0-edge.4"
 edition = "2024"
 rust-version.workspace = true
 authors = ["bartosz.ciesla@gmail.com"]
diff --git a/core/common/Cargo.toml b/core/common/Cargo.toml
index 59dd949..1bb9328 100644
--- a/core/common/Cargo.toml
+++ b/core/common/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_common"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
 description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
 edition = "2024"
 rust-version.workspace = true
@@ -44,7 +44,6 @@
 derive_more = { workspace = true }
 humantime = { workspace = true }
 iggy_binary_protocol = { workspace = true }
-lending-iterator = { workspace = true }
 papaya = { workspace = true }
 ring = { workspace = true }
 secrecy = { workspace = true }
diff --git a/core/common/src/traits/binary_impls/consumer_offsets.rs b/core/common/src/traits/binary_impls/consumer_offsets.rs
index 1cde6f2..a5d95bd 100644
--- a/core/common/src/traits/binary_impls/consumer_offsets.rs
+++ b/core/common/src/traits/binary_impls/consumer_offsets.rs
@@ -20,6 +20,7 @@
 use crate::{
     BinaryClient, Consumer, ConsumerOffsetClient, ConsumerOffsetInfo, Identifier, IggyError,
 };
+use iggy_binary_protocol::AckLevel;
 use iggy_binary_protocol::codec::WireEncode;
 use iggy_binary_protocol::codes::{
     DELETE_CONSUMER_OFFSET_CODE, GET_CONSUMER_OFFSET_CODE, STORE_CONSUMER_OFFSET_CODE,
@@ -51,6 +52,7 @@
                 topic_id: wire_topic_id,
                 partition_id,
                 offset,
+                ack: AckLevel::Quorum,
             }
             .to_bytes(),
         )
@@ -106,6 +108,7 @@
                 stream_id: wire_stream_id,
                 topic_id: wire_topic_id,
                 partition_id,
+                ack: AckLevel::Quorum,
             }
             .to_bytes(),
         )
diff --git a/core/common/src/traits/binary_impls/messages.rs b/core/common/src/traits/binary_impls/messages.rs
index ab1f260..bf720a1 100644
--- a/core/common/src/traits/binary_impls/messages.rs
+++ b/core/common/src/traits/binary_impls/messages.rs
@@ -337,6 +337,14 @@
         let wire_stream_id = identifier_to_wire(stream_id)?;
         let wire_topic_id = identifier_to_wire(topic_id)?;
         let wire_partitioning = partitioning_to_wire(partitioning)?;
+        // The producer owns message ids now that batches ride the wire
+        // verbatim: a zero id is minted here, before the frame checksum
+        // covers it.
+        for message in messages.iter_mut() {
+            if message.header.id == 0 {
+                message.header.id = crate::utils::random_id::get_uuid();
+            }
+        }
         let raw_messages: Vec<RawMessage<'_>> = messages
             .iter()
             .map(|m| RawMessage {
@@ -359,7 +367,13 @@
             &wire_topic_id,
             &wire_partitioning,
             &raw_messages,
-        );
+        )
+        .map_err(|error| match error {
+            iggy_binary_protocol::WireError::InvalidMessageTimestampDelta(delta) => {
+                IggyError::InvalidMessageTimestampDelta(delta)
+            }
+            _ => IggyError::InvalidCommand,
+        })?;
         let response = self
             .send_raw_with_response(SEND_MESSAGES_CODE, buf.freeze())
             .await?;
diff --git a/core/common/src/types/message/message_view_mut.rs b/core/common/src/types/message/message_view_mut.rs
deleted file mode 100644
index 574eb58..0000000
--- a/core/common/src/types/message/message_view_mut.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use crate::{
-    IGGY_MESSAGE_HEADER_SIZE, IggyMessageHeaderView, calculate_checksum,
-    types::message::message_header_view_mut::IggyMessageHeaderViewMut,
-};
-use lending_iterator::prelude::*;
-
-/// A mutable view of a message for in-place modifications
-#[derive(Debug)]
-pub struct IggyMessageViewMut<'a> {
-    /// The buffer containing the message
-    buffer: &'a mut [u8],
-}
-
-impl<'a> IggyMessageViewMut<'a> {
-    /// Create a new mutable message view from a buffer
-    pub fn new(buffer: &'a mut [u8]) -> Self {
-        Self { buffer }
-    }
-
-    /// Get an immutable header view
-    pub fn header(&self) -> IggyMessageHeaderView<'_> {
-        let hdr_slice = &self.buffer[0..IGGY_MESSAGE_HEADER_SIZE];
-        IggyMessageHeaderView::new(hdr_slice)
-    }
-
-    /// Get an ephemeral mutable header view for reading/writing
-    pub fn header_mut(&mut self) -> IggyMessageHeaderViewMut<'_> {
-        let hdr_slice = &mut self.buffer[0..IGGY_MESSAGE_HEADER_SIZE];
-        IggyMessageHeaderViewMut::new(hdr_slice)
-    }
-
-    /// Returns the size of the entire message (header + payload + user headers).
-    pub fn size(&self) -> usize {
-        let hdr_view = self.header();
-
-        IGGY_MESSAGE_HEADER_SIZE + hdr_view.payload_length() + hdr_view.user_headers_length()
-    }
-
-    /// Convenience method to update the checksum field in the header
-    pub fn update_checksum(&mut self) {
-        let checksum_field_size = size_of::<u64>(); // Skip checksum field for checksum calculation
-        let size = self.size() - checksum_field_size;
-        let data = &self.buffer[checksum_field_size..checksum_field_size + size];
-        let checksum = calculate_checksum(data);
-        self.header_mut().set_checksum(checksum);
-    }
-}
-
-/// Iterator over mutable message views in a buffer
-pub struct IggyMessageViewMutIterator<'a> {
-    buffer: &'a mut [u8],
-    position: usize,
-}
-
-impl<'a> IggyMessageViewMutIterator<'a> {
-    pub fn new(buffer: &'a mut [u8]) -> Self {
-        Self {
-            buffer,
-            position: 0,
-        }
-    }
-}
-
-#[gat]
-impl LendingIterator for IggyMessageViewMutIterator<'_> {
-    type Item<'next> = IggyMessageViewMut<'next>;
-
-    fn next(&mut self) -> Option<Self::Item<'_>> {
-        let buffer_len = self.buffer.len();
-        if self.position >= buffer_len {
-            return None;
-        }
-
-        if self.position + IGGY_MESSAGE_HEADER_SIZE > self.buffer.len() {
-            tracing::error!(
-                "Buffer too small for message header at position {}, buffer len: {}",
-                self.position,
-                self.buffer.len()
-            );
-            self.position = self.buffer.len();
-            return None;
-        }
-
-        let buffer_slice = &mut self.buffer[self.position..];
-        let view = IggyMessageViewMut::new(buffer_slice);
-
-        let message_size = view.size();
-        if message_size == 0 {
-            tracing::error!(
-                "Message size is 0 at position {}, preventing infinite loop",
-                self.position
-            );
-            self.position = buffer_len;
-            return None;
-        }
-
-        self.position += message_size;
-        Some(view)
-    }
-}
diff --git a/core/common/src/types/message/mod.rs b/core/common/src/types/message/mod.rs
index ccba3df..86f2360 100644
--- a/core/common/src/types/message/mod.rs
+++ b/core/common/src/types/message/mod.rs
@@ -24,7 +24,6 @@
 mod message_header_view;
 mod message_header_view_mut;
 mod message_view;
-mod message_view_mut;
 mod messages_batch;
 pub mod partitioning;
 pub mod partitioning_kind;
@@ -55,7 +54,6 @@
 pub use message_header_view::IggyMessageHeaderView;
 pub use message_header_view_mut::IggyMessageHeaderViewMut;
 pub use message_view::{IggyMessageView, IggyMessageViewIterator};
-pub use message_view_mut::{IggyMessageViewMut, IggyMessageViewMutIterator};
 pub use messages_batch::IggyMessagesBatch;
 pub use partitioning::Partitioning;
 pub use partitioning_kind::PartitioningKind;
diff --git a/core/common/src/types/message/polled_messages.rs b/core/common/src/types/message/polled_messages.rs
index 6c45d54..af23d4e 100644
--- a/core/common/src/types/message/polled_messages.rs
+++ b/core/common/src/types/message/polled_messages.rs
@@ -15,8 +15,9 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::{IGGY_MESSAGE_HEADER_SIZE, IggyMessage, IggyMessageHeader, error::IggyError};
+use crate::{IggyMessage, IggyMessageHeader, error::IggyError};
 use bytes::Bytes;
+use iggy_binary_protocol::batch::{BATCH_HEADER_SIZE, BatchHeader, BatchMessageHeader};
 use serde::{Deserialize, Serialize};
 use tracing::error;
 
@@ -50,7 +51,17 @@
 }
 
 impl PolledMessages {
+    /// Decode a `PollMessages` response body: the 16-byte prefix followed by
+    /// the served batch records (`[256B batch header][frames]`, deltas
+    /// resolved against the stamped bases).
+    ///
+    /// # Errors
+    /// [`IggyError::InvalidNumberEncoding`] on a short prefix;
+    /// [`IggyError::InvalidMessagePayloadLength`] on a malformed record.
     pub fn from_bytes(bytes: Bytes) -> Result<Self, IggyError> {
+        if bytes.len() < 16 {
+            return Err(IggyError::InvalidNumberEncoding);
+        }
         let partition_id = u32::from_le_bytes(
             bytes[0..4]
                 .try_into()
@@ -67,7 +78,7 @@
                 .map_err(|_| IggyError::InvalidNumberEncoding)?,
         );
 
-        let messages = messages_from_bytes_and_count(bytes.slice(16..), count)?;
+        let messages = messages_from_batches(bytes.slice(16..), count)?;
 
         Ok(Self {
             partition_id,
@@ -78,48 +89,61 @@
     }
 }
 
-/// Convert Bytes to messages
-fn messages_from_bytes_and_count(buffer: Bytes, count: u32) -> Result<Vec<IggyMessage>, IggyError> {
+/// Walk the served batch records, resolving each frame's deltas to absolute
+/// values. Payload and user-header `Bytes` are zero-copy slices of the
+/// response buffer.
+fn messages_from_batches(buffer: Bytes, count: u32) -> Result<Vec<IggyMessage>, IggyError> {
     let mut messages = Vec::with_capacity(count as usize);
-    let mut position = 0;
-    let buf_len = buffer.len();
-    while position < buf_len {
-        if position + IGGY_MESSAGE_HEADER_SIZE > buf_len {
-            break;
-        }
-        let header_bytes = buffer.slice(position..position + IGGY_MESSAGE_HEADER_SIZE);
-        let header = match IggyMessageHeader::from_bytes(header_bytes) {
-            Ok(h) => h,
-            Err(e) => {
-                error!("Failed to deserialize message header: {}", e);
-                return Err(e);
+    let mut position = 0usize;
+    while position < buffer.len() {
+        let batch = BatchHeader::decode(&buffer[position..]).map_err(|decode_error| {
+            error!("Failed to decode polled batch header: {decode_error}");
+            IggyError::InvalidMessagePayloadLength
+        })?;
+        let batch_end = position
+            .checked_add(batch.total_size())
+            .filter(|end| *end <= buffer.len())
+            .ok_or(IggyError::InvalidMessagePayloadLength)?;
+        let mut cursor = position + BATCH_HEADER_SIZE;
+        while cursor < batch_end {
+            let frame =
+                BatchMessageHeader::decode(&buffer[cursor..batch_end]).map_err(|decode_error| {
+                    error!("Failed to decode polled message frame: {decode_error}");
+                    IggyError::InvalidMessagePayloadLength
+                })?;
+            let payload_start = cursor + iggy_binary_protocol::batch::BATCH_MESSAGE_HEADER_SIZE;
+            let payload_end = payload_start + frame.payload_length as usize;
+            let user_headers_end = payload_end + frame.user_headers_length as usize;
+            if user_headers_end > batch_end {
+                return Err(IggyError::InvalidMessagePayloadLength);
             }
-        };
-        position += IGGY_MESSAGE_HEADER_SIZE;
 
-        let payload_end = position + header.payload_length as usize;
-        if payload_end > buf_len {
-            break;
+            let header = IggyMessageHeader {
+                checksum: frame.checksum,
+                id: frame.id,
+                offset: batch.base_offset + u64::from(frame.offset_delta),
+                // Broker append time is stamped once per batch; the
+                // per-message delta applies to `origin_timestamp` only.
+                timestamp: batch.base_timestamp,
+                origin_timestamp: batch.origin_timestamp + u64::from(frame.timestamp_delta),
+                user_headers_length: frame.user_headers_length,
+                payload_length: frame.payload_length,
+                reserved: 0,
+            };
+            let payload = buffer.slice(payload_start..payload_end);
+            let user_headers = if frame.user_headers_length > 0 {
+                Some(buffer.slice(payload_end..user_headers_end))
+            } else {
+                None
+            };
+            messages.push(IggyMessage {
+                header,
+                payload,
+                user_headers,
+            });
+            cursor = user_headers_end;
         }
-        let payload = buffer.slice(position..payload_end);
-        position = payload_end;
-
-        let user_headers_end = position + header.user_headers_length as usize;
-        if user_headers_end > buf_len {
-            break;
-        }
-        let user_headers = if header.user_headers_length > 0 {
-            Some(buffer.slice(position..user_headers_end))
-        } else {
-            None
-        };
-        position = user_headers_end;
-
-        messages.push(IggyMessage {
-            header,
-            payload,
-            user_headers,
-        });
+        position = batch_end;
     }
 
     Ok(messages)
diff --git a/core/connectors/runtime/Cargo.toml b/core/connectors/runtime/Cargo.toml
index 448c1a4..dba184d 100644
--- a/core/connectors/runtime/Cargo.toml
+++ b/core/connectors/runtime/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy-connectors"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Connectors runtime for Iggy message streaming platform"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sdk/Cargo.toml b/core/connectors/sdk/Cargo.toml
index bfd99e2..70d1919 100644
--- a/core/connectors/sdk/Cargo.toml
+++ b/core/connectors/sdk/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_sdk"
-version = "0.4.0-edge.2"
+version = "0.4.0-edge.3"
 description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/clickhouse_sink/Cargo.toml b/core/connectors/sinks/clickhouse_sink/Cargo.toml
index a624d1c..5ca6a29 100644
--- a/core/connectors/sinks/clickhouse_sink/Cargo.toml
+++ b/core/connectors/sinks/clickhouse_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_clickhouse_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
 description = "Iggy ClickHouse sink connector for streaming messages into ClickHouse"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/delta_sink/Cargo.toml b/core/connectors/sinks/delta_sink/Cargo.toml
index 29b836a..51a2e54 100644
--- a/core/connectors/sinks/delta_sink/Cargo.toml
+++ b/core/connectors/sinks/delta_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_delta_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
 description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/doris_sink/Cargo.toml b/core/connectors/sinks/doris_sink/Cargo.toml
index a9f228a..ca031b5 100644
--- a/core/connectors/sinks/doris_sink/Cargo.toml
+++ b/core/connectors/sinks/doris_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_doris_sink"
-version = "0.2.0-edge.3"
+version = "0.2.0-edge.4"
 description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/elasticsearch_sink/Cargo.toml b/core/connectors/sinks/elasticsearch_sink/Cargo.toml
index 3f104c0..8c83d51 100644
--- a/core/connectors/sinks/elasticsearch_sink/Cargo.toml
+++ b/core/connectors/sinks/elasticsearch_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_elasticsearch_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy Elasticsearch sink connector"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/http_sink/Cargo.toml b/core/connectors/sinks/http_sink/Cargo.toml
index 5ad0d8f..1ab4fc6 100644
--- a/core/connectors/sinks/http_sink/Cargo.toml
+++ b/core/connectors/sinks/http_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_http_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy HTTP sink connector for delivering stream messages to any HTTP endpoint via webhooks, REST APIs, or serverless functions."
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/iceberg_sink/Cargo.toml b/core/connectors/sinks/iceberg_sink/Cargo.toml
index 14433a3..1d7f5d3 100644
--- a/core/connectors/sinks/iceberg_sink/Cargo.toml
+++ b/core/connectors/sinks/iceberg_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_iceberg_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 edition = "2024"
 license = "Apache-2.0"
 keywords = ["iggy", "messaging", "streaming"]
diff --git a/core/connectors/sinks/influxdb_sink/Cargo.toml b/core/connectors/sinks/influxdb_sink/Cargo.toml
index 39e29c7..0b5d567 100644
--- a/core/connectors/sinks/influxdb_sink/Cargo.toml
+++ b/core/connectors/sinks/influxdb_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_influxdb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy InfluxDB sink connector for storing stream messages as line protocol"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/meilisearch_sink/Cargo.toml b/core/connectors/sinks/meilisearch_sink/Cargo.toml
index a21988a..6a12ffb 100644
--- a/core/connectors/sinks/meilisearch_sink/Cargo.toml
+++ b/core/connectors/sinks/meilisearch_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_meilisearch_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy Meilisearch sink connector"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/mongodb_sink/Cargo.toml b/core/connectors/sinks/mongodb_sink/Cargo.toml
index bfe2b5d..f705c1d 100644
--- a/core/connectors/sinks/mongodb_sink/Cargo.toml
+++ b/core/connectors/sinks/mongodb_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_mongodb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy MongoDB sink connector for storing stream messages into MongoDB database"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/postgres_sink/Cargo.toml b/core/connectors/sinks/postgres_sink/Cargo.toml
index 374911b..315cbed 100644
--- a/core/connectors/sinks/postgres_sink/Cargo.toml
+++ b/core/connectors/sinks/postgres_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_postgres_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy PostgreSQL sink connector for storing stream messages into PostgreSQL database"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/quickwit_sink/Cargo.toml b/core/connectors/sinks/quickwit_sink/Cargo.toml
index 3cca112..b571c6b 100644
--- a/core/connectors/sinks/quickwit_sink/Cargo.toml
+++ b/core/connectors/sinks/quickwit_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_quickwit_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/s3_sink/Cargo.toml b/core/connectors/sinks/s3_sink/Cargo.toml
index 1502439..caa128c 100644
--- a/core/connectors/sinks/s3_sink/Cargo.toml
+++ b/core/connectors/sinks/s3_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_s3_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy S3 sink connector for writing stream messages to Amazon S3 and S3-compatible stores"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/stdout_sink/Cargo.toml b/core/connectors/sinks/stdout_sink/Cargo.toml
index f79d5c4..864e776 100644
--- a/core/connectors/sinks/stdout_sink/Cargo.toml
+++ b/core/connectors/sinks/stdout_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_stdout_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sinks/surrealdb_sink/Cargo.toml b/core/connectors/sinks/surrealdb_sink/Cargo.toml
index ed6a2d0..9169207 100644
--- a/core/connectors/sinks/surrealdb_sink/Cargo.toml
+++ b/core/connectors/sinks/surrealdb_sink/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_surrealdb_sink"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy SurrealDB sink connector for writing stream messages into SurrealDB"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sources/elasticsearch_source/Cargo.toml b/core/connectors/sources/elasticsearch_source/Cargo.toml
index 75cddf6..be79d85 100644
--- a/core/connectors/sources/elasticsearch_source/Cargo.toml
+++ b/core/connectors/sources/elasticsearch_source/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_elasticsearch_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy Elasticsearch source connector"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sources/influxdb_source/Cargo.toml b/core/connectors/sources/influxdb_source/Cargo.toml
index 3da5c36..7efb4a8 100644
--- a/core/connectors/sources/influxdb_source/Cargo.toml
+++ b/core/connectors/sources/influxdb_source/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_influxdb_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy InfluxDB source connector for polling Flux query results"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sources/postgres_source/Cargo.toml b/core/connectors/sources/postgres_source/Cargo.toml
index 48453a8..6999acc 100644
--- a/core/connectors/sources/postgres_source/Cargo.toml
+++ b/core/connectors/sources/postgres_source/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_postgres_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy PostgreSQL source connector supporting CDC and table polling for message streaming platform"
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/connectors/sources/random_source/Cargo.toml b/core/connectors/sources/random_source/Cargo.toml
index e3e07c1..ed18d54 100644
--- a/core/connectors/sources/random_source/Cargo.toml
+++ b/core/connectors/sources/random_source/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy_connector_random_source"
-version = "0.5.0-edge.3"
+version = "0.5.0-edge.4"
 description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
 edition = "2024"
 license = "Apache-2.0"
diff --git a/core/consensus/src/client_table.rs b/core/consensus/src/client_table.rs
index c1efdd8..18c6d86 100644
--- a/core/consensus/src/client_table.rs
+++ b/core/consensus/src/client_table.rs
@@ -1239,7 +1239,7 @@
 #[allow(clippy::cast_possible_truncation)]
 mod tests {
     use super::*;
-    use iggy_binary_protocol::{Command2, Operation};
+    use iggy_binary_protocol::{Command, Operation};
 
     /// Arbitrary non-zero user id for register fixtures; most tests don't
     /// assert on it (see `register_stores_user_id` for the accessor check).
@@ -1259,7 +1259,7 @@
             commit,
             // Real size so codec-roundtripped replies re-parse.
             size: header_size as u32,
-            command: Command2::Reply,
+            command: Command::Reply,
             operation: Operation::Register,
             ..ReplyHeader::default()
         };
@@ -1290,7 +1290,7 @@
             request_checksum,
             // Real size so codec-roundtripped replies re-parse.
             size: header_size as u32,
-            command: Command2::Reply,
+            command: Command::Reply,
             operation: Operation::SendMessages,
             ..ReplyHeader::default()
         };
diff --git a/core/consensus/src/dvc_merge.rs b/core/consensus/src/dvc_merge.rs
index 19bda15..93387da 100644
--- a/core/consensus/src/dvc_merge.rs
+++ b/core/consensus/src/dvc_merge.rs
@@ -530,7 +530,7 @@
     use super::*;
     use crate::DVC_HEADERS_MAX;
     use crate::view_change_quorum::{dvc_blank, dvc_quorum_array_empty, dvc_record};
-    use iggy_binary_protocol::{Command2, Operation};
+    use iggy_binary_protocol::{Command, Operation};
 
     /// Three replicas: replication 2, view-change 2, nack 2.
     fn quorums_r3() -> MergeQuorums {
@@ -545,7 +545,7 @@
     /// A prepare whose checksum derives from its op, so the hash chain connects.
     fn prepare(op: u64, view: u32) -> PrepareHeader {
         PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             operation: Operation::CreateStream,
             op,
             view,
diff --git a/core/consensus/src/impls.rs b/core/consensus/src/impls.rs
index e04aa0d..a481e34 100644
--- a/core/consensus/src/impls.rs
+++ b/core/consensus/src/impls.rs
@@ -27,7 +27,7 @@
 use bit_set::BitSet;
 use clock::{Clock, IggySystemClock};
 use iggy_binary_protocol::{
-    Command2, ConsensusHeader, DoViewChangeHeader, GenericHeader, PrepareHeader, PrepareOkHeader,
+    Command, ConsensusHeader, DoViewChangeHeader, GenericHeader, PrepareHeader, PrepareOkHeader,
     ReplyHeader, RequestStartViewHeader, RoutedRequestHeader, StartViewChangeHeader,
     StartViewHeader, frame_body,
 };
@@ -3562,14 +3562,14 @@
     /// Caller (`on_ack`) should validate `is_primary` and status before calling.
     ///
     /// # Panics
-    /// - If `header.command` is not `Command2::PrepareOk`.
+    /// - If `header.command` is not `Command::PrepareOk`.
     /// - If `header.replica >= self.replica_count`.
     pub fn handle_prepare_ok(
         &self,
         plane: PlaneKind,
         header: &PrepareOkHeader,
     ) -> PrepareOkOutcome {
-        assert_eq!(header.command, Command2::PrepareOk);
+        assert_eq!(header.command, Command::PrepareOk);
         assert!(
             header.replica < self.replica_count,
             "handle_prepare_ok: invalid replica {}",
@@ -3772,7 +3772,7 @@
                 size: old.size,
                 view: consensus.view.get(),
                 release: old.release,
-                command: Command2::Prepare,
+                command: Command::Prepare,
                 replica: consensus.replica,
                 client: old.client,
                 parent: consensus.last_prepare_checksum(),
@@ -3814,7 +3814,7 @@
     fn project(self, consensus: &Self::Consensus) -> Message<PrepareOkHeader> {
         self.transmute_header(|old, new| {
             *new = PrepareOkHeader {
-                command: Command2::PrepareOk,
+                command: Command::PrepareOk,
                 parent: old.parent,
                 prepare_checksum: old.checksum,
                 request: old.request,
@@ -3881,7 +3881,7 @@
 #[cfg(test)]
 mod request_queue_tests {
     use super::*;
-    use iggy_binary_protocol::{Command2, Operation};
+    use iggy_binary_protocol::{Command, Operation};
 
     fn make_request(client: u128, request_num: u64) -> Message<RoutedRequestHeader> {
         let header_size = std::mem::size_of::<RoutedRequestHeader>();
@@ -3891,7 +3891,7 @@
         )
         .expect("zeroed bytes are valid");
         *header = RoutedRequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             client,
             session: 1,
             request: request_num,
@@ -4035,7 +4035,7 @@
     //! subscriber `Canceled` even on happy path. Tests pin both halves.
 
     use super::*;
-    use iggy_binary_protocol::{Command2, ReplyHeader};
+    use iggy_binary_protocol::{Command, ReplyHeader};
     use server_common::Message;
 
     fn make_reply(client: u128, request: u64) -> Message<ReplyHeader> {
@@ -4046,7 +4046,7 @@
         )
         .expect("zeroed bytes are valid");
         *header = ReplyHeader {
-            command: Command2::Reply,
+            command: Command::Reply,
             client,
             request,
             ..ReplyHeader::default()
@@ -4126,7 +4126,7 @@
         for op in 1..=depth as u64 {
             let checksum = u128::from(op);
             let header = PrepareHeader {
-                command: Command2::Prepare,
+                command: Command::Prepare,
                 size: std::mem::size_of::<PrepareHeader>() as u32,
                 op,
                 parent,
@@ -4259,7 +4259,7 @@
             &mut msg.as_mut_slice()[..size],
         )
         .expect("zeroed bytes are a valid StartViewHeader");
-        header.command = Command2::StartView;
+        header.command = Command::StartView;
         header.cluster = 1;
         header.view = view;
         header.op = op;
@@ -4361,7 +4361,7 @@
                 &mut msg.as_mut_slice()[..header_size],
             )
             .expect("zeroed bytes are a valid RoutedRequestHeader");
-            header.command = Command2::Request;
+            header.command = Command::Request;
             header.client = 1;
             header.request = 1;
             header.operation = iggy_binary_protocol::Operation::SendMessages;
@@ -4703,7 +4703,7 @@
     fn projected_prepare(op: u64, parent: u128) -> Message<PrepareHeader> {
         Message::<PrepareHeader>::new(size_of::<PrepareHeader>()).transmute_header(|_, new| {
             *new = PrepareHeader {
-                command: Command2::Prepare,
+                command: Command::Prepare,
                 size: size_of::<PrepareHeader>() as u32,
                 op,
                 parent,
@@ -4723,7 +4723,7 @@
     ) -> Message<PrepareHeader> {
         Message::<PrepareHeader>::new(size_of::<PrepareHeader>()).transmute_header(|_, new| {
             *new = PrepareHeader {
-                command: Command2::Prepare,
+                command: Command::Prepare,
                 size: size_of::<PrepareHeader>() as u32,
                 op,
                 parent,
@@ -4760,7 +4760,7 @@
         Message::<RoutedRequestHeader>::new(size_of::<RoutedRequestHeader>()).transmute_header(
             |_, new| {
                 *new = RoutedRequestHeader {
-                    command: Command2::Request,
+                    command: Command::Request,
                     size: size_of::<RoutedRequestHeader>() as u32,
                     client,
                     session: 1,
diff --git a/core/consensus/src/metadata_helpers.rs b/core/consensus/src/metadata_helpers.rs
index 3dd5140..7202227 100644
--- a/core/consensus/src/metadata_helpers.rs
+++ b/core/consensus/src/metadata_helpers.rs
@@ -470,7 +470,7 @@
     use super::*;
     use crate::client_table::REGISTER_REQUEST_ID;
     use crate::{CLIENTS_TABLE_MAX, LocalPipeline};
-    use iggy_binary_protocol::{Command2, Operation, ReplyHeader};
+    use iggy_binary_protocol::{Command, Operation, ReplyHeader};
     use message_bus::SendError;
 
     /// Acting user for register fixtures; these tests exercise preflight /
@@ -579,7 +579,7 @@
         let header =
             bytemuck::checked::try_from_bytes::<EvictionHeader>(&frozen.as_slice()[..HEADER_SIZE])
                 .expect("valid EvictionHeader");
-        assert_eq!(header.command, Command2::Eviction);
+        assert_eq!(header.command, Command::Eviction);
         assert_eq!(header.reason, EvictionReason::NoSession);
         assert_eq!(header.client, client_id);
     }
@@ -908,7 +908,7 @@
             cluster: consensus.cluster(),
             size: header_size as u32,
             view: consensus.view(),
-            command: Command2::Reply,
+            command: Command::Reply,
             replica: consensus.replica(),
             client: client_id,
             op: register_commit,
@@ -942,7 +942,7 @@
             cluster: consensus.cluster(),
             size: header_size as u32,
             view: consensus.view(),
-            command: Command2::Reply,
+            command: Command::Reply,
             replica: consensus.replica(),
             client: client_id,
             op: commit,
diff --git a/core/consensus/src/observability.rs b/core/consensus/src/observability.rs
index e5c5832..4519693 100644
--- a/core/consensus/src/observability.rs
+++ b/core/consensus/src/observability.rs
@@ -671,8 +671,6 @@
         Operation::SendMessages => "send_messages",
         Operation::StoreConsumerOffset => "store_consumer_offset",
         Operation::DeleteConsumerOffset => "delete_consumer_offset",
-        Operation::StoreConsumerOffset2 => "store_consumer_offset_2",
-        Operation::DeleteConsumerOffset2 => "delete_consumer_offset_2",
     }
 }
 
diff --git a/core/consensus/src/plane_helpers.rs b/core/consensus/src/plane_helpers.rs
index 344336b..1c576d0 100644
--- a/core/consensus/src/plane_helpers.rs
+++ b/core/consensus/src/plane_helpers.rs
@@ -20,7 +20,7 @@
     Status, VsrConsensus,
 };
 use iggy_binary_protocol::{
-    CHECKSUM_UNSEALED, Command2, ConsensusHeader, GenericHeader, PrepareHeader, PrepareOkHeader,
+    CHECKSUM_UNSEALED, Command, ConsensusHeader, GenericHeader, PrepareHeader, PrepareOkHeader,
     ReplyHeader, RoutedRequestHeader, frame_body,
 };
 use message_bus::{MessageBus, SendError};
@@ -35,7 +35,7 @@
 #[non_exhaustive]
 pub enum ChainReplicationError {
     MalformedPrepare,
-    UnexpectedCommand { command: Command2 },
+    UnexpectedCommand { command: Command },
     CommittedPrepare { op: u64, commit_min: u64 },
     SelfRoute { replica: u8 },
     Transport(SendError),
@@ -215,7 +215,7 @@
     B: MessageBus,
     P: Pipeline<Entry = PipelineEntry>,
 {
-    if header.command != Command2::Prepare {
+    if header.command != Command::Prepare {
         return Err(ChainReplicationError::UnexpectedCommand {
             command: header.command,
         });
@@ -307,7 +307,7 @@
 /// status, or the message's view differs from the replica's.
 ///
 /// # Panics
-/// If `header.command` is not `Command2::Prepare`.
+/// If `header.command` is not `Command::Prepare`.
 pub fn replicate_preflight<B, P>(
     consensus: &VsrConsensus<B, P>,
     header: &PrepareHeader,
@@ -316,7 +316,7 @@
     B: MessageBus,
     P: Pipeline<Entry = PipelineEntry>,
 {
-    assert_eq!(header.command, Command2::Prepare);
+    assert_eq!(header.command, Command::Prepare);
 
     if consensus.is_transferring() {
         return Err(IgnoreReason::StateTransfer);
@@ -544,7 +544,7 @@
         // Commit-time view
         view: prepare_header.view,
         release: prepare_header.release,
-        command: Command2::Reply,
+        command: Command::Reply,
         // Original primary's id
         replica: prepare_header.replica,
         reserved_frame: [0; 66],
@@ -608,7 +608,7 @@
         size: total_size as u32,
         view: request_header.view,
         release: request_header.release,
-        command: Command2::Reply,
+        command: Command::Reply,
         replica: request_header.replica,
         request_checksum: request_header.request_checksum,
         client: request_header.client,
@@ -662,7 +662,7 @@
         size: total_size as u32,
         view: consensus.view(),
         release: 0,
-        command: Command2::Reply,
+        command: Command::Reply,
         replica: consensus.replica(),
         reserved_frame: [0; 66],
         request_checksum: request_header.request_checksum,
@@ -741,7 +741,7 @@
         size: header_size as u32,
         view: request_header.view,
         release: request_header.release,
-        command: Command2::Reply,
+        command: Command::Reply,
         replica: request_header.replica,
         request_checksum: request_header.request_checksum,
         client: request_header.client,
@@ -777,7 +777,7 @@
 
 // TODO: Figure out how to make this check the journal if it contains the prepare.
 /// # Panics
-/// - If `header.command` is not `Command2::Prepare`.
+/// - If `header.command` is not `Command::Prepare`.
 /// - If `header.view > consensus.view()`.
 #[allow(clippy::cast_possible_truncation, clippy::future_not_send)]
 pub async fn send_prepare_ok<B, P>(
@@ -788,7 +788,7 @@
     B: MessageBus,
     P: Pipeline<Entry = PipelineEntry>,
 {
-    assert_eq!(header.command, Command2::Prepare);
+    assert_eq!(header.command, Command::Prepare);
 
     if consensus.status() != Status::Normal {
         return;
@@ -814,7 +814,7 @@
     }
 
     let prepare_ok_header = PrepareOkHeader {
-        command: Command2::PrepareOk,
+        command: Command::PrepareOk,
         cluster: consensus.cluster(),
         replica: consensus.replica(),
         view: consensus.view(),
@@ -892,7 +892,7 @@
         Message::<PrepareHeader>::new(std::mem::size_of::<PrepareHeader>()).transmute_header(
             |_, new| {
                 *new = PrepareHeader {
-                    command: Command2::Prepare,
+                    command: Command::Prepare,
                     size: std::mem::size_of::<PrepareHeader>() as u32,
                     op,
                     parent,
@@ -914,7 +914,7 @@
             Message::<PrepareHeader>::new(std::mem::size_of::<PrepareHeader>()).transmute_header(
                 |_, new| {
                     *new = PrepareHeader {
-                        command: Command2::Prepare,
+                        command: Command::Prepare,
                         size: std::mem::size_of::<PrepareHeader>() as u32,
                         op: 1,
                         view,
@@ -966,7 +966,7 @@
         let svc =
             Message::<StartViewChangeHeader>::new(std::mem::size_of::<StartViewChangeHeader>())
                 .transmute_header(|_, new: &mut StartViewChangeHeader| {
-                    new.command = Command2::StartViewChange;
+                    new.command = Command::StartViewChange;
                     new.size = std::mem::size_of::<StartViewChangeHeader>() as u32;
                     new.view = 1;
                     new.replica = 0;
@@ -1006,7 +1006,7 @@
         let svc =
             Message::<StartViewChangeHeader>::new(std::mem::size_of::<StartViewChangeHeader>())
                 .transmute_header(|_, new: &mut StartViewChangeHeader| {
-                    new.command = Command2::StartViewChange;
+                    new.command = Command::StartViewChange;
                     new.size = std::mem::size_of::<StartViewChangeHeader>() as u32;
                     new.view = 1;
                     new.replica = 0;
@@ -1036,7 +1036,7 @@
             size: std::mem::size_of::<DoViewChangeHeader>() as u32,
             view: 1,
             release: 0,
-            command: Command2::DoViewChange,
+            command: Command::DoViewChange,
             replica: 1,
             reserved_frame: [0; 66],
             op: dvc_op,
@@ -1060,7 +1060,7 @@
         // moved with it, one op would carry different checksums per receiving view
         // and the merge would read them as competing prepares nacking each other.
         let base = PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             operation: iggy_binary_protocol::Operation::CreateStream,
             op: 9,
             view: 4,
@@ -1160,7 +1160,7 @@
         // The distinction the merge depends on: two prepares at one op number are
         // told apart, so a canonical header is distinguishable from a stale one.
         let first = PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             operation: iggy_binary_protocol::Operation::CreateStream,
             op: 5,
             client: 1,
@@ -1223,7 +1223,7 @@
         consensus.init();
 
         let prepare_header = PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             cluster: 1,
             view: 0,
             op: 0,
@@ -1236,13 +1236,13 @@
         let mut buf = Vec::new();
         consensus.drain_loopback_into(&mut buf);
         assert_eq!(buf.len(), 1);
-        assert_eq!(buf[0].header().command, Command2::PrepareOk);
+        assert_eq!(buf[0].header().command, Command::PrepareOk);
 
         let typed: Message<PrepareOkHeader> = buf
             .remove(0)
             .try_into_typed()
             .expect("loopback message must be PrepareOk");
-        assert_eq!(typed.header().command, Command2::PrepareOk);
+        assert_eq!(typed.header().command, Command::PrepareOk);
     }
 
     /// A sender's suffix and matching body bytes for a replica that holds every op
@@ -1291,7 +1291,7 @@
                 .expect("synthetic DVC frame fits u32"),
             view,
             release: 0,
-            command: Command2::DoViewChange,
+            command: Command::DoViewChange,
             replica,
             reserved_frame: [0; 66],
             op,
@@ -1319,7 +1319,7 @@
         let mut ascending = Vec::new();
         for op in low.max(1)..=high {
             let mut header = PrepareHeader {
-                command: Command2::Prepare,
+                command: Command::Prepare,
                 operation: iggy_binary_protocol::Operation::CreateStream,
                 op,
                 view,
@@ -1349,7 +1349,7 @@
             .expect("header fits u32"),
             view,
             release: 0,
-            command: Command2::StartViewChange,
+            command: Command::StartViewChange,
             replica,
             reserved_frame: [0; 66],
             group: 0,
@@ -1414,7 +1414,7 @@
     fn given_a_sealed_prepare_when_verifying_integrity_should_accept() {
         let message = Message::<PrepareHeader>::new(size_of::<PrepareHeader>()).transmute_header(
             |_, header: &mut PrepareHeader| {
-                header.command = Command2::Prepare;
+                header.command = Command::Prepare;
                 header.op = 7;
                 header.size = u32::try_from(size_of::<PrepareHeader>()).expect("header fits u32");
             },
@@ -1429,7 +1429,7 @@
         // corrupted in transit is journaled and then re-served to peers from the WAL.
         let message = Message::<PrepareHeader>::new(size_of::<PrepareHeader>()).transmute_header(
             |_, header: &mut PrepareHeader| {
-                header.command = Command2::Prepare;
+                header.command = Command::Prepare;
                 header.op = 7;
                 header.size = u32::try_from(size_of::<PrepareHeader>()).expect("header fits u32");
             },
@@ -1447,7 +1447,7 @@
         // The partition plane leaves `checksum` at `CHECKSUM_UNSEALED` and carries a
         // verified `batch_checksum` over the same bytes instead.
         let header = PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             op: 7,
             ..Default::default()
         };
@@ -1470,7 +1470,7 @@
         let header = bytemuck::checked::from_bytes_mut::<PrepareHeader>(
             &mut bytes[..size_of::<PrepareHeader>()],
         );
-        header.command = Command2::Prepare;
+        header.command = Command::Prepare;
         header.op = 7;
         header.size = u32::try_from(size).expect("fits u32");
         header.checksum_body = u128::from(calculate_checksum(body));
@@ -1642,7 +1642,7 @@
                 .expect("synthetic StartView fits u32"),
             view,
             release: 0,
-            command: Command2::StartView,
+            command: Command::StartView,
             replica,
             reserved_frame: [0; 66],
             op,
@@ -1870,7 +1870,7 @@
         consensus.init();
 
         let prepare_header = PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             cluster: 1,
             view: 0,
             op: 0,
@@ -1995,8 +1995,8 @@
         consensus.advance_commit_max(4);
 
         let request = RoutedRequestHeader {
-            command: Command2::Request,
-            operation: Operation::DeleteConsumerOffset2,
+            command: Command::Request,
+            operation: Operation::DeleteConsumerOffset,
             client: 42,
             request: 7,
             ..Default::default()
@@ -2005,13 +2005,13 @@
         let reply = build_deny_reply_from_request(&consensus, &request, status);
 
         let header = reply.header();
-        assert_eq!(header.command, Command2::Reply);
+        assert_eq!(header.command, Command::Reply);
         assert_eq!(header.status, status);
         assert_eq!(header.op, 0, "a deny commits nothing");
         assert_eq!(header.commit, 4);
         assert_eq!(header.client, 42);
         assert_eq!(header.request, 7);
-        assert_eq!(header.operation, Operation::DeleteConsumerOffset2);
+        assert_eq!(header.operation, Operation::DeleteConsumerOffset);
         assert_eq!(
             header.size as usize,
             std::mem::size_of::<ReplyHeader>(),
diff --git a/core/consensus/src/state_manifest.rs b/core/consensus/src/state_manifest.rs
index be5a530..cd73bca 100644
--- a/core/consensus/src/state_manifest.rs
+++ b/core/consensus/src/state_manifest.rs
@@ -46,7 +46,7 @@
     /// Metadata plane: [`crate::ClientTable::encode`] bytes.
     pub const CLIENT_TABLE: u8 = 1;
     /// Partition plane: one retained segment's `.log` bytes verbatim
-    /// (prepare-stripped `SendMessages2` records); `frontier` = the
+    /// (prepare-stripped `SendMessages` records); `frontier` = the
     /// segment's base offset.
     pub const SEGMENT_LOG: u8 = 2;
     /// Partition plane: the encoded consumer + consumer-group offset table
diff --git a/core/consensus/src/view_change_quorum.rs b/core/consensus/src/view_change_quorum.rs
index 95da481..89d5c26 100644
--- a/core/consensus/src/view_change_quorum.rs
+++ b/core/consensus/src/view_change_quorum.rs
@@ -17,7 +17,7 @@
 
 use crate::REPLICAS_MAX;
 use iggy_binary_protocol::{
-    CHECKSUM_UNSEALED, Command2, ConsensusHeader, DVC_HEADERS_MAX, Operation, PrepareHeader,
+    CHECKSUM_UNSEALED, Command, ConsensusHeader, DVC_HEADERS_MAX, Operation, PrepareHeader,
 };
 
 /// Write prepare headers into a control-message body, high-to-low op.
@@ -50,7 +50,7 @@
 #[must_use]
 pub fn dvc_blank(op: u64) -> PrepareHeader {
     PrepareHeader {
-        command: Command2::Prepare,
+        command: Command::Prepare,
         operation: Operation::Reserved,
         op,
         ..Default::default()
diff --git a/core/integration/tests/cluster/client_table_restart.rs b/core/integration/tests/cluster/client_table_restart.rs
index 2d93d10..e2b87a5 100644
--- a/core/integration/tests/cluster/client_table_restart.rs
+++ b/core/integration/tests/cluster/client_table_restart.rs
@@ -75,7 +75,7 @@
 use iggy::prelude::*;
 use iggy_binary_protocol::codec::{WireDecode, WireEncode};
 use iggy_binary_protocol::consensus::{
-    Command2, Operation, ReplyHeader, RequestHeader, read_size_field, result_code,
+    Command, Operation, ReplyHeader, RequestHeader, read_size_field, result_code,
     result_section_len,
 };
 use iggy_binary_protocol::requests::streams::CreateStreamRequest;
@@ -272,7 +272,7 @@
     body_len: usize,
 ) -> RequestHeader {
     RequestHeader {
-        command: Command2::Request,
+        command: Command::Request,
         operation,
         size: u32::try_from(HEADER_SIZE + body_len).unwrap(),
         client: CLIENT_ID,
@@ -572,14 +572,14 @@
     }
 
     let command_offset = offset_of!(RequestHeader, command);
-    if reply_header[command_offset] == Command2::Eviction as u8 {
+    if reply_header[command_offset] == Command::Eviction as u8 {
         return Exchange::Eviction {
             reason: reply_header[HEADER_SIZE - 1],
         };
     }
     assert_eq!(
         reply_header[command_offset],
-        Command2::Reply as u8,
+        Command::Reply as u8,
         "expected a Reply frame"
     );
 
diff --git a/core/integration/tests/sdk/protocol_version.rs b/core/integration/tests/sdk/protocol_version.rs
index 91bdb3b..3d82a7c 100644
--- a/core/integration/tests/sdk/protocol_version.rs
+++ b/core/integration/tests/sdk/protocol_version.rs
@@ -24,7 +24,7 @@
 
 use iggy::prelude::*;
 use iggy_binary_protocol::codec::WireEncode;
-use iggy_binary_protocol::consensus::{Command2, Operation, RequestHeader};
+use iggy_binary_protocol::consensus::{Command, Operation, RequestHeader};
 use iggy_binary_protocol::requests::users::LoginRegisterRequest;
 use iggy_binary_protocol::{
     ClientVersionInfo, HEADER_SIZE, IGGY_PROTOCOL_VERSION, IGGY_PROTOCOL_VERSION_MIN, WireName,
@@ -79,7 +79,7 @@
     expected_window: (u32, u32),
 ) {
     let header = RequestHeader {
-        command: Command2::Request,
+        command: Command::Request,
         operation: Operation::Register,
         size: u32::try_from(HEADER_SIZE + body.len()).unwrap(),
         client: 0xC0FFEE,
@@ -103,7 +103,7 @@
     let command_offset = offset_of!(RequestHeader, command);
     assert_eq!(
         reply[command_offset],
-        Command2::Eviction as u8,
+        Command::Eviction as u8,
         "expected an Eviction frame"
     );
     assert_eq!(
diff --git a/core/integration/tests/server/legacy_login_vsr.rs b/core/integration/tests/server/legacy_login_vsr.rs
index 06ae510..98b685e 100644
--- a/core/integration/tests/server/legacy_login_vsr.rs
+++ b/core/integration/tests/server/legacy_login_vsr.rs
@@ -29,7 +29,7 @@
 
 use iggy_binary_protocol::HEADER_SIZE;
 use iggy_binary_protocol::codes::{LOGIN_USER_CODE, LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE};
-use iggy_binary_protocol::consensus::{Command2, Operation, RequestHeader};
+use iggy_binary_protocol::consensus::{Command, Operation, RequestHeader};
 use integration::harness::TestHarness;
 use integration::iggy_harness;
 use std::mem::offset_of;
@@ -61,7 +61,7 @@
 /// exercises the same path a bound connection would.
 async fn assert_legacy_login_code_evicted(harness: &TestHarness, code: u32) {
     let mut header = RequestHeader {
-        command: Command2::Request,
+        command: Command::Request,
         operation: Operation::NonReplicated,
         size: u32::try_from(HEADER_SIZE).unwrap(),
         // NonReplicated leaves session / request unchecked, but the header
@@ -93,7 +93,7 @@
     let command_offset = offset_of!(RequestHeader, command);
     assert_eq!(
         reply[command_offset],
-        Command2::Eviction as u8,
+        Command::Eviction as u8,
         "expected an Eviction frame for legacy login code {code}, not a Reply"
     );
     assert_eq!(
diff --git a/core/integration/tests/server/scenarios/authentication_scenario.rs b/core/integration/tests/server/scenarios/authentication_scenario.rs
index 2736ea5..63a155f 100644
--- a/core/integration/tests/server/scenarios/authentication_scenario.rs
+++ b/core/integration/tests/server/scenarios/authentication_scenario.rs
@@ -144,17 +144,6 @@
         ) {
             continue;
         }
-        // v2 consumer-offset ops are registered in the dispatch table for the
-        // consensus/simulator pathway but are not wired into the legacy binary
-        // server's dispatch. They'll move into the server alongside the rest of
-        // the v2 surface; re-enable these codes here once that lands.
-        if matches!(
-            code,
-            STORE_CONSUMER_OFFSET_2_CODE | DELETE_CONSUMER_OFFSET_2_CODE
-        ) {
-            continue;
-        }
-
         // ================================================================
         // REQUIRES AUTH
         // ================================================================
diff --git a/core/integration/tests/server/scenarios/purge_delete_scenario.rs b/core/integration/tests/server/scenarios/purge_delete_scenario.rs
index 79f1d29..a217e1f 100644
--- a/core/integration/tests/server/scenarios/purge_delete_scenario.rs
+++ b/core/integration/tests/server/scenarios/purge_delete_scenario.rs
@@ -37,11 +37,11 @@
 /// volume carries what a 5 KiB segment used to.
 const SEGMENT_SIZE: u64 = 1024 * 1024;
 
-/// The server persists the actual `SendMessages2` batch framing: a 256-byte
+/// The server persists the actual `SendMessages` batch framing: a 256-byte
 /// command header per append (each send below is a single-message batch) plus
 /// a 48-byte per-message header, and a 24-byte sparse index entry per flush
 /// (one per message with messages_required_to_save = 1). See
-/// `server_common::send_messages2` and `stream_size_validation_scenario`.
+/// `server_common::send_messages` and `stream_size_validation_scenario`.
 ///
 /// Sized so five messages seal a [`SEGMENT_SIZE`] segment and four do not:
 /// 4 * 220304 = 881216 < 1 MiB <= 5 * 220304 = 1101520. Must stay a multiple
diff --git a/core/integration/tests/server/scenarios/stream_size_validation_scenario.rs b/core/integration/tests/server/scenarios/stream_size_validation_scenario.rs
index a7b76d4..8d6953a 100644
--- a/core/integration/tests/server/scenarios/stream_size_validation_scenario.rs
+++ b/core/integration/tests/server/scenarios/stream_size_validation_scenario.rs
@@ -38,10 +38,10 @@
 const MESSAGE_PAYLOAD_SIZE_BYTES: u64 = 57;
 const MSGS_COUNT: u64 = 117; // number of messages in a single topic after one pass of appending
 // The server accounts the actual on-disk batch framing: one 256-byte
-// `SendMessages2` command header per append pass plus a 48-byte per-message
-// header (`server_common::send_messages2::{COMMAND_HEADER_SIZE,
-// MESSAGE_HEADER_SIZE}`). Each pass below sends all `MSGS_COUNT` messages in
-// one batch.
+// `SendMessages` command header per append pass plus a 48-byte per-message
+// header (`server_common::send_messages::COMMAND_HEADER_SIZE` and
+// `iggy_binary_protocol::batch::BATCH_MESSAGE_HEADER_SIZE`). Each pass below
+// sends all `MSGS_COUNT` messages in one batch.
 const NG_BATCH_HEADER_SIZE: u64 = 256;
 const NG_MESSAGE_HEADER_SIZE: u64 = 48;
 const MSGS_SIZE: u64 =
diff --git a/core/journal/src/prepare_journal.rs b/core/journal/src/prepare_journal.rs
index f56a746..dc15e02 100644
--- a/core/journal/src/prepare_journal.rs
+++ b/core/journal/src/prepare_journal.rs
@@ -18,7 +18,7 @@
 use crate::file_storage::FileStorage;
 use crate::{Journal, JournalHandle};
 use compio::io::AsyncWriteAtExt;
-use iggy_binary_protocol::consensus::{CHECKSUM_UNSEALED, Command2, PrepareHeader};
+use iggy_binary_protocol::consensus::{CHECKSUM_UNSEALED, Command, PrepareHeader};
 use server_common::{MESSAGE_ALIGN, Message, iobuf::Owned};
 use std::cell::{Cell, OnceCell, Ref, RefCell};
 use std::fmt;
@@ -287,7 +287,7 @@
 
         let last_start = want - HEADER_SIZE;
         for offset in 0..=last_start {
-            if buf[offset + COMMAND_OFFSET] != Command2::Prepare as u8 {
+            if buf[offset + COMMAND_OFFSET] != Command::Prepare as u8 {
                 continue;
             }
             let candidate = &buf[offset..offset + HEADER_SIZE];
@@ -310,7 +310,7 @@
 fn valid_entry_header(scratch: &mut Owned<16>, bytes: &[u8]) -> Option<PrepareHeader> {
     scratch.as_mut_slice().copy_from_slice(bytes);
     let header = *bytemuck::checked::try_from_bytes::<PrepareHeader>(scratch.as_slice()).ok()?;
-    if header.command != Command2::Prepare
+    if header.command != Command::Prepare
         || (header.size as usize) < HEADER_SIZE
         || u64::from(header.size) > MAX_ENTRY_SIZE
     {
@@ -454,7 +454,7 @@
             let header: PrepareHeader = *header_ref;
 
             // Validate: must be a Prepare command with sane size
-            if header.command != Command2::Prepare
+            if header.command != Command::Prepare
                 || (header.size as usize) < HEADER_SIZE
                 || u64::from(header.size) > MAX_ENTRY_SIZE
             {
@@ -1206,7 +1206,7 @@
             &mut buffer.as_mut_slice()[..HEADER_SIZE],
         );
         header.size = total_size as u32;
-        header.command = Command2::Prepare;
+        header.command = Command::Prepare;
         header.op = op;
         header.operation = Operation::CreateStream;
         header.checksum_body = checksum_body;
@@ -1699,7 +1699,7 @@
 
     #[compio::test]
     async fn corrupt_command_byte_truncates_on_reopen() {
-        // Bit-flipped `Command2` discriminant: must truncate, not panic.
+        // Bit-flipped `Command` discriminant: must truncate, not panic.
         let dir = tempdir().unwrap();
         let path = dir.path().join("journal.wal");
 
@@ -1719,7 +1719,7 @@
             use std::io::{Seek, SeekFrom, Write};
             let mut file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
             file.seek(SeekFrom::Start(command_byte_offset)).unwrap();
-            file.write_all(&[99u8]).unwrap(); // out of range for Command2
+            file.write_all(&[99u8]).unwrap(); // out of range for Command
             file.sync_all().unwrap();
         }
 
@@ -1810,7 +1810,7 @@
         {
             use std::io::Write;
             let mut file = std::fs::File::create(&path).unwrap();
-            // All-0xFF is not a valid `Command2`/`Operation` bit pattern,
+            // All-0xFF is not a valid `Command`/`Operation` bit pattern,
             // so `try_from_bytes` rejects the header.
             file.write_all(&[0xFF_u8; HEADER_SIZE]).unwrap();
             // Sparse-extend past MAX_ENTRY_SIZE so the corruption at pos 0
@@ -1857,7 +1857,7 @@
             use std::io::{Seek, SeekFrom, Write};
             let mut file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
             file.seek(SeekFrom::Start(command_byte_offset)).unwrap();
-            file.write_all(&[99u8]).unwrap(); // out of range for Command2
+            file.write_all(&[99u8]).unwrap(); // out of range for Command
             file.sync_all().unwrap();
         }
 
@@ -1890,7 +1890,7 @@
         let header = bytemuck::checked::from_bytes_mut::<PrepareHeader>(
             &mut buffer.as_mut_slice()[..HEADER_SIZE],
         );
-        header.command = Command2::Prepare;
+        header.command = Command::Prepare;
         header.op = 1;
         header.operation = Operation::CreateStream;
         header.size = (HEADER_SIZE + 48) as u32; // 16 bytes of slack
diff --git a/core/message_bus/src/framing.rs b/core/message_bus/src/framing.rs
index 39ce24d..ab626c6 100644
--- a/core/message_bus/src/framing.rs
+++ b/core/message_bus/src/framing.rs
@@ -175,10 +175,10 @@
 mod tests {
     use super::*;
     use compio::net::{TcpListener, TcpStream};
-    use iggy_binary_protocol::{Command2, SIZE_FIELD_OFFSET};
+    use iggy_binary_protocol::{Command, SIZE_FIELD_OFFSET};
 
     #[allow(clippy::cast_possible_truncation)]
-    fn make_header_only(command: Command2) -> Message<GenericHeader> {
+    fn make_header_only(command: Command) -> Message<GenericHeader> {
         Message::<GenericHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut GenericHeader| {
             h.command = command;
             h.size = HEADER_SIZE as u32;
@@ -200,10 +200,10 @@
     #[allow(clippy::future_not_send)]
     async fn write_then_read_header_only() {
         let (mut a, mut b) = local_pair().await;
-        let msg = make_header_only(Command2::Ping);
+        let msg = make_header_only(Command::Ping);
         write_message(&mut a, msg).await.unwrap();
         let read = read_message(&mut b, MAX_MESSAGE_SIZE).await.unwrap();
-        assert_eq!(read.header().command, Command2::Ping);
+        assert_eq!(read.header().command, Command::Ping);
         assert_eq!(read.header().size as usize, HEADER_SIZE);
     }
 
diff --git a/core/message_bus/src/installer/tcp.rs b/core/message_bus/src/installer/tcp.rs
index 3530d14..12cfffb 100644
--- a/core/message_bus/src/installer/tcp.rs
+++ b/core/message_bus/src/installer/tcp.rs
@@ -30,7 +30,7 @@
 use async_channel::Receiver;
 use compio::net::TcpStream;
 use futures::FutureExt;
-use iggy_binary_protocol::{Command2, GenericHeader};
+use iggy_binary_protocol::{Command, GenericHeader};
 use server_common::Message;
 use std::cell::Cell;
 use std::rc::Rc;
@@ -275,7 +275,7 @@
                     return;
                 }
                 let cmd = msg.header().command;
-                if cmd != Command2::Request {
+                if cmd != Command::Request {
                     warn!(
                         client = client_id,
                         ?cmd,
diff --git a/core/message_bus/src/lib.rs b/core/message_bus/src/lib.rs
index 6445911..414312a 100644
--- a/core/message_bus/src/lib.rs
+++ b/core/message_bus/src/lib.rs
@@ -1418,7 +1418,7 @@
 }
 
 /// Reserved client id stamped on server-generated auto-commit
-/// `StoreConsumerOffset2` ops (a poll's `auto_commit` replicated for failover).
+/// `StoreConsumerOffset` ops (a poll's `auto_commit` replicated for failover).
 ///
 /// Never belongs to a live connection: `mint_client_id` produces
 /// `(shard << 112) | seq` and no real shard is `u16::MAX`, so `u128::MAX` is
@@ -1463,13 +1463,13 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use iggy_binary_protocol::{Command2, HEADER_SIZE};
+    use iggy_binary_protocol::{Command, HEADER_SIZE};
     use std::cell::RefCell;
 
     #[allow(clippy::cast_possible_truncation)]
     fn dummy_message() -> Message<GenericHeader> {
         Message::<GenericHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut GenericHeader| {
-            h.command = Command2::Prepare;
+            h.command = Command::Prepare;
             h.size = HEADER_SIZE as u32;
         })
     }
@@ -1513,7 +1513,7 @@
     #[allow(clippy::cast_possible_truncation)]
     fn reply_message(request: u64) -> Message<ReplyHeader> {
         Message::<ReplyHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut ReplyHeader| {
-            h.command = Command2::Reply;
+            h.command = Command::Reply;
             h.size = HEADER_SIZE as u32;
             h.request = request;
         })
diff --git a/core/message_bus/src/lifecycle/connection_registry.rs b/core/message_bus/src/lifecycle/connection_registry.rs
index 8dc22ad..cda45a8 100644
--- a/core/message_bus/src/lifecycle/connection_registry.rs
+++ b/core/message_bus/src/lifecycle/connection_registry.rs
@@ -949,14 +949,14 @@
 mod tests {
     use super::*;
     use crate::lifecycle::Shutdown;
-    use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+    use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
     use server_common::Message;
 
     #[allow(clippy::cast_possible_truncation)]
     fn make_bus_msg() -> BusMessage {
         Message::<GenericHeader>::new(HEADER_SIZE)
             .transmute_header(|_, h: &mut GenericHeader| {
-                h.command = Command2::Ping;
+                h.command = Command::Ping;
                 h.size = HEADER_SIZE as u32;
             })
             .into_frozen()
diff --git a/core/message_bus/src/replica/auth.rs b/core/message_bus/src/replica/auth.rs
index b455f69..3b46138 100644
--- a/core/message_bus/src/replica/auth.rs
+++ b/core/message_bus/src/replica/auth.rs
@@ -22,7 +22,7 @@
 //!
 //! The handshake is a 3-message mutual challenge-response riding the
 //! already-zeroed `reserved_command` bytes of the 256-byte `GenericHeader`.
-//! Each message has its own `Command2` discriminant (no Ping/Pong reuse):
+//! Each message has its own `Command` discriminant (no Ping/Pong reuse):
 //!
 //! 1. `ReplicaHello`     dialer -> acceptor: `nonce_d` (no MAC; the dialer has
 //!    no acceptor nonce yet);
diff --git a/core/message_bus/src/replica/handshake.rs b/core/message_bus/src/replica/handshake.rs
index 2267666..13d81ce 100644
--- a/core/message_bus/src/replica/handshake.rs
+++ b/core/message_bus/src/replica/handshake.rs
@@ -65,7 +65,7 @@
 use crate::replica::auth::{self, ChannelBinding, HandshakeStatus, ReplicaAuth, Transcript};
 use crate::{GenericHeader, Message};
 use compio::io::{AsyncRead, AsyncWrite};
-use iggy_binary_protocol::{Command2, HEADER_SIZE};
+use iggy_binary_protocol::{Command, HEADER_SIZE};
 use iggy_common::IggyError;
 use rustls::pki_types::ServerName;
 use std::collections::HashMap;
@@ -154,7 +154,7 @@
     // fd without reading, so a reject frame would land in its VSR reader instead.
     let nackable = auth.is_some() && has_nonce;
 
-    if header.command != Command2::ReplicaHello {
+    if header.command != Command::ReplicaHello {
         return reject(
             stream,
             our_cluster,
@@ -238,7 +238,7 @@
     // reject here is log-only (no frame).
     // Check the command before the MAC: the finish frame is identified by its
     // own discriminant, not by handshake position.
-    if finish.header().command != Command2::ReplicaFinish {
+    if finish.header().command != Command::ReplicaFinish {
         return reject(
             stream,
             our_cluster,
@@ -320,7 +320,7 @@
             return Err(());
         }
     };
-    if challenge.header().command != Command2::ReplicaChallenge {
+    if challenge.header().command != Command::ReplicaChallenge {
         warn!(
             replica = peer_id,
             command = ?challenge.header().command,
@@ -415,7 +415,7 @@
     #[allow(clippy::cast_possible_truncation)]
     Message::<GenericHeader>::new(size_of::<GenericHeader>()).transmute_header(
         |_, h: &mut GenericHeader| {
-            h.command = Command2::ReplicaChallenge;
+            h.command = Command::ReplicaChallenge;
             h.cluster = cluster_id;
             h.replica = replica_id;
             h.size = HEADER_SIZE as u32;
@@ -441,7 +441,7 @@
     #[allow(clippy::cast_possible_truncation)]
     Message::<GenericHeader>::new(size_of::<GenericHeader>()).transmute_header(
         |_, h: &mut GenericHeader| {
-            h.command = Command2::ReplicaHello;
+            h.command = Command::ReplicaHello;
             h.cluster = cluster_id;
             h.replica = replica_id;
             h.size = HEADER_SIZE as u32;
@@ -462,7 +462,7 @@
     #[allow(clippy::cast_possible_truncation)]
     Message::<GenericHeader>::new(size_of::<GenericHeader>()).transmute_header(
         |_, h: &mut GenericHeader| {
-            h.command = Command2::ReplicaFinish;
+            h.command = Command::ReplicaFinish;
             h.cluster = cluster_id;
             h.replica = replica_id;
             h.size = HEADER_SIZE as u32;
diff --git a/core/message_bus/src/transports/quic.rs b/core/message_bus/src/transports/quic.rs
index 728c632..26550d5 100644
--- a/core/message_bus/src/transports/quic.rs
+++ b/core/message_bus/src/transports/quic.rs
@@ -90,7 +90,7 @@
     crypto::rustls::QuicServerConfig,
 };
 use futures::FutureExt;
-use iggy_binary_protocol::{Command2, HEADER_SIZE, ReplyHeader, RequestHeader};
+use iggy_binary_protocol::{Command, HEADER_SIZE, ReplyHeader, RequestHeader};
 use std::io;
 use std::net::SocketAddr;
 use std::sync::Arc;
@@ -300,7 +300,7 @@
 /// Closing the connection drops the mailbox and any late reply with it.
 const REPLY_WAIT_BACKSTOP: Duration = Duration::from_mins(1);
 
-/// `Command2` byte offset shared by every consensus header (after the
+/// `Command` byte offset shared by every consensus header (after the
 /// fixed checksum/cluster/size/view/release prefix).
 const COMMAND_OFFSET: usize = 60;
 
@@ -316,13 +316,13 @@
     Unkeyed,
 }
 
-/// Decide how to route an outbound frame. Only `Command2::Reply` carries a
+/// Decide how to route an outbound frame. Only `Command::Reply` carries a
 /// `request` id; everything else must pass through unfiltered.
 fn reply_route(frame: &BusMessage) -> ReplyRoute {
     let bytes = frame.as_slice();
     let is_reply = bytes
         .get(COMMAND_OFFSET)
-        .is_some_and(|&command| command == Command2::Reply as u8);
+        .is_some_and(|&command| command == Command::Reply as u8);
     if !is_reply {
         return ReplyRoute::Unkeyed;
     }
@@ -554,7 +554,7 @@
     use async_channel::bounded;
     use compio::io::AsyncWrite;
     use compio_quic::ClientBuilder;
-    use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE, SIZE_FIELD_OFFSET};
+    use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE, SIZE_FIELD_OFFSET};
     use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
     use server_common::iobuf::Frozen;
     use server_common::{MESSAGE_ALIGN, Message};
@@ -566,7 +566,7 @@
         let _ = rustls::crypto::ring::default_provider().install_default();
     }
 
-    fn header_only(command: Command2) -> Frozen<MESSAGE_ALIGN> {
+    fn header_only(command: Command) -> Frozen<MESSAGE_ALIGN> {
         #[allow(clippy::cast_possible_truncation)]
         Message::<GenericHeader>::new(HEADER_SIZE)
             .transmute_header(|_, h: &mut GenericHeader| {
@@ -654,11 +654,11 @@
             // and then awaits exactly one reply on `rx` before
             // accepting the next bidi. Feed three replies in step.
             let a = in_rx.recv().await.unwrap();
-            out_tx.send(header_only(Command2::Reply)).await.unwrap();
+            out_tx.send(header_only(Command::Reply)).await.unwrap();
             let b = in_rx.recv().await.unwrap();
-            out_tx.send(header_only(Command2::Reply)).await.unwrap();
+            out_tx.send(header_only(Command::Reply)).await.unwrap();
             let c = in_rx.recv().await.unwrap();
-            out_tx.send(header_only(Command2::Reply)).await.unwrap();
+            out_tx.send(header_only(Command::Reply)).await.unwrap();
             shutdown.trigger();
             let _ = handle.await;
             (a.header().command, b.header().command, c.header().command)
@@ -671,7 +671,7 @@
         let connection = connecting.await.expect("client handshake");
 
         // Three sequential bidis, one frame each.
-        for command in [Command2::Ping, Command2::Prepare, Command2::Request] {
+        for command in [Command::Ping, Command::Prepare, Command::Request] {
             let (mut send, _recv) = connection.open_bi_wait().await.expect("open_bi");
             let BufResult(result, _) = send.write_all(header_only(command)).await;
             result.expect("write");
@@ -682,9 +682,9 @@
             .await
             .expect("server task within 5s")
             .unwrap();
-        assert_eq!(a, Command2::Ping);
-        assert_eq!(b, Command2::Prepare);
-        assert_eq!(c, Command2::Request);
+        assert_eq!(a, Command::Ping);
+        assert_eq!(b, Command::Prepare);
+        assert_eq!(c, Command::Request);
     }
 
     /// An oversize size field in the request header makes
diff --git a/core/message_bus/src/transports/tcp.rs b/core/message_bus/src/transports/tcp.rs
index 340d587..87a3103 100644
--- a/core/message_bus/src/transports/tcp.rs
+++ b/core/message_bus/src/transports/tcp.rs
@@ -294,14 +294,14 @@
     use super::*;
     use crate::lifecycle::Shutdown;
     use async_channel::bounded;
-    use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE, SIZE_FIELD_OFFSET};
+    use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE, SIZE_FIELD_OFFSET};
     use server_common::MESSAGE_ALIGN;
     use server_common::Message;
     use server_common::iobuf::Frozen;
     use std::time::Duration;
 
     #[allow(clippy::cast_possible_truncation)]
-    fn header_only(command: Command2) -> Frozen<MESSAGE_ALIGN> {
+    fn header_only(command: Command) -> Frozen<MESSAGE_ALIGN> {
         Message::<GenericHeader>::new(HEADER_SIZE)
             .transmute_header(|_, h: &mut GenericHeader| {
                 h.command = command;
@@ -369,7 +369,7 @@
         let (_server_out, server_in, server_shutdown, server_handle) =
             drive(TcpTransportConn::new(server));
 
-        for cmd in [Command2::Ping, Command2::Prepare, Command2::Request] {
+        for cmd in [Command::Ping, Command::Prepare, Command::Request] {
             client_out.send(header_only(cmd)).await.unwrap();
         }
 
@@ -385,9 +385,9 @@
         let a = recv_with_timeout(&server_in).await;
         let b = recv_with_timeout(&server_in).await;
         let c = recv_with_timeout(&server_in).await;
-        assert_eq!(a.header().command, Command2::Ping);
-        assert_eq!(b.header().command, Command2::Prepare);
-        assert_eq!(c.header().command, Command2::Request);
+        assert_eq!(a.header().command, Command::Ping);
+        assert_eq!(b.header().command, Command::Prepare);
+        assert_eq!(c.header().command, Command::Request);
 
         client_shutdown.trigger();
         server_shutdown.trigger();
diff --git a/core/message_bus/src/transports/tcp_tls.rs b/core/message_bus/src/transports/tcp_tls.rs
index 7c699d9..a248fb8 100644
--- a/core/message_bus/src/transports/tcp_tls.rs
+++ b/core/message_bus/src/transports/tcp_tls.rs
@@ -657,7 +657,7 @@
     use crate::transports::tls::{install_default_crypto_provider, self_signed_for_loopback};
     use async_channel::{Receiver, Sender, bounded};
     use compio::net::TcpListener;
-    use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+    use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
     use rustls::RootCertStore;
     use server_common::{MESSAGE_ALIGN, Message, iobuf::Frozen};
     use std::net::SocketAddr;
@@ -719,7 +719,7 @@
     }
 
     #[allow(clippy::cast_possible_truncation)]
-    fn header_only(command: Command2) -> Frozen<MESSAGE_ALIGN> {
+    fn header_only(command: Command) -> Frozen<MESSAGE_ALIGN> {
         Message::<GenericHeader>::new(HEADER_SIZE)
             .transmute_header(|_, h: &mut GenericHeader| {
                 h.command = command;
@@ -729,7 +729,7 @@
     }
 
     #[allow(clippy::cast_possible_truncation)]
-    fn padded(command: Command2, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
+    fn padded(command: Command, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
         Message::<GenericHeader>::new(total_size)
             .transmute_header(|_, h: &mut GenericHeader| {
                 h.command = command;
@@ -780,24 +780,24 @@
         let (client_out, client_in, client_shutdown, client_handle) = drive(client_conn);
 
         client_out
-            .send(header_only(Command2::Request))
+            .send(header_only(Command::Request))
             .await
             .expect("client send");
         let received = compio::time::timeout(Duration::from_secs(5), server_in.recv())
             .await
             .expect("server recv within 5 s")
             .expect("server frame");
-        assert_eq!(received.header().command, Command2::Request);
+        assert_eq!(received.header().command, Command::Request);
 
         server_out
-            .send(header_only(Command2::Reply))
+            .send(header_only(Command::Reply))
             .await
             .expect("server send");
         let reply = compio::time::timeout(Duration::from_secs(5), client_in.recv())
             .await
             .expect("client recv within 5 s")
             .expect("client frame");
-        assert_eq!(reply.header().command, Command2::Reply);
+        assert_eq!(reply.header().command, Command::Reply);
 
         server_shutdown.trigger();
         client_shutdown.trigger();
@@ -854,14 +854,14 @@
         let (client_out, _client_in, client_shutdown, client_handle) = drive(client_conn);
 
         client_out
-            .send(padded(Command2::Request, total))
+            .send(padded(Command::Request, total))
             .await
             .expect("client send 1 MiB");
         let received = compio::time::timeout(Duration::from_secs(10), server_in.recv())
             .await
             .expect("server recv within 10 s")
             .expect("server frame");
-        assert_eq!(received.header().command, Command2::Request);
+        assert_eq!(received.header().command, Command::Request);
         assert_eq!(received.header().size as usize, total);
 
         server_shutdown.trigger();
@@ -924,14 +924,14 @@
         let (client_out, _client_in, client_shutdown, client_handle) = drive(client_conn);
 
         client_out
-            .send(header_only(Command2::Request))
+            .send(header_only(Command::Request))
             .await
             .expect("client send");
         let received = compio::time::timeout(Duration::from_secs(5), server_in.recv())
             .await
             .expect("server recv within 5 s")
             .expect("server frame");
-        assert_eq!(received.header().command, Command2::Request);
+        assert_eq!(received.header().command, Command::Request);
 
         server_shutdown.trigger();
         client_shutdown.trigger();
diff --git a/core/message_bus/src/transports/ws.rs b/core/message_bus/src/transports/ws.rs
index e7b6253..f985191 100644
--- a/core/message_bus/src/transports/ws.rs
+++ b/core/message_bus/src/transports/ws.rs
@@ -316,13 +316,13 @@
     use crate::lifecycle::Shutdown;
     use async_channel::{Receiver, Sender, bounded};
     use compio::net::TcpListener;
-    use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+    use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
     use server_common::Message;
     use server_common::iobuf::Frozen;
     use std::time::Duration;
 
     #[allow(clippy::cast_possible_truncation)]
-    fn header_only(command: Command2) -> Frozen<MESSAGE_ALIGN> {
+    fn header_only(command: Command) -> Frozen<MESSAGE_ALIGN> {
         Message::<GenericHeader>::new(HEADER_SIZE)
             .transmute_header(|_, h: &mut GenericHeader| {
                 h.command = command;
@@ -332,7 +332,7 @@
     }
 
     #[allow(clippy::cast_possible_truncation)]
-    fn padded(command: Command2, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
+    fn padded(command: Command, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
         Message::<GenericHeader>::new(total_size)
             .transmute_header(|_, h: &mut GenericHeader| {
                 h.command = command;
@@ -432,23 +432,23 @@
         let (server_out, server_in, server_shutdown, server_handle) = drive(server_conn);
 
         // Client raw-sends a Request; the server pump reads it.
-        raw_send(&mut client_ws, header_only(Command2::Request)).await;
+        raw_send(&mut client_ws, header_only(Command::Request)).await;
         let received = compio::time::timeout(Duration::from_secs(5), server_in.recv())
             .await
             .expect("server recv within 5 s")
             .expect("server frame");
-        assert_eq!(received.header().command, Command2::Request);
+        assert_eq!(received.header().command, Command::Request);
 
         // Server replies via its outbound mailbox; the serial pump writes
         // the reply on the same bidi the request arrived on, client reads.
         server_out
-            .send(header_only(Command2::Reply))
+            .send(header_only(Command::Reply))
             .await
             .expect("server send");
         let reply = compio::time::timeout(Duration::from_secs(5), raw_recv(&mut client_ws))
             .await
             .expect("client recv within 5 s");
-        assert_eq!(reply.header().command, Command2::Reply);
+        assert_eq!(reply.header().command, Command::Reply);
 
         server_shutdown.trigger();
         let _ = compio::time::timeout(Duration::from_secs(5), server_handle).await;
@@ -464,12 +464,12 @@
         let server_conn = WsTransportConn::new_server(server_ws);
         let (_server_out, server_in, server_shutdown, server_handle) = drive(server_conn);
 
-        raw_send(&mut client_ws, padded(Command2::Request, total)).await;
+        raw_send(&mut client_ws, padded(Command::Request, total)).await;
         let received = compio::time::timeout(Duration::from_secs(15), server_in.recv())
             .await
             .expect("server recv within 15 s")
             .expect("server frame");
-        assert_eq!(received.header().command, Command2::Request);
+        assert_eq!(received.header().command, Command::Request);
         assert_eq!(received.header().size as usize, total);
 
         server_shutdown.trigger();
@@ -487,7 +487,7 @@
         let (_server_out, server_in, _server_shutdown, server_handle) =
             drive_with_cap(server_conn, CUSTOM_CAP);
 
-        raw_send(&mut client_ws, padded(Command2::Request, OVER_CAP)).await;
+        raw_send(&mut client_ws, padded(Command::Request, OVER_CAP)).await;
 
         // Decode rejection tears the server pump down; join must complete
         // within the grace window and no frame must surface to in_rx.
diff --git a/core/message_bus/src/transports/wss.rs b/core/message_bus/src/transports/wss.rs
index a83c67d..e30b6a2 100644
--- a/core/message_bus/src/transports/wss.rs
+++ b/core/message_bus/src/transports/wss.rs
@@ -476,7 +476,7 @@
     use crate::transports::tls::{install_default_crypto_provider, self_signed_for_loopback};
     use async_channel::{Receiver, Sender, bounded};
     use compio::net::TcpListener;
-    use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+    use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
     use rustls::RootCertStore;
     use server_common::MESSAGE_ALIGN;
     use server_common::Message;
@@ -531,7 +531,7 @@
     }
 
     #[allow(clippy::cast_possible_truncation)]
-    fn header_only(command: Command2) -> Frozen<MESSAGE_ALIGN> {
+    fn header_only(command: Command) -> Frozen<MESSAGE_ALIGN> {
         Message::<GenericHeader>::new(HEADER_SIZE)
             .transmute_header(|_, h: &mut GenericHeader| {
                 h.command = command;
@@ -541,7 +541,7 @@
     }
 
     #[allow(clippy::cast_possible_truncation)]
-    fn padded(command: Command2, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
+    fn padded(command: Command, total_size: usize) -> Frozen<MESSAGE_ALIGN> {
         Message::<GenericHeader>::new(total_size)
             .transmute_header(|_, h: &mut GenericHeader| {
                 h.command = command;
@@ -604,24 +604,24 @@
         let (client_out, client_in, client_shutdown, client_handle) = drive(client_conn);
 
         client_out
-            .send(header_only(Command2::Request))
+            .send(header_only(Command::Request))
             .await
             .expect("client send");
         let received = compio::time::timeout(Duration::from_secs(5), server_in.recv())
             .await
             .expect("server recv within 5 s")
             .expect("server frame");
-        assert_eq!(received.header().command, Command2::Request);
+        assert_eq!(received.header().command, Command::Request);
 
         server_out
-            .send(header_only(Command2::Reply))
+            .send(header_only(Command::Reply))
             .await
             .expect("server send");
         let reply = compio::time::timeout(Duration::from_secs(5), client_in.recv())
             .await
             .expect("client recv within 5 s")
             .expect("client frame");
-        assert_eq!(reply.header().command, Command2::Reply);
+        assert_eq!(reply.header().command, Command::Reply);
 
         server_shutdown.trigger();
         client_shutdown.trigger();
@@ -647,14 +647,14 @@
         let (client_out, _client_in, client_shutdown, client_handle) = drive(client_conn);
 
         client_out
-            .send(padded(Command2::Request, total))
+            .send(padded(Command::Request, total))
             .await
             .expect("client send 1 MiB");
         let received = compio::time::timeout(Duration::from_secs(15), server_in.recv())
             .await
             .expect("server recv within 15 s")
             .expect("server frame");
-        assert_eq!(received.header().command, Command2::Request);
+        assert_eq!(received.header().command, Command::Request);
         assert_eq!(received.header().size as usize, total);
 
         server_shutdown.trigger();
@@ -683,7 +683,7 @@
             drive_with_cap(client_conn, framing::MAX_MESSAGE_SIZE);
 
         client_out
-            .send(padded(Command2::Request, OVER_CAP))
+            .send(padded(Command::Request, OVER_CAP))
             .await
             .expect("client send oversize");
 
diff --git a/core/message_bus/tests/backpressure.rs b/core/message_bus/tests/backpressure.rs
index ef46d92..a329d50 100644
--- a/core/message_bus/tests/backpressure.rs
+++ b/core/message_bus/tests/backpressure.rs
@@ -28,7 +28,7 @@
     header_only, install_dialed_replicas_locally, install_replicas_locally, loopback,
     set_replica_ctx,
 };
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
 use message_bus::replica::listener::{MessageHandler, bind, run};
 use message_bus::{IggyMessageBus, MessageBus, SendError};
@@ -82,7 +82,7 @@
     // the exact threshold, only that it is finite and reachable.
     let mut hit_backpressure = false;
     for _ in 0..100_000 {
-        let msg = header_only(Command2::Prepare, CLUSTER, 0);
+        let msg = header_only(Command::Prepare, CLUSTER, 0);
         match bus0.send_to_replica(1, msg.into_frozen()).await {
             Ok(()) => {}
             Err(SendError::Backpressure) => {
diff --git a/core/message_bus/tests/common/mod.rs b/core/message_bus/tests/common/mod.rs
index b90a1c6..b5630b7 100644
--- a/core/message_bus/tests/common/mod.rs
+++ b/core/message_bus/tests/common/mod.rs
@@ -26,7 +26,7 @@
 
 #![allow(dead_code)] // each test binary uses a subset
 
-use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
 use message_bus::ConnectionInstaller;
 use message_bus::client_listener::RequestHandler;
 use message_bus::replica::auth::ReplicaAuth;
@@ -67,7 +67,7 @@
 /// Used by tests to fabricate `Request`, `Reply`, `Ping`, etc. directly.
 #[must_use]
 #[allow(clippy::cast_possible_truncation)]
-pub fn header_only(command: Command2, cluster: u128, replica: u8) -> Message<GenericHeader> {
+pub fn header_only(command: Command, cluster: u128, replica: u8) -> Message<GenericHeader> {
     Message::<GenericHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut GenericHeader| {
         h.command = command;
         h.cluster = cluster;
diff --git a/core/message_bus/tests/duplicate_client_id.rs b/core/message_bus/tests/duplicate_client_id.rs
index 8721b70..0f311d3 100644
--- a/core/message_bus/tests/duplicate_client_id.rs
+++ b/core/message_bus/tests/duplicate_client_id.rs
@@ -25,7 +25,7 @@
 
 use common::{header_only, loopback, test_client_meta};
 use compio::net::{TcpListener, TcpStream};
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use message_bus::client_listener::RequestHandler;
 use message_bus::framing::write_message;
 use message_bus::installer::install_client_tcp;
@@ -145,7 +145,7 @@
     // Push a valid Request frame down the orphan reader's wire. Before
     // the fix, the orphan would call `on_request(client_id, msg)` and
     // bump the counter; with the fix the `aborted` guard skips it.
-    let msg = header_only(Command2::Request, 0, 0);
+    let msg = header_only(Command::Request, 0, 0);
     write_message(&mut second_peer, msg)
         .await
         .expect("write Request");
diff --git a/core/message_bus/tests/graceful_shutdown.rs b/core/message_bus/tests/graceful_shutdown.rs
index 1803803..b37ac40 100644
--- a/core/message_bus/tests/graceful_shutdown.rs
+++ b/core/message_bus/tests/graceful_shutdown.rs
@@ -71,7 +71,7 @@
     assert!(bus.is_shutting_down());
 
     // Sends after shutdown must fail with the right variant.
-    let dummy = common::header_only(iggy_binary_protocol::Command2::Reply, 0, 0);
+    let dummy = common::header_only(iggy_binary_protocol::Command::Reply, 0, 0);
     let err = bus
         .send_to_client(1u128 << 112, dummy.into_frozen())
         .await
diff --git a/core/message_bus/tests/head_of_line.rs b/core/message_bus/tests/head_of_line.rs
index e367364..d632f6b 100644
--- a/core/message_bus/tests/head_of_line.rs
+++ b/core/message_bus/tests/head_of_line.rs
@@ -30,7 +30,7 @@
     set_replica_ctx,
 };
 use compio::net::TcpListener;
-use iggy_binary_protocol::{Command2, HEADER_SIZE};
+use iggy_binary_protocol::{Command, HEADER_SIZE};
 use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
 use message_bus::replica::listener::{MessageHandler, bind, run};
 use message_bus::{IggyMessageBus, MessageBus, SendError};
@@ -79,7 +79,7 @@
                 .await
                 .is_ok()
             {
-                let ack = header_only(Command2::Ping, CLUSTER, 2);
+                let ack = header_only(Command::Ping, CLUSTER, 2);
                 let _ = message_bus::framing::write_message(&mut stream, ack).await;
             }
             held_streams_clone.borrow_mut().push(stream);
@@ -108,7 +108,7 @@
 
     // Baseline: a single send to A before any saturation. Both peers are
     // healthy, both sends take try_send fast-path time.
-    bus0.send_to_replica(1, header_only(Command2::Prepare, 0, 0).into_frozen())
+    bus0.send_to_replica(1, header_only(Command::Prepare, 0, 0).into_frozen())
         .await
         .expect("send to A (baseline)");
 
@@ -119,7 +119,7 @@
     let mut b_saturated = false;
     for _ in 0..100_000 {
         match bus0
-            .send_to_replica(2, header_only(Command2::Prepare, 0, 0).into_frozen())
+            .send_to_replica(2, header_only(Command::Prepare, 0, 0).into_frozen())
             .await
         {
             Ok(()) => {}
@@ -140,14 +140,14 @@
     // against a freshly-timed B send that we expect to return Backpressure
     // instantly. HOL would manifest as A being as slow as a blocked writev.
     let send_a_start = Instant::now();
-    bus0.send_to_replica(1, header_only(Command2::Prepare, 0, 0).into_frozen())
+    bus0.send_to_replica(1, header_only(Command::Prepare, 0, 0).into_frozen())
         .await
         .expect("send to A while B is saturated");
     let send_a_elapsed = send_a_start.elapsed();
 
     let send_b_start = Instant::now();
     let send_b_result = bus0
-        .send_to_replica(2, header_only(Command2::Prepare, 0, 0).into_frozen())
+        .send_to_replica(2, header_only(Command::Prepare, 0, 0).into_frozen())
         .await;
     let send_b_elapsed = send_b_start.elapsed();
     let b_backpressured = matches!(send_b_result, Err(SendError::Backpressure));
diff --git a/core/message_bus/tests/quic_client_roundtrip.rs b/core/message_bus/tests/quic_client_roundtrip.rs
index 679bf66..23ec2b9 100644
--- a/core/message_bus/tests/quic_client_roundtrip.rs
+++ b/core/message_bus/tests/quic_client_roundtrip.rs
@@ -27,7 +27,7 @@
 use compio::BufResult;
 use compio::io::AsyncWriteExt;
 use compio_quic::{ClientBuilder, Endpoint};
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use message_bus::QuicTuning;
 use message_bus::client_listener::RequestHandler;
 use message_bus::client_listener::quic::{bind, run};
@@ -68,10 +68,10 @@
 
     let bus_for_handler = bus.clone();
     let on_request: RequestHandler = Rc::new(move |client_id, msg| {
-        assert_eq!(msg.header().command, Command2::Request);
+        assert_eq!(msg.header().command, Command::Request);
         let bus = bus_for_handler.clone();
         compio::runtime::spawn(async move {
-            let reply = header_only(Command2::Reply, 42, 0);
+            let reply = header_only(Command::Reply, 42, 0);
             bus.send_to_client(client_id, reply.into_frozen())
                 .await
                 .expect("send_to_client should succeed");
@@ -103,7 +103,7 @@
     // pattern; the server's accept_bi loop accepts the bidi, dispatches
     // the request, writes the Reply, `finish()`-es the send half.
     let (mut send, mut recv) = connection.open_bi_wait().await.expect("open_bi");
-    let request = header_only(Command2::Request, 42, 0).into_frozen();
+    let request = header_only(Command::Request, 42, 0).into_frozen();
     let BufResult(result, _) = send.write_all(request).await;
     result.expect("client write request");
     send.finish().expect("client finish");
@@ -115,7 +115,7 @@
     .await
     .expect("client must receive reply within 5 s")
     .expect("reply frame");
-    assert_eq!(reply.header().command, Command2::Reply);
+    assert_eq!(reply.header().command, Command::Reply);
     assert_eq!(reply.header().cluster, 42);
 
     let outcome = bus.shutdown(Duration::from_secs(2)).await;
@@ -143,7 +143,7 @@
     let (request_tx, request_rx) = bounded::<()>(8);
     let request_tx = Rc::new(request_tx);
     let on_request: RequestHandler = Rc::new(move |_client_id, msg| {
-        assert_eq!(msg.header().command, Command2::Request);
+        assert_eq!(msg.header().command, Command::Request);
         let _ = request_tx.try_send(());
     });
 
@@ -189,7 +189,7 @@
             .expect("fast client open_bi within 2 s")
             .expect("open_bi");
 
-    let request = header_only(Command2::Request, 1, 0).into_frozen();
+    let request = header_only(Command::Request, 1, 0).into_frozen();
     let BufResult(result, _) = send.write_all(request).await;
     result.expect("fast client write request");
     send.finish().expect("fast client finish");
diff --git a/core/message_bus/tests/replica_auth_handshake.rs b/core/message_bus/tests/replica_auth_handshake.rs
index 44e4893..d20787b 100644
--- a/core/message_bus/tests/replica_auth_handshake.rs
+++ b/core/message_bus/tests/replica_auth_handshake.rs
@@ -31,7 +31,7 @@
     install_dialed_replicas_locally, install_replicas_locally, loopback, set_replica_ctx,
 };
 use compio::net::TcpStream;
-use iggy_binary_protocol::{Command2, GenericHeader, HEADER_SIZE};
+use iggy_binary_protocol::{Command, GenericHeader, HEADER_SIZE};
 use iggy_common::IggyError;
 use message_bus::connector::start as start_connector;
 use message_bus::framing::{self, MAX_MESSAGE_SIZE};
@@ -220,7 +220,7 @@
     let resp = framing::read_message(&mut stream, MAX_MESSAGE_SIZE)
         .await
         .expect("read reject");
-    assert_eq!(resp.header().command, Command2::ReplicaChallenge);
+    assert_eq!(resp.header().command, Command::ReplicaChallenge);
     assert_eq!(
         auth::read_status(&resp.header().reserved_command),
         HandshakeStatus::ClusterMismatch,
@@ -269,14 +269,14 @@
     let challenge = framing::read_message(&mut stream, MAX_MESSAGE_SIZE)
         .await
         .expect("read challenge");
-    assert_eq!(challenge.header().command, Command2::ReplicaChallenge);
+    assert_eq!(challenge.header().command, Command::ReplicaChallenge);
     assert_eq!(
         auth::read_status(&challenge.header().reserved_command),
         HandshakeStatus::Ok,
     );
 
     // Wrong command in the finish slot (Prepare instead of ReplicaFinish).
-    framing::write_message(&mut stream, build_raw(CLUSTER, 0, Command2::Prepare))
+    framing::write_message(&mut stream, build_raw(CLUSTER, 0, Command::Prepare))
         .await
         .expect("write wrong finish");
 
@@ -303,7 +303,7 @@
     nonce: Option<&[u8; auth::NONCE_LEN]>,
 ) -> Message<GenericHeader> {
     Message::<GenericHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut GenericHeader| {
-        h.command = Command2::ReplicaHello;
+        h.command = Command::ReplicaHello;
         h.cluster = cluster_id;
         h.replica = replica_id;
         h.size = HEADER_SIZE as u32;
@@ -316,7 +316,7 @@
 /// Build a raw frame with an arbitrary command for the wire-level tests (used to
 /// send a wrong-command third frame in place of a `ReplicaFinish`).
 #[allow(clippy::cast_possible_truncation)]
-fn build_raw(cluster_id: u128, replica_id: u8, command: Command2) -> Message<GenericHeader> {
+fn build_raw(cluster_id: u128, replica_id: u8, command: Command) -> Message<GenericHeader> {
     Message::<GenericHeader>::new(HEADER_SIZE).transmute_header(|_, h: &mut GenericHeader| {
         h.command = command;
         h.cluster = cluster_id;
diff --git a/core/message_bus/tests/replica_roundtrip.rs b/core/message_bus/tests/replica_roundtrip.rs
index e16fbf3..0b2ed65 100644
--- a/core/message_bus/tests/replica_roundtrip.rs
+++ b/core/message_bus/tests/replica_roundtrip.rs
@@ -27,7 +27,7 @@
     header_only, install_dialed_replicas_locally, install_replicas_locally, loopback,
     set_replica_ctx,
 };
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
 use message_bus::replica::listener::{MessageHandler, bind, run};
 use message_bus::{IggyMessageBus, MessageBus};
@@ -78,24 +78,24 @@
     wait_until(|| bus0.replicas().contains(1), Duration::from_secs(2)).await;
 
     // Replica 0 sends a Prepare to replica 1.
-    let prepare = header_only(Command2::Prepare, CLUSTER, 0);
+    let prepare = header_only(Command::Prepare, CLUSTER, 0);
     bus0.send_to_replica(1, prepare.into_frozen())
         .await
         .expect("send prepare");
 
     // Replica 1 should observe the Prepare via its on_message callback.
     let cmd = expect_recv(&rx1, Duration::from_secs(2)).await;
-    assert_eq!(cmd, Command2::Prepare as u8);
+    assert_eq!(cmd, Command::Prepare as u8);
 
     // Replica 1 responds with PrepareOk.
-    let ack = header_only(Command2::PrepareOk, CLUSTER, 1);
+    let ack = header_only(Command::PrepareOk, CLUSTER, 1);
     bus1.send_to_replica(0, ack.into_frozen())
         .await
         .expect("send ack");
 
     // Replica 0 should observe the PrepareOk on its outbound read loop.
     let cmd = expect_recv(&rx0, Duration::from_secs(2)).await;
-    assert_eq!(cmd, Command2::PrepareOk as u8);
+    assert_eq!(cmd, Command::PrepareOk as u8);
 
     bus0.shutdown(Duration::from_secs(2)).await;
     bus1.shutdown(Duration::from_secs(2)).await;
diff --git a/core/message_bus/tests/replica_tls_bench.rs b/core/message_bus/tests/replica_tls_bench.rs
index 059699f..f3f9f30 100644
--- a/core/message_bus/tests/replica_tls_bench.rs
+++ b/core/message_bus/tests/replica_tls_bench.rs
@@ -37,7 +37,7 @@
     install_dialed_replicas_locally, install_replicas_locally, loopback,
     self_signed_replica_tls_ctx, set_replica_ctx, set_replica_ctx_with_tls,
 };
-use iggy_binary_protocol::{Command2, GenericHeader};
+use iggy_binary_protocol::{Command, GenericHeader};
 use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
 use message_bus::replica::listener::{MessageHandler, bind, run};
 use message_bus::{IggyMessageBus, MessageBus, SendError};
@@ -121,7 +121,7 @@
         loop {
             let frame = Message::<GenericHeader>::new(FRAME_SIZE)
                 .transmute_header(|_, h: &mut GenericHeader| {
-                    h.command = Command2::Prepare;
+                    h.command = Command::Prepare;
                     h.cluster = CLUSTER;
                     h.size = FRAME_SIZE as u32;
                 })
diff --git a/core/message_bus/tests/replica_tls_roundtrip.rs b/core/message_bus/tests/replica_tls_roundtrip.rs
index e3f1bee..8cf1c0b 100644
--- a/core/message_bus/tests/replica_tls_roundtrip.rs
+++ b/core/message_bus/tests/replica_tls_roundtrip.rs
@@ -28,7 +28,7 @@
     header_only, install_dialed_replicas_locally, install_replicas_locally, loopback,
     self_signed_replica_tls_ctx, set_replica_ctx, set_replica_ctx_with_tls,
 };
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
 use message_bus::replica::auth::ReplicaAuth;
 use message_bus::replica::listener::{MessageHandler, bind, run};
@@ -95,21 +95,21 @@
 
     wait_until(|| bus0.replicas().contains(1), Duration::from_secs(2)).await;
 
-    let prepare = header_only(Command2::Prepare, CLUSTER, 0);
+    let prepare = header_only(Command::Prepare, CLUSTER, 0);
     bus0.send_to_replica(1, prepare.into_frozen())
         .await
         .expect("send prepare");
 
     let cmd = expect_recv(&rx1, Duration::from_secs(2)).await;
-    assert_eq!(cmd, Command2::Prepare as u8);
+    assert_eq!(cmd, Command::Prepare as u8);
 
-    let ack = header_only(Command2::PrepareOk, CLUSTER, 1);
+    let ack = header_only(Command::PrepareOk, CLUSTER, 1);
     bus1.send_to_replica(0, ack.into_frozen())
         .await
         .expect("send ack");
 
     let cmd = expect_recv(&rx0, Duration::from_secs(2)).await;
-    assert_eq!(cmd, Command2::PrepareOk as u8);
+    assert_eq!(cmd, Command::PrepareOk as u8);
 
     bus0.shutdown(Duration::from_secs(2)).await;
     bus1.shutdown(Duration::from_secs(2)).await;
diff --git a/core/message_bus/tests/tcp_client_roundtrip.rs b/core/message_bus/tests/tcp_client_roundtrip.rs
index 6c8cd59..9925026 100644
--- a/core/message_bus/tests/tcp_client_roundtrip.rs
+++ b/core/message_bus/tests/tcp_client_roundtrip.rs
@@ -23,7 +23,7 @@
 
 use common::{header_only, install_clients_locally, loopback};
 use compio::net::TcpStream;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use message_bus::client_listener::RequestHandler;
 use message_bus::client_listener::tcp::{bind, run};
 use message_bus::framing;
@@ -38,10 +38,10 @@
     // Handler echoes a Reply back via send_to_client.
     let bus_for_handler = bus.clone();
     let on_request: RequestHandler = Rc::new(move |client_id, msg| {
-        assert_eq!(msg.header().command, Command2::Request);
+        assert_eq!(msg.header().command, Command::Request);
         let bus = bus_for_handler.clone();
         compio::runtime::spawn(async move {
-            let reply = header_only(Command2::Reply, 42, 0);
+            let reply = header_only(Command::Reply, 42, 0);
             bus.send_to_client(client_id, reply.into_frozen())
                 .await
                 .expect("send_to_client should succeed");
@@ -60,7 +60,7 @@
     // Dial as a raw TCP client.
     let mut client = TcpStream::connect(addr).await.expect("connect");
 
-    let request = header_only(Command2::Request, 42, 0);
+    let request = header_only(Command::Request, 42, 0);
     framing::write_message(&mut client, request)
         .await
         .expect("client write");
@@ -68,7 +68,7 @@
     let reply = framing::read_message(&mut client, framing::MAX_MESSAGE_SIZE)
         .await
         .expect("client read");
-    assert_eq!(reply.header().command, Command2::Reply);
+    assert_eq!(reply.header().command, Command::Reply);
     assert_eq!(reply.header().cluster, 42);
 
     let outcome = bus.shutdown(Duration::from_secs(2)).await;
@@ -97,7 +97,7 @@
     bus.track_background(accept_handle);
 
     let mut client = TcpStream::connect(addr).await.unwrap();
-    let bogus = header_only(Command2::Ping, 0, 0);
+    let bogus = header_only(Command::Ping, 0, 0);
     framing::write_message(&mut client, bogus).await.unwrap();
 
     // Give the read loop a chance to observe + ignore it.
diff --git a/core/message_bus/tests/tcp_tls_client_listener.rs b/core/message_bus/tests/tcp_tls_client_listener.rs
index a3ba661..0ce3ed0 100644
--- a/core/message_bus/tests/tcp_tls_client_listener.rs
+++ b/core/message_bus/tests/tcp_tls_client_listener.rs
@@ -20,7 +20,7 @@
 use async_channel::bounded;
 use common::{header_only, install_tls_clients_locally, loopback};
 use compio::net::TcpStream;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use iggy_binary_protocol::GenericHeader;
 use message_bus::client_listener::RequestHandler;
 use message_bus::client_listener::tcp_tls::{bind, run};
@@ -46,10 +46,10 @@
     // take. Spawned because the handler signature is synchronous.
     let bus_for_handler = Rc::clone(&bus);
     let on_request: RequestHandler = Rc::new(move |client_id, msg| {
-        assert_eq!(msg.header().command, Command2::Request);
+        assert_eq!(msg.header().command, Command::Request);
         let bus = Rc::clone(&bus_for_handler);
         compio::runtime::spawn(async move {
-            let reply = header_only(Command2::Reply, 42, 0).into_frozen();
+            let reply = header_only(Command::Reply, 42, 0).into_frozen();
             bus.send_to_client(client_id, reply)
                 .await
                 .expect("server send_to_client");
@@ -98,14 +98,14 @@
     };
     let client_handle = compio::runtime::spawn(async move { conn.run(ctx).await });
 
-    let request = header_only(Command2::Request, 42, 0).into_frozen();
+    let request = header_only(Command::Request, 42, 0).into_frozen();
     out_tx.send(request).await.expect("client send");
 
     let reply = compio::time::timeout(Duration::from_secs(5), in_rx.recv())
         .await
         .expect("client must receive reply within 5 s")
         .expect("reply frame");
-    assert_eq!(reply.header().command, Command2::Reply);
+    assert_eq!(reply.header().command, Command::Reply);
     assert_eq!(reply.header().cluster, 42);
 
     client_shutdown.trigger();
diff --git a/core/message_bus/tests/tcp_tls_client_roundtrip.rs b/core/message_bus/tests/tcp_tls_client_roundtrip.rs
index 2ecb840..f4d55f5 100644
--- a/core/message_bus/tests/tcp_tls_client_roundtrip.rs
+++ b/core/message_bus/tests/tcp_tls_client_roundtrip.rs
@@ -23,7 +23,7 @@
     install_replicas_locally, install_tls_clients_locally, loopback, set_replica_ctx,
 };
 use compio::net::TcpStream;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use iggy_binary_protocol::GenericHeader;
 use message_bus::client_listener::RequestHandler;
 use message_bus::connector::DEFAULT_RECONNECT_PERIOD;
@@ -51,10 +51,10 @@
     let on_message: MessageHandler = Rc::new(|_, _| {});
     let bus_for_handler = Rc::clone(&bus);
     let on_request: RequestHandler = Rc::new(move |client_id, msg| {
-        assert_eq!(msg.header().command, Command2::Request);
+        assert_eq!(msg.header().command, Command::Request);
         let bus = Rc::clone(&bus_for_handler);
         compio::runtime::spawn(async move {
-            let reply = header_only(Command2::Reply, CLUSTER, 0).into_frozen();
+            let reply = header_only(Command::Reply, CLUSTER, 0).into_frozen();
             bus.send_to_client(client_id, reply)
                 .await
                 .expect("server send_to_client");
@@ -131,14 +131,14 @@
     };
     let client_handle = compio::runtime::spawn(async move { conn.run(ctx).await });
 
-    let request = header_only(Command2::Request, CLUSTER, 0).into_frozen();
+    let request = header_only(Command::Request, CLUSTER, 0).into_frozen();
     out_tx.send(request).await.expect("client send");
 
     let reply = compio::time::timeout(Duration::from_secs(5), in_rx.recv())
         .await
         .expect("client must receive reply within 5 s")
         .expect("reply frame");
-    assert_eq!(reply.header().command, Command2::Reply);
+    assert_eq!(reply.header().command, Command::Reply);
     assert_eq!(reply.header().cluster, CLUSTER);
 
     client_shutdown.trigger();
diff --git a/core/message_bus/tests/vectored_batch.rs b/core/message_bus/tests/vectored_batch.rs
index 6f6adb6..fc692c3 100644
--- a/core/message_bus/tests/vectored_batch.rs
+++ b/core/message_bus/tests/vectored_batch.rs
@@ -24,7 +24,7 @@
     header_only, install_dialed_replicas_locally, install_replicas_locally, loopback,
     set_replica_ctx,
 };
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use message_bus::connector::{DEFAULT_RECONNECT_PERIOD, start as start_connector};
 use message_bus::replica::listener::{MessageHandler, bind, run};
 use message_bus::{IggyMessageBus, MessageBus};
@@ -73,7 +73,7 @@
     // Pipeline N sends back-to-back without yielding. The writer task
     // should drain many of them in single writev calls.
     for i in 0..N {
-        let msg = header_only(Command2::Prepare, i as u128, 0);
+        let msg = header_only(Command::Prepare, i as u128, 0);
         bus0.send_to_replica(1, msg.into_frozen())
             .await
             .expect("send should succeed - queue capacity is 256");
diff --git a/core/message_bus/tests/ws_client_roundtrip.rs b/core/message_bus/tests/ws_client_roundtrip.rs
index a66f5a6..3f10ff4 100644
--- a/core/message_bus/tests/ws_client_roundtrip.rs
+++ b/core/message_bus/tests/ws_client_roundtrip.rs
@@ -35,7 +35,7 @@
 use compio::net::TcpStream;
 use compio_ws::WebSocketStream;
 use compio_ws::tungstenite::Message as WsMessage;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use iggy_binary_protocol::GenericHeader;
 use message_bus::client_listener::RequestHandler;
 use message_bus::client_listener::ws::{bind, run};
@@ -77,10 +77,10 @@
     // completes within the same runtime tick.
     let bus_for_handler = Rc::clone(&bus);
     let on_request: RequestHandler = Rc::new(move |client_id, msg| {
-        assert_eq!(msg.header().command, Command2::Request);
+        assert_eq!(msg.header().command, Command::Request);
         let bus = Rc::clone(&bus_for_handler);
         compio::runtime::spawn(async move {
-            let reply = header_only(Command2::Reply, 42, 0).into_frozen();
+            let reply = header_only(Command::Reply, 42, 0).into_frozen();
             bus.send_to_client(client_id, reply)
                 .await
                 .expect("server send_to_client");
@@ -105,7 +105,7 @@
         .await
         .expect("ws handshake");
 
-    let request = header_only(Command2::Request, 42, 0).into_frozen();
+    let request = header_only(Command::Request, 42, 0).into_frozen();
     ws_client
         .send(WsMessage::Binary(Bytes::from_owner(request)))
         .await
@@ -114,7 +114,7 @@
     let reply = compio::time::timeout(Duration::from_secs(2), raw_recv(&mut ws_client))
         .await
         .expect("client must receive reply within 2 s");
-    assert_eq!(reply.header().command, Command2::Reply);
+    assert_eq!(reply.header().command, Command::Reply);
 
     bus.shutdown(Duration::from_secs(2)).await;
 }
diff --git a/core/message_bus/tests/wss_client_listener.rs b/core/message_bus/tests/wss_client_listener.rs
index 4136379..56566a6 100644
--- a/core/message_bus/tests/wss_client_listener.rs
+++ b/core/message_bus/tests/wss_client_listener.rs
@@ -20,7 +20,7 @@
 use async_channel::bounded;
 use common::{header_only, install_wss_clients_locally, loopback};
 use compio::net::TcpStream;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use iggy_binary_protocol::GenericHeader;
 use message_bus::client_listener::RequestHandler;
 use message_bus::client_listener::wss::{bind, run};
@@ -43,10 +43,10 @@
 
     let bus_for_handler = Rc::clone(&bus);
     let on_request: RequestHandler = Rc::new(move |client_id, msg| {
-        assert_eq!(msg.header().command, Command2::Request);
+        assert_eq!(msg.header().command, Command::Request);
         let bus = Rc::clone(&bus_for_handler);
         compio::runtime::spawn(async move {
-            let reply = header_only(Command2::Reply, 7, 0).into_frozen();
+            let reply = header_only(Command::Reply, 7, 0).into_frozen();
             bus.send_to_client(client_id, reply)
                 .await
                 .expect("server send_to_client");
@@ -93,14 +93,14 @@
     };
     let client_handle = compio::runtime::spawn(async move { conn.run(ctx).await });
 
-    let request = header_only(Command2::Request, 7, 0).into_frozen();
+    let request = header_only(Command::Request, 7, 0).into_frozen();
     out_tx.send(request).await.expect("client send");
 
     let reply = compio::time::timeout(Duration::from_secs(5), in_rx.recv())
         .await
         .expect("client must receive reply within 5 s")
         .expect("reply frame");
-    assert_eq!(reply.header().command, Command2::Reply);
+    assert_eq!(reply.header().command, Command::Reply);
     assert_eq!(reply.header().cluster, 7);
 
     client_shutdown.trigger();
diff --git a/core/message_bus/tests/wss_client_roundtrip.rs b/core/message_bus/tests/wss_client_roundtrip.rs
index 971df4c..24cb272 100644
--- a/core/message_bus/tests/wss_client_roundtrip.rs
+++ b/core/message_bus/tests/wss_client_roundtrip.rs
@@ -23,7 +23,7 @@
     install_replicas_locally, install_wss_clients_locally, loopback, set_replica_ctx,
 };
 use compio::net::TcpStream;
-use iggy_binary_protocol::Command2;
+use iggy_binary_protocol::Command;
 use iggy_binary_protocol::GenericHeader;
 use message_bus::client_listener::RequestHandler;
 use message_bus::connector::DEFAULT_RECONNECT_PERIOD;
@@ -51,10 +51,10 @@
     let on_message: MessageHandler = Rc::new(|_, _| {});
     let bus_for_handler = Rc::clone(&bus);
     let on_request: RequestHandler = Rc::new(move |client_id, msg| {
-        assert_eq!(msg.header().command, Command2::Request);
+        assert_eq!(msg.header().command, Command::Request);
         let bus = Rc::clone(&bus_for_handler);
         compio::runtime::spawn(async move {
-            let reply = header_only(Command2::Reply, CLUSTER, 0).into_frozen();
+            let reply = header_only(Command::Reply, CLUSTER, 0).into_frozen();
             bus.send_to_client(client_id, reply)
                 .await
                 .expect("server send_to_client");
@@ -132,14 +132,14 @@
     };
     let client_handle = compio::runtime::spawn(async move { conn.run(ctx).await });
 
-    let request = header_only(Command2::Request, CLUSTER, 0).into_frozen();
+    let request = header_only(Command::Request, CLUSTER, 0).into_frozen();
     out_tx.send(request).await.expect("client send");
 
     let reply = compio::time::timeout(Duration::from_secs(5), in_rx.recv())
         .await
         .expect("client must receive reply within 5 s")
         .expect("reply frame");
-    assert_eq!(reply.header().command, Command2::Reply);
+    assert_eq!(reply.header().command, Command::Reply);
     assert_eq!(reply.header().cluster, CLUSTER);
 
     client_shutdown.trigger();
diff --git a/core/metadata/src/impls/metadata.rs b/core/metadata/src/impls/metadata.rs
index 2bb877f..acd3574 100644
--- a/core/metadata/src/impls/metadata.rs
+++ b/core/metadata/src/impls/metadata.rs
@@ -43,7 +43,7 @@
 use iggy_binary_protocol::requests::topics::CreateTopicRequest as WireCreateTopicRequest;
 use iggy_binary_protocol::requests::topics::CreateTopicWithAssignmentsRequest as PersistedCreateTopicRequest;
 use iggy_binary_protocol::{
-    Command2, ConsensusHeader, EvictionReason, GenericHeader, Operation, PrepareHeader,
+    Command, ConsensusHeader, EvictionReason, GenericHeader, Operation, PrepareHeader,
     PrepareOkHeader, ProtocolVersion, ReplyHeader, RoutedRequestHeader, WireDecode, WireEncode,
     WireName,
 };
@@ -1326,7 +1326,7 @@
     {
         assert!(matches!(
             message.header().command(),
-            Command2::Request | Command2::Prepare | Command2::PrepareOk
+            Command::Request | Command::Prepare | Command::PrepareOk
         ));
         message.header().operation().is_metadata_plane()
     }
@@ -2508,7 +2508,7 @@
         consensus.drain_loopback_into(&mut loopback);
         for message in loopback {
             match message.header().command {
-                Command2::PrepareOk => match message.try_into_typed::<PrepareOkHeader>() {
+                Command::PrepareOk => match message.try_into_typed::<PrepareOkHeader>() {
                     Ok(prepare_ok) => self.on_ack(prepare_ok).await,
                     Err(error) => warn!(
                         error = %error,
@@ -2614,7 +2614,7 @@
         }
         for message in loopback.drain(..) {
             match message.header().command {
-                Command2::PrepareOk => match message.try_into_typed::<PrepareOkHeader>() {
+                Command::PrepareOk => match message.try_into_typed::<PrepareOkHeader>() {
                     Ok(prepare_ok) => self.on_ack(prepare_ok).await,
                     Err(error) => warn!(
                         error = %error,
@@ -3424,7 +3424,7 @@
         // TODO: calculate the index;
         #[allow(clippy::cast_possible_truncation)]
         let idx = header.op as usize;
-        assert_eq!(header.command, Command2::Prepare);
+        assert_eq!(header.command, Command::Prepare);
         assert!(
             journal.handle().header(idx).is_some(),
             "replicate: prepare must be durable in local journal before chain-forward"
@@ -3579,7 +3579,7 @@
     )
     .expect("zeroed bytes are a valid RoutedRequestHeader");
     *header = RoutedRequestHeader {
-        command: Command2::Request,
+        command: Command::Request,
         operation: Operation::Register,
         size: u32::try_from(header_size).expect("RoutedRequestHeader size fits u32"),
         cluster: consensus.cluster(),
@@ -3617,7 +3617,7 @@
     )
     .expect("zeroed bytes are a valid RoutedRequestHeader");
     *header = RoutedRequestHeader {
-        command: Command2::Request,
+        command: Command::Request,
         operation: Operation::Logout,
         size: u32::try_from(header_size).expect("RoutedRequestHeader size fits u32"),
         cluster: consensus.cluster(),
@@ -3653,7 +3653,7 @@
             bytemuck::checked::try_from_bytes_mut::<RoutedRequestHeader>(&mut slice[..header_size])
                 .expect("zeroed bytes are a valid RoutedRequestHeader");
         *header = RoutedRequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             operation: Operation::CompleteConsumerGroupRevocation,
             size: u32::try_from(total).expect("request size fits u32"),
             cluster: consensus.cluster(),
@@ -3744,7 +3744,7 @@
             bytemuck::checked::try_from_bytes_mut::<RoutedRequestHeader>(&mut slice[..header_size])
                 .expect("zeroed bytes are a valid RoutedRequestHeader");
         *header = RoutedRequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             operation: Operation::TruncatePartition,
             size: u32::try_from(total).expect("request size fits u32"),
             cluster: template.cluster,
@@ -3797,7 +3797,7 @@
         size: u32::try_from(size).expect("prepare message size exceeds u32"),
         view: consensus.view(),
         release: request.release,
-        command: Command2::Prepare,
+        command: Command::Prepare,
         replica: consensus.replica(),
         client: request.client,
         parent: consensus.last_prepare_checksum(),
@@ -4165,7 +4165,7 @@
             client,
             request: 0,
             commit: session,
-            command: Command2::Reply,
+            command: Command::Reply,
             operation: Operation::Register,
             ..Default::default()
         };
@@ -4344,7 +4344,7 @@
             let header =
                 bytemuck::checked::from_bytes_mut::<RoutedRequestHeader>(&mut slice[..header_size]);
             *header = RoutedRequestHeader {
-                command: Command2::Request,
+                command: Command::Request,
                 operation: Operation::CreateTopic,
                 size: u32::try_from(total).unwrap(),
                 client,
@@ -4578,7 +4578,7 @@
                 request,
                 commit: request,
                 size: u32::try_from(total).unwrap(),
-                command: Command2::Reply,
+                command: Command::Reply,
                 operation,
                 ..Default::default()
             };
@@ -4593,7 +4593,7 @@
             &mut message.as_mut_slice()[..header_size],
         );
         *header = RoutedRequestHeader {
-            command: Command2::Request,
+            command: Command::Request,
             operation: Operation::CreatePersonalAccessToken,
             size: u32::try_from(header_size).unwrap(),
             client,
@@ -4624,7 +4624,7 @@
             let header =
                 bytemuck::checked::from_bytes_mut::<RoutedRequestHeader>(&mut slice[..header_size]);
             *header = RoutedRequestHeader {
-                command: Command2::Request,
+                command: Command::Request,
                 operation: Operation::CreateStream,
                 size: u32::try_from(total).unwrap(),
                 client,
diff --git a/core/metadata/src/impls/recovery.rs b/core/metadata/src/impls/recovery.rs
index 624f933..f192d38 100644
--- a/core/metadata/src/impls/recovery.rs
+++ b/core/metadata/src/impls/recovery.rs
@@ -796,7 +796,7 @@
     use crate::impls::metadata::checkpoint_checksum;
     use crate::stm::snapshot::SNAPSHOT_FORMAT_VERSION;
     use consensus::CLIENTS_TABLE_MAX;
-    use iggy_binary_protocol::consensus::{Command2, Operation};
+    use iggy_binary_protocol::consensus::{Command, Operation};
     use journal::Journal;
     use server_common::iobuf::Owned;
     use tempfile::tempdir;
@@ -910,7 +910,7 @@
             &mut buffer.as_mut_slice()[..HEADER_SIZE],
         );
         header.size = total_size as u32;
-        header.command = Command2::Prepare;
+        header.command = Command::Prepare;
         header.op = op;
         header.commit = commit;
         header.operation = Operation::CreateStream;
@@ -936,7 +936,7 @@
             &mut buffer.as_mut_slice()[..HEADER_SIZE],
         );
         header.size = total_size as u32;
-        header.command = Command2::Prepare;
+        header.command = Command::Prepare;
         header.op = op;
         header.commit = op.saturating_sub(1);
         header.operation = operation;
diff --git a/core/metadata/src/stm/authz.rs b/core/metadata/src/stm/authz.rs
index 4f235ff..de33414 100644
--- a/core/metadata/src/stm/authz.rs
+++ b/core/metadata/src/stm/authz.rs
@@ -341,9 +341,7 @@
         | Operation::DeletePersonalAccessToken
         | Operation::SendMessages
         | Operation::StoreConsumerOffset
-        | Operation::DeleteConsumerOffset
-        | Operation::StoreConsumerOffset2
-        | Operation::DeleteConsumerOffset2 => None,
+        | Operation::DeleteConsumerOffset => None,
     }
 }
 
@@ -409,7 +407,7 @@
     use iggy_binary_protocol::requests::streams::CreateStreamRequest;
     use iggy_binary_protocol::requests::topics::CreateTopicRequest;
     use iggy_binary_protocol::requests::users::CreateUserRequest;
-    use iggy_binary_protocol::{Command2, WireEncode, WireName, WireOptions};
+    use iggy_binary_protocol::{Command, WireEncode, WireName, WireOptions};
     use iggy_common::UserStatus;
     use server_common::iobuf::Owned;
 
@@ -438,7 +436,7 @@
             let header = bytemuck::checked::from_bytes_mut::<PrepareHeader>(
                 &mut buffer.as_mut_slice()[..HEADER_SIZE],
             );
-            header.command = Command2::Prepare;
+            header.command = Command::Prepare;
             header.operation = operation;
             header.op = op;
             header.user_id = user_id;
diff --git a/core/metadata/src/stm/stream.rs b/core/metadata/src/stm/stream.rs
index f7ba28f..eefe9e4 100644
--- a/core/metadata/src/stm/stream.rs
+++ b/core/metadata/src/stm/stream.rs
@@ -3270,7 +3270,7 @@
         name: &str,
         op: u64,
     ) -> server_common::Message<iggy_binary_protocol::PrepareHeader> {
-        use iggy_binary_protocol::{Command2, Operation, PrepareHeader};
+        use iggy_binary_protocol::{Command, Operation, PrepareHeader};
         use server_common::Message;
         use server_common::iobuf::Owned;
         use std::mem::size_of;
@@ -3287,7 +3287,7 @@
             let header = bytemuck::checked::from_bytes_mut::<PrepareHeader>(
                 &mut buffer.as_mut_slice()[..header_size],
             );
-            header.command = Command2::Prepare;
+            header.command = Command::Prepare;
             header.operation = Operation::CreateStream;
             header.op = op;
             header.size = u32::try_from(total).unwrap();
diff --git a/core/partitions/src/iggy_partition.rs b/core/partitions/src/iggy_partition.rs
index 2aec169..76267af 100644
--- a/core/partitions/src/iggy_partition.rs
+++ b/core/partitions/src/iggy_partition.rs
@@ -45,8 +45,7 @@
     send_prepare_ok as send_prepare_ok_common, verify_prepare_integrity,
 };
 use iggy_binary_protocol::requests::consumer_offsets::{
-    DeleteConsumerOffset2Request, DeleteConsumerOffsetRequest, StoreConsumerOffset2Request,
-    StoreConsumerOffsetRequest,
+    DeleteConsumerOffsetRequest, StoreConsumerOffsetRequest,
 };
 use iggy_binary_protocol::responses::messages::{
     SendMessagesConfirmationResponse, SendMessagesResponse,
@@ -70,8 +69,8 @@
 use server_common::{
     MESSAGE_ALIGN, Message, SegmentStorage,
     iobuf::{Frozen, Owned},
-    send_messages2::{
-        ChecksumMode, SendMessages2Header, convert_request_message, decode_prepare_slice,
+    send_messages::{
+        BatchHeader, ChecksumMode, convert_request_message, decode_prepare_slice,
         decode_prepare_slice_trusted, stamp_prepare_for_persistence,
     },
     sharding::IggyNamespace,
@@ -1268,7 +1267,7 @@
             // primary is real divergence (log corruption / out-of-order apply)
             // and must surface rather than silently mask a split state. A
             // FOLLOWER may legitimately miss the offset: `AckLevel::NoAck`
-            // (v2) stores apply on the primary only and are never replicated,
+            // stores apply on the primary only and are never replicated,
             // so a later quorum delete finds nothing on the backups -- erroring
             // there would fail the committed apply, panic the replica as
             // divergent, and crash-loop on every journal replay. The
@@ -1894,9 +1893,9 @@
                 // Skip the batch-checksum pass: on the partition ingest path
                 // nothing reads it before `stamp_prepare_for_persistence`
                 // recomputes it over the stamped header. An already-canonical
-                // batch (native v2, or the plane's pre-encrypt convert output)
-                // returns early above, so Skip only affects the legacy
-                // transcode, whose output goes straight to project/stamp.
+                // batch (the plane's pre-encrypt convert output) returns early
+                // inside the convert, so Skip only affects the wire-form
+                // admission, whose output goes straight to project/stamp.
                 match convert_request_message(namespace, message, ChecksumMode::Skip) {
                     Ok(message) => message,
                     Err(error) => {
@@ -1918,10 +1917,7 @@
 
             // Parse once for both the delete-existence check and AckLevel dispatch.
             let consumer_offset = match message.header().operation {
-                Operation::StoreConsumerOffset
-                | Operation::StoreConsumerOffset2
-                | Operation::DeleteConsumerOffset
-                | Operation::DeleteConsumerOffset2 => {
+                Operation::StoreConsumerOffset | Operation::DeleteConsumerOffset => {
                     match Self::parse_consumer_offset_request(message.header().operation, &message)
                     {
                         Ok(parsed) => Some(parsed),
@@ -1945,10 +1941,8 @@
                 _ => None,
             };
 
-            if matches!(
-                message.header().operation,
-                Operation::DeleteConsumerOffset | Operation::DeleteConsumerOffset2
-            ) && let Some((kind, consumer_id, _, _)) = consumer_offset
+            if matches!(message.header().operation, Operation::DeleteConsumerOffset)
+                && let Some((kind, consumer_id, _, _)) = consumer_offset
                 && let Err(error) = self.ensure_consumer_offset_exists(kind, consumer_id)
             {
                 emit_partition_diag(
@@ -1984,10 +1978,8 @@
             // `InvalidOffset` rides `ReplyHeader.status` (op=0, empty body): the
             // status-only `classify_partition_reply` would misread a result-body
             // code on this committed-shaped frame (op=commit_max) as success.
-            if matches!(
-                message.header().operation,
-                Operation::StoreConsumerOffset | Operation::StoreConsumerOffset2
-            ) && let Some((_, _, Some(requested_offset), _)) = consumer_offset
+            if matches!(message.header().operation, Operation::StoreConsumerOffset)
+                && let Some((_, _, Some(requested_offset), _)) = consumer_offset
             {
                 let current_offset = self.stats.current_offset();
                 let partition_empty =
@@ -2039,11 +2031,11 @@
                 return;
             }
 
-            // NoAck v2 -> fast path. Quorum + v1 -> VSR pipeline.
+            // NoAck -> fast path. Quorum -> VSR pipeline.
             if let Some((kind, consumer_id, offset, AckLevel::NoAck)) = consumer_offset
                 && matches!(
                     message.header().operation,
-                    Operation::StoreConsumerOffset2 | Operation::DeleteConsumerOffset2,
+                    Operation::StoreConsumerOffset | Operation::DeleteConsumerOffset,
                 )
             {
                 Disposition::NoAck {
@@ -2546,10 +2538,7 @@
                 );
                 Ok(frozen)
             }
-            Operation::StoreConsumerOffset
-            | Operation::DeleteConsumerOffset
-            | Operation::StoreConsumerOffset2
-            | Operation::DeleteConsumerOffset2 => {
+            Operation::StoreConsumerOffset | Operation::DeleteConsumerOffset => {
                 // Replicated path is Quorum-only by construction; ack ignored.
                 let (kind, consumer_id, offset, _ack) =
                     Self::parse_staged_consumer_offset_commit(header.operation, &message)?;
@@ -2574,7 +2563,7 @@
                     .map_err(|_| IggyError::CannotAppendMessage)?;
 
                 match header.operation {
-                    Operation::StoreConsumerOffset | Operation::StoreConsumerOffset2 => {
+                    Operation::StoreConsumerOffset => {
                         self.stage_consumer_offset_upsert(
                             header.op,
                             kind,
@@ -2583,7 +2572,7 @@
                             is_auto_commit_client(header.client),
                         );
                     }
-                    Operation::DeleteConsumerOffset | Operation::DeleteConsumerOffset2 => {
+                    Operation::DeleteConsumerOffset => {
                         self.stage_consumer_offset_delete(header.op, kind, consumer_id);
                     }
                     _ => unreachable!(),
@@ -2663,7 +2652,7 @@
     async fn append_stamped_messages(
         &mut self,
         message: Message<PrepareHeader>,
-        batch: SendMessages2Header,
+        batch: BatchHeader,
     ) -> Result<JournaledMessages, IggyError> {
         let batch_messages_count = batch.message_count;
         if batch_messages_count == 0 {
@@ -3367,10 +3356,7 @@
                 }
                 !*failed_commit
             }
-            Operation::StoreConsumerOffset
-            | Operation::DeleteConsumerOffset
-            | Operation::StoreConsumerOffset2
-            | Operation::DeleteConsumerOffset2 => {
+            Operation::StoreConsumerOffset | Operation::DeleteConsumerOffset => {
                 self.commit_consumer_offset_entry(prepare_header, failed_commit)
                     .await
             }
@@ -3499,7 +3485,7 @@
         let (kind, consumer_id, offset, _ack) =
             Self::parse_staged_consumer_offset_commit(header.operation, &message)?;
         match header.operation {
-            Operation::StoreConsumerOffset | Operation::StoreConsumerOffset2 => {
+            Operation::StoreConsumerOffset => {
                 let offset = offset.ok_or(IggyError::InvalidCommand)?;
                 Ok(if is_auto_commit_client(header.client) {
                     PendingConsumerOffsetCommit::upsert_auto_commit(kind, consumer_id, offset)
@@ -3507,7 +3493,7 @@
                     PendingConsumerOffsetCommit::upsert(kind, consumer_id, offset)
                 })
             }
-            Operation::DeleteConsumerOffset | Operation::DeleteConsumerOffset2 => {
+            Operation::DeleteConsumerOffset => {
                 Ok(PendingConsumerOffsetCommit::delete(kind, consumer_id))
             }
             _ => Err(IggyError::InvalidCommand),
@@ -3540,21 +3526,11 @@
             Operation::StoreConsumerOffset => {
                 let request = StoreConsumerOffsetRequest::decode_from(body)
                     .map_err(|_| IggyError::InvalidCommand)?;
-                (request.consumer, Some(request.offset), AckLevel::Quorum)
-            }
-            Operation::StoreConsumerOffset2 => {
-                let request = StoreConsumerOffset2Request::decode_from(body)
-                    .map_err(|_| IggyError::InvalidCommand)?;
                 (request.consumer, Some(request.offset), request.ack)
             }
             Operation::DeleteConsumerOffset => {
                 let request = DeleteConsumerOffsetRequest::decode_from(body)
                     .map_err(|_| IggyError::InvalidCommand)?;
-                (request.consumer, None, AckLevel::Quorum)
-            }
-            Operation::DeleteConsumerOffset2 => {
-                let request = DeleteConsumerOffset2Request::decode_from(body)
-                    .map_err(|_| IggyError::InvalidCommand)?;
                 (request.consumer, None, request.ack)
             }
             _ => return Err(IggyError::InvalidCommand),
@@ -3663,7 +3639,6 @@
             let segment_index = self.log.segments().len() - 1;
             let segment = &mut self.log.segments_mut()[segment_index];
             segment.size = IggyByteSize::from(segment.size.as_bytes_u64() + saved_bytes as u64);
-            self.log.clear_in_flight();
             return Ok(());
         }
 
@@ -3715,7 +3690,6 @@
         let segment = &mut self.log.segments_mut()[segment_index];
         segment.size = IggyByteSize::from(segment.size.as_bytes_u64() + saved.as_bytes_u64());
 
-        self.log.clear_in_flight();
         Ok(())
     }
 
@@ -3759,17 +3733,9 @@
             },
         );
 
-        let storage = SegmentStorage::new(
-            &messages_path,
-            &index_path,
-            0,
-            0,
-            enforce_fsync,
-            enforce_fsync,
-            false,
-        )
-        .await
-        .map_err(|_| IggyError::CannotCreateSegmentLogFile(messages_path.clone()))?;
+        let storage = SegmentStorage::new(&messages_path, &index_path, 0, 0, false)
+            .await
+            .map_err(|_| IggyError::CannotCreateSegmentLogFile(messages_path.clone()))?;
         let messages_size_bytes = storage
             .messages_writer
             .as_ref()
@@ -4035,17 +4001,9 @@
         let enforce_fsync = self.effective_enforce_fsync(config);
         let preallocate_segments = self.effective_preallocate_segments(config);
         let segment = Segment::new(start_offset, segment_size);
-        let storage = SegmentStorage::new(
-            &messages_path,
-            &index_path,
-            0,
-            0,
-            enforce_fsync,
-            enforce_fsync,
-            false,
-        )
-        .await
-        .map_err(|_| IggyError::CannotCreateSegmentLogFile(messages_path.clone()))?;
+        let storage = SegmentStorage::new(&messages_path, &index_path, 0, 0, false)
+            .await
+            .map_err(|_| IggyError::CannotCreateSegmentLogFile(messages_path.clone()))?;
         let messages_size_bytes = storage
             .messages_writer
             .as_ref()
@@ -5042,11 +5000,11 @@
     use bytes::Bytes;
     use compio::io::AsyncWriteAtExt;
     use consensus::LocalPipeline;
-    use iggy_binary_protocol::{Command2, ReplyHeader, WireConsumer, WireEncode};
+    use iggy_binary_protocol::{Command, ReplyHeader, WireConsumer, WireEncode};
     use message_bus::SendError;
     use server_common::MESSAGE_ALIGN;
-    use server_common::send_messages2::{
-        COMMAND_HEADER_SIZE, IggyMessage2, IggyMessage2Header, IggyMessages2, SendMessages2Owned,
+    use server_common::send_messages::{
+        COMMAND_HEADER_SIZE, IggyMessage, IggyMessageHeader, IggyMessages, SendMessagesOwned,
     };
     use std::cell::RefCell;
     use std::rc::Rc;
@@ -5354,7 +5312,7 @@
         let size = std::mem::size_of::<PrepareHeader>();
         let prepare = Message::<PrepareHeader>::new(size).transmute_header(
             |_, header: &mut PrepareHeader| {
-                header.command = Command2::Prepare;
+                header.command = Command::Prepare;
                 header.op = 1;
                 // Current view: an older-view prepare is fenced as deposed-primary
                 // traffic and would never reach the ack send under test.
@@ -5479,7 +5437,7 @@
         request_id: u64,
         consumer_id: u32,
     ) -> Message<RoutedRequestHeader> {
-        let body = DeleteConsumerOffset2Request {
+        let body = DeleteConsumerOffsetRequest {
             consumer: WireConsumer::consumer(WireIdentifier::Numeric(consumer_id)),
             stream_id: WireIdentifier::Numeric(1),
             topic_id: WireIdentifier::Numeric(1),
@@ -5492,8 +5450,8 @@
         let mut message = Message::<RoutedRequestHeader>::new(total);
         message.as_mut_slice()[header_size..].copy_from_slice(&body);
         message.transmute_header(|_, header: &mut RoutedRequestHeader| {
-            header.command = Command2::Request;
-            header.operation = Operation::DeleteConsumerOffset2;
+            header.command = Command::Request;
+            header.operation = Operation::DeleteConsumerOffset;
             header.client = client_id;
             header.session = 1;
             header.request = request_id;
@@ -5527,7 +5485,7 @@
                 &frame.as_slice()[..std::mem::size_of::<ReplyHeader>()],
             )
             .expect("deny frame starts with a valid reply header");
-            assert_eq!(header.command, Command2::Reply);
+            assert_eq!(header.command, Command::Reply);
             assert_eq!(
                 header.status,
                 IggyError::ConsumerOffsetNotFound(0).as_code()
@@ -5784,17 +5742,17 @@
     /// stamped at `base_offset`, with a valid batch checksum so it decodes
     /// through `decode_batch_slice` and matches an `Offset` poll.
     pub(super) fn build_segment_record(namespace: IggyNamespace, base_offset: u64) -> Vec<u8> {
-        let mut batch = IggyMessages2::with_capacity(1);
-        batch.push(IggyMessage2 {
-            header: IggyMessage2Header {
+        let mut batch = IggyMessages::with_capacity(1);
+        batch.push(IggyMessage {
+            header: IggyMessageHeader {
                 payload_length: 8,
                 ..Default::default()
             },
             payload: Bytes::from_static(b"abcdefgh"),
             user_headers: None,
         });
-        let mut owned = SendMessages2Owned::from_messages(namespace, &batch)
-            .expect("build send_messages batch");
+        let mut owned =
+            SendMessagesOwned::from_messages(namespace, &batch).expect("build send_messages batch");
         owned.header.base_offset = base_offset;
         owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
 
@@ -6471,10 +6429,9 @@
         let log_path = format!("{partition_dir}/{:0>20}.log", 0u64);
         let index_path = format!("{partition_dir}/{:0>20}.index", 0u64);
         partition.log.segments_mut()[0].sealed = true;
-        partition.log.storages_mut()[0] =
-            SegmentStorage::new(&log_path, &index_path, 0, 0, false, false, false)
-                .await
-                .expect("create segment storage");
+        partition.log.storages_mut()[0] = SegmentStorage::new(&log_path, &index_path, 0, 0, false)
+            .await
+            .expect("create segment storage");
 
         let record = build_segment_record(namespace, 0);
         let record_len = record.len() as u64;
@@ -6594,7 +6551,7 @@
         let size = std::mem::size_of::<PrepareHeader>();
         let prepare = Message::<PrepareHeader>::new(size).transmute_header(
             |_, header: &mut PrepareHeader| {
-                header.command = Command2::Prepare;
+                header.command = Command::Prepare;
                 header.op = op;
                 header.operation = operation;
                 header.size = u32::try_from(size).expect("prepare header size fits in u32");
@@ -7043,7 +7000,7 @@
     #[test]
     fn given_result_framed_operation_when_committed_should_reply_empty_result_section() {
         assert_eq!(
-            &committed_reply_body(Operation::StoreConsumerOffset2)[..],
+            &committed_reply_body(Operation::StoreConsumerOffset)[..],
             &[0, 0, 0, 0]
         );
     }
@@ -7341,7 +7298,7 @@
 mod purge_floor_tests {
     use super::tests::{build_segment_record, repair_config, test_partition};
     use super::*;
-    use iggy_binary_protocol::{Command2, WireConsumer, WireEncode};
+    use iggy_binary_protocol::{Command, WireConsumer, WireEncode};
 
     /// Fresh temp dir wired as the partition dir, so `purge()` can recreate
     /// real segment files and write `purge.gen`.
@@ -7371,7 +7328,7 @@
         let mut message = Message::<PrepareHeader>::new(total);
         message.as_mut_slice()[header_size..].copy_from_slice(&record);
         let message = message.transmute_header(|_, header: &mut PrepareHeader| {
-            header.command = Command2::Prepare;
+            header.command = Command::Prepare;
             header.operation = Operation::SendMessages;
             header.op = op;
             header.timestamp = op;
@@ -7385,7 +7342,7 @@
         partition.consensus().sequencer().set_sequence(op);
     }
 
-    /// A `StoreConsumerOffset2` prepare for `op`, journaled and staged through
+    /// A `StoreConsumerOffset` prepare for `op`, journaled and staged through
     /// the replicated-apply path.
     async fn journal_store_offset(
         partition: &mut IggyPartition<IggyMessageBus>,
@@ -7393,7 +7350,7 @@
         consumer_id: u32,
         offset: u64,
     ) {
-        let body = StoreConsumerOffset2Request {
+        let body = StoreConsumerOffsetRequest {
             consumer: WireConsumer::consumer(WireIdentifier::Numeric(consumer_id)),
             stream_id: WireIdentifier::Numeric(1),
             topic_id: WireIdentifier::Numeric(1),
@@ -7407,8 +7364,8 @@
         let mut message = Message::<PrepareHeader>::new(total);
         message.as_mut_slice()[header_size..].copy_from_slice(&body);
         let message = message.transmute_header(|_, header: &mut PrepareHeader| {
-            header.command = Command2::Prepare;
-            header.operation = Operation::StoreConsumerOffset2;
+            header.command = Command::Prepare;
+            header.operation = Operation::StoreConsumerOffset;
             header.op = op;
             header.group = IggyNamespace::new(1, 1, 0).inner();
             header.size = u32::try_from(total).expect("prepare size fits u32");
@@ -7737,7 +7694,7 @@
         let mut message = Message::<PrepareHeader>::new(total);
         message.as_mut_slice()[header_size..].copy_from_slice(&record);
         let message = message.transmute_header(|_, header: &mut PrepareHeader| {
-            header.command = Command2::Prepare;
+            header.command = Command::Prepare;
             header.operation = Operation::SendMessages;
             header.op = 1;
             header.group = namespace.inner();
diff --git a/core/partitions/src/iggy_partitions.rs b/core/partitions/src/iggy_partitions.rs
index 45c07ad..c0bcead 100644
--- a/core/partitions/src/iggy_partitions.rs
+++ b/core/partitions/src/iggy_partitions.rs
@@ -23,11 +23,11 @@
 use ahash::AHashSet;
 use consensus::{Consensus, Plane, PlaneIdentity, VsrConsensus};
 use iggy_binary_protocol::{
-    Command2, ConsensusHeader, Operation, PrepareHeader, PrepareOkHeader, RoutedRequestHeader,
+    Command, ConsensusHeader, Operation, PrepareHeader, PrepareOkHeader, RoutedRequestHeader,
 };
 use journal::superblock::{PingPongSuperblock, SuperblockStore};
 use message_bus::MessageBus;
-use server_common::send_messages2::{ChecksumMode, convert_request_message, encrypt_batch_request};
+use server_common::send_messages::{ChecksumMode, convert_request_message, encrypt_batch_request};
 use server_common::sharding::{IggyNamespace, LocalIdx, ShardId};
 #[cfg(debug_assertions)]
 use std::cell::Cell;
@@ -643,7 +643,7 @@
     {
         assert!(matches!(
             message.header().command(),
-            Command2::Request | Command2::Prepare | Command2::PrepareOk
+            Command::Request | Command::Prepare | Command::PrepareOk
         ));
         message.header().operation().is_partition()
     }
@@ -659,8 +659,8 @@
     use iggy_common::{IggyByteSize, PartitionStats};
     use journal::Journal as _;
     use message_bus::IggyMessageBus;
-    use server_common::send_messages2::{
-        IggyMessage2, IggyMessage2Header, IggyMessages2, PREPARE_SPLIT_POINT, SendMessages2Owned,
+    use server_common::send_messages::{
+        IggyMessage, IggyMessageHeader, IggyMessages, PREPARE_SPLIT_POINT, SendMessagesOwned,
         stamp_prepare_for_persistence,
     };
     use server_common::{Message, iobuf::Frozen};
@@ -718,22 +718,22 @@
         op: u64,
         base_offset: u64,
     ) -> Frozen<4096> {
-        let mut batch = IggyMessages2::with_capacity(1);
-        batch.push(IggyMessage2 {
-            header: IggyMessage2Header {
+        let mut batch = IggyMessages::with_capacity(1);
+        batch.push(IggyMessage {
+            header: IggyMessageHeader {
                 payload_length: 8,
                 ..Default::default()
             },
             payload: Bytes::from_static(b"abcdefgh"),
             user_headers: None,
         });
-        let owned = SendMessages2Owned::from_messages(namespace, &batch)
-            .expect("build send_messages batch");
+        let owned =
+            SendMessagesOwned::from_messages(namespace, &batch).expect("build send_messages batch");
 
         let total_size = PREPARE_SPLIT_POINT + owned.blob.len();
         let prepare = Message::<PrepareHeader>::new(total_size).transmute_header(
             |_, header: &mut PrepareHeader| {
-                header.command = Command2::Prepare;
+                header.command = Command::Prepare;
                 header.operation = Operation::SendMessages;
                 header.op = op;
                 header.size = u32::try_from(total_size).expect("size fits u32");
diff --git a/core/partitions/src/journal.rs b/core/partitions/src/journal.rs
index 689e2b7..905ae61 100644
--- a/core/partitions/src/journal.rs
+++ b/core/partitions/src/journal.rs
@@ -19,7 +19,7 @@
 use journal::{Journal, Storage};
 use server_common::{
     iobuf::{Frozen, Owned},
-    send_messages2::{COMMAND_HEADER_SIZE, SendMessages2Ref, decode_prepare_slice_trusted},
+    send_messages::{self, BatchRef, COMMAND_HEADER_SIZE, decode_prepare_slice_trusted},
 };
 use std::io;
 use std::{
@@ -1023,7 +1023,7 @@
 }
 
 pub fn select_batch_slice(
-    batch: &SendMessages2Ref<'_>,
+    batch: &BatchRef<'_>,
     query: MessageLookup,
     already_matched: u32,
 ) -> Option<SelectedBatchSlice> {
@@ -1087,7 +1087,7 @@
 
 /// Push the fragments for one selected batch, shared by the resident-journal
 /// walk and the disk-chunk walk. `source` holds a stamped
-/// `[256B SendMessages2Header][blob]` batch starting at byte `batch_base`
+/// `[256B BatchHeader][blob]` batch starting at byte `batch_base`
 /// (the disk walk passes the chunk cursor; the resident walk passes
 /// `size_of::<PrepareHeader>()`, the batch's offset past the prepare header).
 /// A full-body selection forwards the original batch bytes by reference; a
@@ -1099,7 +1099,7 @@
     matched_messages: &mut u32,
     source: &Frozen<4096>,
     batch_base: usize,
-    batch: &SendMessages2Ref<'_>,
+    batch: &BatchRef<'_>,
     selection: SelectedBatchSlice,
 ) {
     let full_body_selected = selection.start == 0 && selection.end == batch.blob().len();
@@ -1122,7 +1122,9 @@
                 .get(selection.start..selection.end)
                 .expect("selected batch slice must stay within blob bounds"),
         );
-        fragments.push(Fragment::whole(rewritten.into_frozen()));
+        fragments.push(Fragment::whole(send_messages::frozen_batch_header(
+            &rewritten,
+        )));
         fragments.push(Fragment::slice(
             source.clone(),
             batch_base + COMMAND_HEADER_SIZE + selection.start,
@@ -1228,17 +1230,17 @@
 mod tests {
     use super::*;
     use bytes::Bytes;
-    use iggy_binary_protocol::{Command2, HEADER_SIZE};
+    use iggy_binary_protocol::{Command, HEADER_SIZE};
     use journal::Journal;
     use server_common::Message;
-    use server_common::send_messages2::{
-        IggyMessage2, IggyMessage2Header, IggyMessages2, SendMessages2Owned, decode_batch_slice,
+    use server_common::send_messages::{
+        IggyMessage, IggyMessageHeader, IggyMessages, SendMessagesOwned, decode_batch_slice,
     };
     use server_common::sharding::IggyNamespace;
 
     fn build_prepare(op: u64, size: usize) -> Message<PrepareHeader> {
         Message::<PrepareHeader>::new(size).transmute_header(|_, h: &mut PrepareHeader| {
-            h.command = Command2::Prepare;
+            h.command = Command::Prepare;
             h.op = op;
             h.size = u32::try_from(size).expect("size fits in u32");
         })
@@ -1468,10 +1470,10 @@
     /// producer). Timestamp polls filter on the broker time because that is
     /// the timestamp replies surface per message.
     fn build_timestamped_batch(base_timestamp: u64, origin_timestamp: u64) -> Vec<u8> {
-        let mut messages = IggyMessages2::with_capacity(3);
+        let mut messages = IggyMessages::with_capacity(3);
         for index in 0..3u64 {
-            messages.push(IggyMessage2 {
-                header: IggyMessage2Header {
+            messages.push(IggyMessage {
+                header: IggyMessageHeader {
                     origin_timestamp: origin_timestamp + index,
                     payload_length: 8,
                     ..Default::default()
@@ -1480,7 +1482,7 @@
                 user_headers: None,
             });
         }
-        let mut owned = SendMessages2Owned::from_messages(IggyNamespace::new(1, 1, 0), &messages)
+        let mut owned = SendMessagesOwned::from_messages(IggyNamespace::new(1, 1, 0), &messages)
             .expect("build send_messages batch");
         owned.header.base_timestamp = base_timestamp;
         owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
diff --git a/core/partitions/src/lib.rs b/core/partitions/src/lib.rs
index 81078ba..6d14d4b 100644
--- a/core/partitions/src/lib.rs
+++ b/core/partitions/src/lib.rs
@@ -44,7 +44,7 @@
 pub use poll_plan::{AutoCommitApplied, PollPlan};
 pub use segment::Segment;
 use server_common::Message;
-pub use server_common::send_messages2::{IggyMessage2, IggyMessage2Header, IggyMessages2};
+pub use server_common::send_messages::{IggyMessage, IggyMessageHeader, IggyMessages};
 pub use types::{
     AppendResult, Fragment, PartitionOffsets, PartitionsConfig, PollFragments, PollQueryResult,
     PollingArgs, PollingConsumer, REPAIR_RETRY_TICKS, RepairConclusion, RepairSession,
diff --git a/core/partitions/src/log.rs b/core/partitions/src/log.rs
index 574b371..fc3a774 100644
--- a/core/partitions/src/log.rs
+++ b/core/partitions/src/log.rs
@@ -20,10 +20,10 @@
 use crate::messages_writer::MessagesWriter;
 use crate::poll_plan::SealedSegmentHandle;
 use crate::segment::Segment;
-use iggy_common::{IggyByteSize, IggyMessagesBatch};
+use iggy_common::IggyByteSize;
 use journal::{Journal, Storage};
 use ringbuffer::AllocRingBuffer;
-use server_common::{IggyMessagesBatchSetInFlight, SegmentStorage};
+use server_common::SegmentStorage;
 use std::collections::VecDeque;
 use std::fmt::Debug;
 use std::rc::Rc;
@@ -166,7 +166,6 @@
     // `SEALED_READ_STATE_CAP`. Keyed by offset (stable), not slot index (which
     // shifts on retire). See `touch_sealed_read_state`.
     sealed_lru: VecDeque<u64>,
-    in_flight: IggyMessagesBatchSetInFlight,
 }
 
 impl<J, S> Default for SegmentedLog<J, S>
@@ -187,7 +186,6 @@
             index_writers: Vec::with_capacity(SEGMENTS_CAPACITY),
             sealed_read_state: Vec::with_capacity(SEGMENTS_CAPACITY),
             sealed_lru: VecDeque::with_capacity(SEALED_READ_STATE_CAP + 1),
-            in_flight: IggyMessagesBatchSetInFlight::default(),
         }
     }
 }
@@ -431,22 +429,6 @@
             *segment_indexes = Some(indexes);
         }
     }
-
-    pub const fn in_flight(&self) -> &IggyMessagesBatchSetInFlight {
-        &self.in_flight
-    }
-
-    pub const fn in_flight_mut(&mut self) -> &mut IggyMessagesBatchSetInFlight {
-        &mut self.in_flight
-    }
-
-    pub fn set_in_flight(&mut self, batches: Vec<IggyMessagesBatch>) {
-        self.in_flight.set(batches);
-    }
-
-    pub fn clear_in_flight(&mut self) {
-        self.in_flight.clear();
-    }
 }
 
 impl<J, S> SegmentedLog<J, S>
diff --git a/core/partitions/src/poll_plan.rs b/core/partitions/src/poll_plan.rs
index 928222d..3b19f0d 100644
--- a/core/partitions/src/poll_plan.rs
+++ b/core/partitions/src/poll_plan.rs
@@ -39,7 +39,7 @@
     ConsumerGroupId, ConsumerGroupOffsets, ConsumerKind, ConsumerOffset, ConsumerOffsets, IggyError,
 };
 use server_common::iobuf::{Frozen, Owned};
-use server_common::send_messages2::{BatchIntegrity, COMMAND_HEADER_SIZE, decode_batch_slice_with};
+use server_common::send_messages::{BatchIntegrity, COMMAND_HEADER_SIZE, decode_batch_slice_with};
 use std::cell::{Cell, RefCell};
 use std::hash::Hash;
 use std::rc::Rc;
@@ -137,7 +137,7 @@
 /// Owned auto-commit input, applied off the partition borrow after a poll (see
 /// module docs). Only the in-memory apply happens here; durability is the
 /// replicated [`crate::iggy_partition::IggyPartition::apply_staged_consumer_offset_commit`]
-/// path's job on every node, driven by the `StoreConsumerOffset2` op the serving
+/// path's job on every node, driven by the `StoreConsumerOffset` op the serving
 /// shard submits from [`AutoCommitApplied`]. A poll-local disk write would be
 /// node-local only and diverge on failover.
 pub struct AutoCommitCtx {
@@ -148,7 +148,7 @@
 ///
 /// The serving shard replicates it through the partition consensus (the only
 /// cross-node durable path); `kind` + `consumer_id` are the offset key the
-/// submitted `StoreConsumerOffset2` op must carry.
+/// submitted `StoreConsumerOffset` op must carry.
 pub struct AutoCommitApplied {
     pub kind: ConsumerKind,
     pub consumer_id: u32,
@@ -455,7 +455,7 @@
 impl DiskReadPlan {
     /// Serve a poll from the on-disk segment files, off the partition borrow.
     /// Reads from owned descriptors so no partition reference is held across
-    /// the file IO. Walks stamped `[256B SendMessages2Header][blob]` batches in
+    /// the file IO. Walks stamped `[256B BatchHeader][blob]` batches in
     /// chunked reads, re-reading a batch split across a chunk boundary in the
     /// next chunk.
     #[allow(clippy::cast_possible_truncation)]
@@ -783,7 +783,7 @@
 
 impl AutoCommitCtx {
     /// The offset key (kind + numeric id) this auto-commit targets, for the
-    /// replicated `StoreConsumerOffset2` op the serving shard submits.
+    /// replicated `StoreConsumerOffset` op the serving shard submits.
     pub(crate) const fn kind_and_id(&self) -> (ConsumerKind, u32) {
         match &self.target {
             AutoCommitTarget::Consumer { consumer_id, .. } => {
@@ -901,7 +901,7 @@
     }
 }
 
-/// Walk stamped `[256B SendMessages2Header][blob]` batches in one disk
+/// Walk stamped `[256B BatchHeader][blob]` batches in one disk
 /// chunk, pushing matching fragments. Returns bytes consumed: the start
 /// of the first batch that did not fully fit in the chunk (the caller
 /// re-reads from there), or the chunk end when everything decoded.
diff --git a/core/partitions/src/state_transfer.rs b/core/partitions/src/state_transfer.rs
index 31802d2..1653b19 100644
--- a/core/partitions/src/state_transfer.rs
+++ b/core/partitions/src/state_transfer.rs
@@ -42,7 +42,7 @@
 use journal::superblock::SuperblockStore;
 use message_bus::MessageBus;
 use server_common::SegmentStorage;
-use server_common::send_messages2::decode_batch_slice;
+use server_common::send_messages::decode_batch_slice;
 use std::collections::HashSet;
 use std::fmt;
 use std::mem::size_of;
@@ -2263,18 +2263,8 @@
             // sweep itself is right (a chain the live state does not know
             // about would resurrect at boot), so one retry against a
             // transient open failure is the only cheap save available.
-            let enforce_fsync = self.effective_enforce_fsync(config);
-            let open = || {
-                SegmentStorage::new(
-                    &log_final,
-                    &index_final,
-                    meta.size,
-                    meta.index_size,
-                    enforce_fsync,
-                    enforce_fsync,
-                    true,
-                )
-            };
+            let open =
+                || SegmentStorage::new(&log_final, &index_final, meta.size, meta.index_size, true);
             let storage = match open().await {
                 Ok(storage) => storage,
                 Err(_) => open()
diff --git a/core/sdk/Cargo.toml b/core/sdk/Cargo.toml
index 4ff5bdc..7d496c4 100644
--- a/core/sdk/Cargo.toml
+++ b/core/sdk/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "iggy"
-version = "0.11.0-edge.3"
+version = "0.11.0-edge.4"
 description = "Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
 edition = "2024"
 rust-version.workspace = true
diff --git a/core/sdk/src/vsr.rs b/core/sdk/src/vsr.rs
index b79f17a..4a6bda5 100644
--- a/core/sdk/src/vsr.rs
+++ b/core/sdk/src/vsr.rs
@@ -21,7 +21,7 @@
     LOGIN_REGISTER_CODE, LOGIN_REGISTER_WITH_PAT_CODE, LOGOUT_USER_CODE,
 };
 use iggy_binary_protocol::consensus::{
-    Command2, EvictionHeader, EvictionReason, GenericHeader, HEADER_SIZE, Operation, ReplyHeader,
+    Command, EvictionHeader, EvictionReason, GenericHeader, HEADER_SIZE, Operation, ReplyHeader,
     RequestHeader, read_size_field, result_code, result_section_len,
 };
 use iggy_common::{IggyError, calculate_checksum, eviction_reason_to_error};
@@ -129,7 +129,7 @@
         reserved[NON_REPLICATED_CODE_RANGE].copy_from_slice(&code.to_le_bytes());
     }
     let header = RequestHeader {
-        command: Command2::Request,
+        command: Command::Request,
         operation,
         size,
         client: session.client_id(),
@@ -184,8 +184,8 @@
         .try_into()
         .map_err(|_| IggyError::InvalidCommand)?;
     match peek_command(header_bytes) {
-        Command2::Eviction => Err(decode_eviction(header_bytes)),
-        Command2::Reply => {
+        Command::Eviction => Err(decode_eviction(header_bytes)),
+        Command::Reply => {
             let total_size = response_size(header_bytes)?;
             if response.len() < total_size {
                 return Err(IggyError::InvalidCommand);
@@ -204,18 +204,18 @@
 /// buffers. Saves the 64B header `put_slice` that `decode_response` would
 /// otherwise perform when callers concatenate header + body before decoding.
 ///
-/// Also surfaces session-terminal `Command2::Eviction` frames as typed
+/// Also surfaces session-terminal `Command::Eviction` frames as typed
 /// errors: callers waiting on a Reply for an unbound session would otherwise
 /// hit a read-timeout because the SDK previously only accepted
-/// `Command2::Reply`. Returns the body slice on a normal Reply, or maps the
+/// `Command::Reply`. Returns the body slice on a normal Reply, or maps the
 /// eviction reason to an `IggyError` so the request fails fast.
 pub(crate) fn decode_response_split(
     header_bytes: &[u8; HEADER_SIZE],
     body: Bytes,
 ) -> Result<Bytes, IggyError> {
     match peek_command(header_bytes) {
-        Command2::Eviction => Err(decode_eviction(header_bytes)),
-        Command2::Reply => {
+        Command::Eviction => Err(decode_eviction(header_bytes)),
+        Command::Reply => {
             let expected_body = response_size(header_bytes)? - HEADER_SIZE;
             if body.len() < expected_body {
                 return Err(IggyError::InvalidCommand);
@@ -301,15 +301,15 @@
     }
 }
 
-/// `Command2` lives at a fixed offset shared by every consensus header
+/// `Command` lives at a fixed offset shared by every consensus header
 /// (Reply, Eviction, Prepare, ...), so a byte read is enough to discriminate
 /// the frame.
-fn peek_command(header_bytes: &[u8; HEADER_SIZE]) -> Command2 {
+fn peek_command(header_bytes: &[u8; HEADER_SIZE]) -> Command {
     const COMMAND_OFFSET: usize = std::mem::offset_of!(GenericHeader, command);
     match header_bytes[COMMAND_OFFSET] {
-        x if x == Command2::Reply as u8 => Command2::Reply,
-        x if x == Command2::Eviction as u8 => Command2::Eviction,
-        _ => Command2::Reserved,
+        x if x == Command::Reply as u8 => Command::Reply,
+        x if x == Command::Eviction as u8 => Command::Eviction,
+        _ => Command::Reserved,
     }
 }
 
@@ -441,7 +441,7 @@
         // decode funnel surfaces it as the typed error before any body decode,
         // even though the deny body is empty.
         let header = ReplyHeader {
-            command: Command2::Reply,
+            command: Command::Reply,
             size: HEADER_SIZE as u32,
             status: IggyError::Unauthorized.as_code(),
             ..Default::default()
@@ -456,7 +456,7 @@
     fn reply_with_zero_status_passes_body_through() {
         // status 0 is the ok channel: a non-metadata reply returns its body.
         let header = ReplyHeader {
-            command: Command2::Reply,
+            command: Command::Reply,
             operation: Operation::NonReplicated,
             size: (HEADER_SIZE + 3) as u32,
             ..Default::default()
@@ -679,7 +679,7 @@
     #[test]
     fn store_consumer_offset_rejection_decodes_to_terminal_error() {
         let code = IggyError::InvalidOffset(42).as_code();
-        let result = split_metadata_result(Operation::StoreConsumerOffset2, rejection_body(code));
+        let result = split_metadata_result(Operation::StoreConsumerOffset, rejection_body(code));
         assert_eq!(result.unwrap_err().as_code(), code);
     }
 
@@ -688,7 +688,7 @@
         let out = split_metadata_result(Operation::StoreConsumerOffset, success_body(b"")).unwrap();
         assert!(out.is_empty());
         let out =
-            split_metadata_result(Operation::DeleteConsumerOffset2, success_body(b"")).unwrap();
+            split_metadata_result(Operation::DeleteConsumerOffset, success_body(b"")).unwrap();
         assert!(out.is_empty());
     }
 
diff --git a/core/server/Cargo.toml b/core/server/Cargo.toml
index 1376a34..35cd5e5 100644
--- a/core/server/Cargo.toml
+++ b/core/server/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "server"
-version = "0.9.0-edge.4"
+version = "0.9.0-edge.5"
 edition = "2024"
 license = "Apache-2.0"
 publish = false
diff --git a/core/server/src/auth.rs b/core/server/src/auth.rs
index 7af2e8c..f752df6 100644
--- a/core/server/src/auth.rs
+++ b/core/server/src/auth.rs
@@ -292,7 +292,7 @@
 /// Terminal auth errors (`InvalidCredentials` / `InvalidToken` /
 /// `UserInactive` / `Session`) fast-fail with a typed `Eviction` frame so the
 /// SDK surfaces the real reason (every frame transport decodes
-/// `Command2::Eviction`) instead of a decode error or a timeout.
+/// `Command::Eviction`) instead of a decode error or a timeout.
 #[allow(clippy::future_not_send)]
 pub(crate) async fn surface_login_failure<B, MJ, S, SB>(
     shard: &Rc<ShellShard<B, MJ, S, SB>>,
diff --git a/core/server/src/bootstrap.rs b/core/server/src/bootstrap.rs
index 03817cd..a78cc84 100644
--- a/core/server/src/bootstrap.rs
+++ b/core/server/src/bootstrap.rs
@@ -2436,16 +2436,12 @@
     let segment_size = runtime_options
         .segment_size
         .unwrap_or_else(|| IggyByteSize::from(iggy_common::DEFAULT_SEGMENT_SIZE));
-    let enforce_fsync = runtime_options
-        .enforce_fsync
-        .unwrap_or(iggy_common::DEFAULT_ENFORCE_FSYNC);
     load_persisted_segments(
         config,
         stream_id,
         topic_id,
         partition_id,
         segment_size,
-        enforce_fsync,
         stats,
     )
     .await
diff --git a/core/server/src/consumer_group.rs b/core/server/src/consumer_group.rs
index fd93e35..5eebb3e 100644
--- a/core/server/src/consumer_group.rs
+++ b/core/server/src/consumer_group.rs
@@ -37,8 +37,7 @@
     LeaveConsumerGroupRequest as WireLeaveConsumerGroupRequest,
 };
 use iggy_binary_protocol::requests::consumer_offsets::{
-    DeleteConsumerOffset2Request, DeleteConsumerOffsetRequest, StoreConsumerOffset2Request,
-    StoreConsumerOffsetRequest,
+    DeleteConsumerOffsetRequest, StoreConsumerOffsetRequest,
 };
 use iggy_binary_protocol::{KIND_CONSUMER_GROUP, Operation, RoutedRequestHeader, WireIdentifier};
 use iggy_common::IggyError;
@@ -220,18 +219,15 @@
     let operation = request.header().operation;
     if !matches!(
         operation,
-        Operation::StoreConsumerOffset
-            | Operation::StoreConsumerOffset2
-            | Operation::DeleteConsumerOffset
-            | Operation::DeleteConsumerOffset2
+        Operation::StoreConsumerOffset | Operation::DeleteConsumerOffset
     ) {
         return Ok(request);
     }
     let body = request_body(&request);
-    // The 4 store/delete (v1 + v2) ops differ only in the decode type; this
-    // collapses their identical decode -> resolve group id -> rewrite consumer
-    // id -> re-encode bodies. A non-group consumer or unresolved group returns
-    // the request untouched (the apply/read path handles the miss).
+    // The store/delete ops differ only in the decode type; this collapses
+    // their identical decode -> resolve group id -> rewrite consumer id ->
+    // re-encode bodies. A non-group consumer or unresolved group returns the
+    // request untouched (the apply/read path handles the miss).
     macro_rules! rewrite_group_offset {
         ($ty:ty) => {{
             let mut wire = <$ty>::decode_from(body).map_err(|_| IggyError::InvalidCommand)?;
@@ -250,10 +246,8 @@
     }
     let rewritten = match operation {
         Operation::StoreConsumerOffset => rewrite_group_offset!(StoreConsumerOffsetRequest),
-        Operation::StoreConsumerOffset2 => rewrite_group_offset!(StoreConsumerOffset2Request),
         Operation::DeleteConsumerOffset => rewrite_group_offset!(DeleteConsumerOffsetRequest),
-        Operation::DeleteConsumerOffset2 => rewrite_group_offset!(DeleteConsumerOffset2Request),
-        // The outer `matches!` already filtered to the 4 ops above, but the
+        // The outer `matches!` already filtered to the 2 ops above, but the
         // match is over the 37-variant `Operation`, so a catch-all is required.
         _ => return Ok(request),
     };
diff --git a/core/server/src/dispatch.rs b/core/server/src/dispatch.rs
index 4600ca0..9251bf9 100644
--- a/core/server/src/dispatch.rs
+++ b/core/server/src/dispatch.rs
@@ -68,7 +68,7 @@
 use iggy_binary_protocol::primitives::polling_strategy::WirePollingStrategy;
 use iggy_binary_protocol::requests::consumer_groups::SyncConsumerGroupRequest;
 use iggy_binary_protocol::requests::consumer_offsets::{
-    GetConsumerOffsetRequest, StoreConsumerOffset2Request,
+    GetConsumerOffsetRequest, StoreConsumerOffsetRequest,
 };
 use iggy_binary_protocol::requests::messages::PollMessagesRequest;
 use iggy_binary_protocol::requests::partitions::{
@@ -88,7 +88,7 @@
 use iggy_binary_protocol::responses::consumer_groups::SyncConsumerGroupResponse;
 use iggy_binary_protocol::responses::system::get_snapshot::GetSnapshotResponse;
 use iggy_binary_protocol::{
-    AckLevel, ClientVersionInfo, Command2, ConsensusHeader, EvictionReason, ForwardLogoutHeader,
+    AckLevel, ClientVersionInfo, Command, ConsensusHeader, EvictionReason, ForwardLogoutHeader,
     ForwardLogoutOutcome, ForwardLogoutResultHeader, ForwardRegisterHeader, ForwardRegisterOutcome,
     ForwardRegisterResultHeader, GenericHeader, HEADER_SIZE, KIND_CONSUMER_GROUP,
     MAX_PARTITIONS_PER_REQUEST, Operation, ProtocolVersion, RequestHeader, RoutedRequestHeader,
@@ -404,7 +404,7 @@
     shard.dispatch(message.into_generic());
 }
 
-/// Build the synthetic `StoreConsumerOffset2` request for an auto-commit, keyed
+/// Build the synthetic `StoreConsumerOffset` request for an auto-commit, keyed
 /// to the resolved numeric consumer/group id and stamped with the reserved
 /// [`AUTO_COMMIT_CLIENT_ID`] so the commit path skips the (unwaited) reply. The
 /// wire stream/topic ids are cosmetic here -- admission and apply key off the
@@ -414,7 +414,7 @@
     namespace: IggyNamespace,
     applied: &AutoCommitApplied,
 ) -> Result<Message<RoutedRequestHeader>, IggyError> {
-    let request = StoreConsumerOffset2Request {
+    let request = StoreConsumerOffsetRequest {
         consumer: WireConsumer {
             kind: applied.kind.as_code(),
             id: WireIdentifier::Numeric(applied.consumer_id),
@@ -434,8 +434,8 @@
     Ok(
         message.transmute_header(|_, header: &mut RoutedRequestHeader| {
             *header = RoutedRequestHeader {
-                command: Command2::Request,
-                operation: Operation::StoreConsumerOffset2,
+                command: Command::Request,
+                operation: Operation::StoreConsumerOffset,
                 size,
                 client: AUTO_COMMIT_CLIENT_ID,
                 // The partition plane is sessionless (no `ClientTable` dedup); a
@@ -2840,7 +2840,7 @@
 ) -> Message<ForwardRegisterHeader> {
     Message::<ForwardRegisterHeader>::new(HEADER_SIZE).transmute_header(
         |_, header: &mut ForwardRegisterHeader| {
-            header.command = Command2::ForwardRegister;
+            header.command = Command::ForwardRegister;
             header.cluster = cluster;
             header.view = view;
             header.replica = replica;
@@ -2865,7 +2865,7 @@
     let (session, outcome) = forward_register_outcome(bound);
     Message::<ForwardRegisterResultHeader>::new(HEADER_SIZE).transmute_header(
         |_, header: &mut ForwardRegisterResultHeader| {
-            header.command = Command2::ForwardRegisterResult;
+            header.command = Command::ForwardRegisterResult;
             header.cluster = cluster;
             header.view = view;
             header.replica = replica;
@@ -3029,7 +3029,7 @@
 ) -> Message<ForwardLogoutHeader> {
     Message::<ForwardLogoutHeader>::new(HEADER_SIZE).transmute_header(
         |_, header: &mut ForwardLogoutHeader| {
-            header.command = Command2::ForwardLogout;
+            header.command = Command::ForwardLogout;
             header.cluster = cluster;
             header.view = view;
             header.replica = replica;
@@ -3055,7 +3055,7 @@
     let (commit, outcome) = forward_logout_outcome(result);
     Message::<ForwardLogoutResultHeader>::new(HEADER_SIZE).transmute_header(
         |_, header: &mut ForwardLogoutResultHeader| {
-            header.command = Command2::ForwardLogoutResult;
+            header.command = Command::ForwardLogoutResult;
             header.cluster = cluster;
             header.view = view;
             header.replica = replica;
@@ -4028,7 +4028,7 @@
             client,
             request: 0,
             commit: session,
-            command: Command2::Reply,
+            command: Command::Reply,
             operation: Operation::Register,
             ..Default::default()
         };
@@ -4051,7 +4051,7 @@
             let header =
                 bytemuck::checked::from_bytes_mut::<RoutedRequestHeader>(&mut slice[..header_size]);
             *header = RoutedRequestHeader {
-                command: Command2::Request,
+                command: Command::Request,
                 operation,
                 size: u32::try_from(total).expect("test request fits u32"),
                 client,
@@ -4083,7 +4083,7 @@
             let header =
                 bytemuck::checked::from_bytes_mut::<PrepareHeader>(&mut slice[..header_size]);
             *header = PrepareHeader {
-                command: Command2::Prepare,
+                command: Command::Prepare,
                 operation,
                 size: u32::try_from(total).expect("test prepare fits u32"),
                 op: 1,
@@ -4559,7 +4559,7 @@
         await_forward(&bus).await;
         let (target, forward) = bus.sole_replica_send::<ForwardRegisterHeader>();
         assert_eq!(target, 0, "forward must address the view's primary");
-        assert_eq!(forward.command, Command2::ForwardRegister);
+        assert_eq!(forward.command, Command::ForwardRegister);
         assert_eq!(forward.client, CLIENT);
         assert_eq!(
             forward.user_id, USER,
@@ -4605,7 +4605,7 @@
         await_forward(&bus).await;
         let (target, forward) = bus.sole_replica_send::<ForwardLogoutHeader>();
         assert_eq!(target, 0, "forward must address the view's primary");
-        assert_eq!(forward.command, Command2::ForwardLogout);
+        assert_eq!(forward.command, Command::ForwardLogout);
         assert_eq!(forward.client, CLIENT);
         assert_eq!(forward.session, SESSION);
         assert_eq!(forward.request, REQUEST);
@@ -4944,7 +4944,7 @@
                     &mut message.as_mut_slice()[..header_size],
                 );
                 *header = RequestHeader {
-                    command: Command2::Request,
+                    command: Command::Request,
                     operation: Operation::NonReplicated,
                     size: u32::try_from(header_size).expect("header fits u32"),
                     client: TRANSPORT,
@@ -5002,7 +5002,7 @@
             assert_eq!(*client, TRANSPORT);
             assert_eq!(
                 frame[COMMAND_OFFSET],
-                Command2::Reply as u8,
+                Command::Reply as u8,
                 "an unbound cluster-metadata read must be denied with a Reply, not evicted"
             );
             let status =
diff --git a/core/server/src/dispatch/authz.rs b/core/server/src/dispatch/authz.rs
index 424572c..73cfc09 100644
--- a/core/server/src/dispatch/authz.rs
+++ b/core/server/src/dispatch/authz.rs
@@ -87,13 +87,13 @@
                 Operation::SendMessages => {
                     permissioner.append_messages(user_id, stream_id, topic_id)
                 }
-                Operation::StoreConsumerOffset | Operation::StoreConsumerOffset2 => {
+                Operation::StoreConsumerOffset => {
                     permissioner.store_consumer_offset(user_id, stream_id, topic_id)
                 }
-                Operation::DeleteConsumerOffset | Operation::DeleteConsumerOffset2 => {
+                Operation::DeleteConsumerOffset => {
                     permissioner.delete_consumer_offset(user_id, stream_id, topic_id)
                 }
-                // The caller only routes the five partition ops above here. The
+                // The caller only routes the three partition ops above here. The
                 // rest are listed exhaustively (no `_`) so a newly added op
                 // forces a gate decision at compile time instead of silently
                 // slipping through ungated.
diff --git a/core/server/src/http/handlers.rs b/core/server/src/http/handlers.rs
index e138249..c1172a4 100644
--- a/core/server/src/http/handlers.rs
+++ b/core/server/src/http/handlers.rs
@@ -1346,7 +1346,7 @@
 ///
 /// Data plane like a produce: the offset write is a replicated op on the
 /// partition group's own consensus, awaited through the session's in-process
-/// reply slot ([`partition_write_replicated`]). The v2 wire op is pinned to
+/// reply slot ([`partition_write_replicated`]). The wire op is pinned to
 /// `ack = Quorum` - `?ack=none` is a produce-only surface. The consumer
 /// identifier passes through on the wire; the dispatch resolvers hash named
 /// consumers and rewrite group ids server-side, identically to TCP.
@@ -1374,7 +1374,7 @@
     SendWrapper::new(partition_write_replicated(
         &state,
         &identity.session,
-        Operation::StoreConsumerOffset2,
+        Operation::StoreConsumerOffset,
         &body,
     ))
     .await?;
@@ -1417,7 +1417,7 @@
     SendWrapper::new(partition_write_replicated(
         &state,
         &identity.session,
-        Operation::DeleteConsumerOffset2,
+        Operation::DeleteConsumerOffset,
         &body,
     ))
     .await?;
diff --git a/core/server/src/http/reply.rs b/core/server/src/http/reply.rs
index 9c3a59f..c17f5ce 100644
--- a/core/server/src/http/reply.rs
+++ b/core/server/src/http/reply.rs
@@ -20,7 +20,7 @@
 //! login error mappings the write and auth paths render through.
 
 use iggy_binary_protocol::consensus::{
-    Command2, EvictionHeader, HEADER_SIZE, result_code, result_section_len,
+    Command, EvictionHeader, HEADER_SIZE, result_code, result_section_len,
 };
 use iggy_binary_protocol::responses::consumer_groups::get_consumer_group::ConsumerGroupDetailsResponse;
 use iggy_binary_protocol::responses::messages::SendMessagesResponse;
@@ -67,7 +67,7 @@
         .get(..HEADER_SIZE)
         .and_then(|bytes| bytemuck::checked::try_from_bytes::<ReplyHeader>(bytes).ok())
         .ok_or(PartitionWriteError::Rejected(IggyError::InvalidCommand))?;
-    if header.command != Command2::Reply {
+    if header.command != Command::Reply {
         return Err(PartitionWriteError::Rejected(IggyError::InvalidCommand));
     }
     if header.status != 0 {
@@ -296,7 +296,7 @@
             "status carries the deny code"
         );
         assert_ne!(status, 0, "a deny status is nonzero so the SDK peek fires");
-        assert_eq!(reply.header().command, Command2::Reply);
+        assert_eq!(reply.header().command, Command::Reply);
         // Echoes the request so the SDK routes it back to the waiting slot.
         assert_eq!(reply.header().request, request.header().request);
         assert_eq!(reply.header().operation, request.header().operation);
@@ -338,7 +338,7 @@
 
         // The consensus plane's committed-reply builder (plane_helpers path).
         let prepare = PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             operation: Operation::SendMessages,
             client: 42,
             op: 1,
@@ -369,7 +369,7 @@
     #[test]
     fn committed_partition_reply_classifies_as_success() {
         let prepare = PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             operation: Operation::SendMessages,
             client: 42,
             op: 1,
@@ -381,12 +381,12 @@
     }
 
     /// The 204 path of the offset write routes: a committed
-    /// `StoreConsumerOffset2` reply (op >= 1) classifies as success.
+    /// `StoreConsumerOffset` reply (op >= 1) classifies as success.
     #[test]
     fn committed_offset_write_reply_classifies_as_success() {
         let prepare = PrepareHeader {
-            command: Command2::Prepare,
-            operation: Operation::StoreConsumerOffset2,
+            command: Command::Prepare,
+            operation: Operation::StoreConsumerOffset,
             client: 42,
             op: 3,
             request: 2,
@@ -488,7 +488,7 @@
 
     fn send_reply(body: &Bytes) -> BusMessage {
         let prepare = PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             operation: Operation::SendMessages,
             client: 42,
             op: 1,
@@ -554,7 +554,7 @@
     /// mapped `IggyError`, not as the generic op-0 not-found.
     #[test]
     fn status_bearing_deny_reply_classifies_as_typed_rejection() {
-        let request = build_request_message(Operation::DeleteConsumerOffset2, 42, 7, 1, &[]);
+        let request = build_request_message(Operation::DeleteConsumerOffset, 42, 7, 1, &[]);
         let mut deny = build_empty_reply(request.header(), 42, 0, 9);
         let header = bytemuck::checked::try_from_bytes_mut::<ReplyHeader>(
             &mut deny.as_mut_slice()[..HEADER_SIZE],
diff --git a/core/server/src/http/submit.rs b/core/server/src/http/submit.rs
index f9543cd..ca763d8 100644
--- a/core/server/src/http/submit.rs
+++ b/core/server/src/http/submit.rs
@@ -25,7 +25,7 @@
 use bytes::Bytes;
 use consensus::MetadataHandle;
 use futures::channel::oneshot;
-use iggy_binary_protocol::consensus::Command2;
+use iggy_binary_protocol::consensus::Command;
 use iggy_binary_protocol::{GenericHeader, Operation, ReplyHeader, RoutedRequestHeader};
 use iggy_common::IggyError;
 use message_bus::BusMessage;
@@ -247,7 +247,7 @@
         let Some(reply) = submit_client_request_on_owner(shard, request).await else {
             return Err(WriteError::Unavailable);
         };
-        let transient = (reply.header().command == Command2::Reply)
+        let transient = (reply.header().command == Command::Reply)
             .then(|| transient_code(&reply))
             .flatten();
         let Some(transient) = transient else {
@@ -283,13 +283,13 @@
     };
 
     match reply.header().command {
-        Command2::Reply => {
+        Command::Reply => {
             // Already burned at stamp time; release the gate so the next
             // write on this session can take its turn.
             drop(next_request_id);
             Ok((request_header, reply, raw_token))
         }
-        Command2::Eviction => Err(WriteError::Evicted(eviction_error(&reply))),
+        Command::Eviction => Err(WriteError::Evicted(eviction_error(&reply))),
         _ => Err(WriteError::Rejected(IggyError::InvalidCommand)),
     }
 }
diff --git a/core/server/src/http/wire.rs b/core/server/src/http/wire.rs
index c7026d0..20ada0c 100644
--- a/core/server/src/http/wire.rs
+++ b/core/server/src/http/wire.rs
@@ -19,11 +19,11 @@
 //! the control-plane [`Message<RoutedRequestHeader>`] builder shared by the write path.
 
 use bytes::{Bytes, BytesMut};
-use iggy_binary_protocol::consensus::{Command2, HEADER_SIZE};
+use iggy_binary_protocol::consensus::{Command, HEADER_SIZE};
 use iggy_binary_protocol::primitives::consumer::WireConsumer;
 use iggy_binary_protocol::primitives::polling_strategy::WirePollingStrategy;
 use iggy_binary_protocol::requests::consumer_offsets::{
-    DeleteConsumerOffset2Request, GetConsumerOffsetRequest, StoreConsumerOffset2Request,
+    DeleteConsumerOffsetRequest, GetConsumerOffsetRequest, StoreConsumerOffsetRequest,
 };
 use iggy_binary_protocol::requests::messages::{
     PollMessagesRequest, RawMessage, SendMessagesEncoder,
@@ -58,7 +58,14 @@
     let raw_messages: Vec<RawMessage<'_>> = views
         .iter()
         .map(|view| RawMessage {
-            id: view.header().id(),
+            // HTTP producers send no id; the producer side owns minting now
+            // (the id sits under the frame checksum), and for JSON bodies
+            // this handler IS the producer encoder.
+            id: if view.header().id() == 0 {
+                iggy_common::random_id::get_uuid()
+            } else {
+                view.header().id()
+            },
             origin_timestamp: view.header().origin_timestamp(),
             headers: view.user_headers(),
             payload: view.payload(),
@@ -77,7 +84,13 @@
         &wire_topic_id,
         &wire_partitioning,
         &raw_messages,
-    );
+    )
+    .map_err(|error| match error {
+        iggy_binary_protocol::WireError::InvalidMessageTimestampDelta(delta) => {
+            IggyError::InvalidMessageTimestampDelta(delta)
+        }
+        _ => IggyError::InvalidCommand,
+    })?;
     Ok(buf.freeze())
 }
 
@@ -128,8 +141,8 @@
     })
 }
 
-/// Map a validated HTTP store-offset body onto the v2 wire request
-/// (`StoreConsumerOffset2Request`), `ack` pinned to `Quorum` so the route can
+/// Map a validated HTTP store-offset body onto the wire request
+/// (`StoreConsumerOffsetRequest`), `ack` pinned to `Quorum` so the route can
 /// await the committed reply. The body's consumer kind is structurally always
 /// `Consumer` (`Consumer::kind` is `#[serde(skip)]`), matching the legacy HTTP
 /// server; `partition_id` passes through as the wire `Option` (flag byte +
@@ -138,8 +151,8 @@
     stream_id: &Identifier,
     topic_id: &Identifier,
     command: &StoreConsumerOffset,
-) -> Result<StoreConsumerOffset2Request, IggyError> {
-    Ok(StoreConsumerOffset2Request {
+) -> Result<StoreConsumerOffsetRequest, IggyError> {
+    Ok(StoreConsumerOffsetRequest {
         consumer: consumer_to_wire(&command.consumer)?,
         stream_id: identifier_to_wire(stream_id)?,
         topic_id: identifier_to_wire(topic_id)?,
@@ -149,16 +162,16 @@
     })
 }
 
-/// Map a validated HTTP delete-offset request onto the v2 wire request
-/// (`DeleteConsumerOffset2Request`), `ack` pinned to `Quorum` like
+/// Map a validated HTTP delete-offset request onto the wire request
+/// (`DeleteConsumerOffsetRequest`), `ack` pinned to `Quorum` like
 /// [`store_offset_wire_request`].
 pub(in crate::http) fn delete_offset_wire_request(
     stream_id: &Identifier,
     topic_id: &Identifier,
     consumer: &Consumer,
     partition_id: Option<u32>,
-) -> Result<DeleteConsumerOffset2Request, IggyError> {
-    Ok(DeleteConsumerOffset2Request {
+) -> Result<DeleteConsumerOffsetRequest, IggyError> {
+    Ok(DeleteConsumerOffsetRequest {
         consumer: consumer_to_wire(consumer)?,
         stream_id: identifier_to_wire(stream_id)?,
         topic_id: identifier_to_wire(topic_id)?,
@@ -197,7 +210,7 @@
         &mut message.as_mut_slice()[..HEADER_SIZE],
     )
     .expect("zeroed bytes form a valid RoutedRequestHeader");
-    header.command = Command2::Request;
+    header.command = Command::Request;
     header.operation = operation;
     header.client = client_id;
     header.session = session_id;
@@ -214,34 +227,31 @@
 
     use iggy_binary_protocol::WireDecode;
     use iggy_binary_protocol::WireEncode;
-    use iggy_binary_protocol::WireMessageIterator;
-    use iggy_binary_protocol::message_layout::WIRE_MESSAGE_INDEX_SIZE;
-    use iggy_binary_protocol::requests::messages::SendMessagesHeader;
     use iggy_common::delete_consumer_offset::DeleteConsumerOffset;
     use iggy_common::{
-        Consumer, ConsumerKind, IggyMessage, IggyMessagesBatch, IggyTimestamp, Partitioning,
-        PartitioningKind, PollingKind, PollingStrategy, Validatable,
+        Consumer, ConsumerKind, IggyMessagesBatch, IggyTimestamp, Partitioning, PartitioningKind,
+        PollingKind, PollingStrategy, Validatable,
     };
     use partitions::{Fragment, PollFragments};
     use server_common::MESSAGE_ALIGN;
     use server_common::iobuf::Owned;
-    use server_common::send_messages2::{
-        COMMAND_HEADER_SIZE, IggyMessage2, IggyMessage2Header, IggyMessages2, SendMessages2Header,
-        SendMessages2Owned,
+    use server_common::send_messages::{
+        BatchHeader, COMMAND_HEADER_SIZE, IggyMessage, IggyMessageHeader, IggyMessages,
+        SendMessagesOwned,
     };
 
     use crate::http::error::{Consistency, ConsistencyQuery};
     use crate::responses::build_polled_messages_body;
 
     fn produce_command(partitioning: Partitioning) -> SendMessages {
-        let first = IggyMessage::builder()
+        let first = iggy_common::IggyMessage::builder()
             .id(7)
             .payload(Bytes::from_static(b"first"))
             .build()
             .expect("valid message");
         // Raw pre-encoded user headers, mirroring the HTTP deserializer's
         // base64 branch.
-        let mut second = IggyMessage::builder()
+        let mut second = iggy_common::IggyMessage::builder()
             .id(8)
             .payload(Bytes::from_static(b"second"))
             .build()
@@ -274,7 +284,10 @@
         let metadata_length =
             u32::from_le_bytes(bytes[..4].try_into().expect("length prefix")) as usize;
         let (header, consumed) =
-            SendMessagesHeader::decode(&bytes[4..4 + metadata_length]).expect("valid metadata");
+            iggy_binary_protocol::requests::messages::SendMessagesHeader::decode(
+                &bytes[4..4 + metadata_length],
+            )
+            .expect("valid metadata");
         assert_eq!(consumed, metadata_length);
         assert_eq!(header.stream_id, identifier_to_wire(&stream_id).unwrap());
         assert_eq!(header.topic_id, identifier_to_wire(&topic_id).unwrap());
@@ -284,18 +297,53 @@
         );
         assert_eq!(header.messages_count, 2);
 
-        let data_offset = 4 + metadata_length + 2 * WIRE_MESSAGE_INDEX_SIZE;
-        let views: Vec<_> = WireMessageIterator::new(&bytes[data_offset..], 2)
-            .collect::<Result<Vec<_>, _>>()
-            .expect("valid message frames");
-        assert_eq!(views[0].id(), 7);
-        assert_eq!(views[0].payload(), b"first");
-        assert_eq!(views[0].user_headers(), b"");
-        assert_eq!(views[0].origin_timestamp(), origin_timestamps[0]);
-        assert_eq!(views[1].id(), 8);
-        assert_eq!(views[1].payload(), b"second");
-        assert_eq!(views[1].user_headers(), b"raw-header-bytes");
-        assert_eq!(views[1].origin_timestamp(), origin_timestamps[1]);
+        let batch = iggy_binary_protocol::batch::decode_batch_slice(&bytes[4 + metadata_length..])
+            .expect("valid producer batch");
+        assert_eq!(batch.header.partition_id, 0);
+        assert_eq!(batch.message_count(), 2);
+        let views: Vec<_> = batch.iter().collect();
+        let batch_origin = batch.header.origin_timestamp;
+        assert_eq!(views[0].header.id, 7);
+        assert_eq!(views[0].payload, b"first");
+        assert_eq!(views[0].user_headers, b"");
+        assert_eq!(
+            batch_origin + u64::from(views[0].header.timestamp_delta),
+            origin_timestamps[0]
+        );
+        assert_eq!(views[1].header.id, 8);
+        assert_eq!(views[1].payload, b"second");
+        assert_eq!(views[1].user_headers, b"raw-header-bytes");
+        assert_eq!(
+            batch_origin + u64::from(views[1].header.timestamp_delta),
+            origin_timestamps[1]
+        );
+    }
+
+    #[test]
+    fn encode_send_messages_mints_ids_for_zero_id_messages() {
+        // JSON producers send no id; the frame checksum covers the id field,
+        // so this handler must mint before encoding - the server no longer
+        // assigns ids at admission.
+        let stream_id = Identifier::from_str_value("1").expect("valid stream id");
+        let topic_id = Identifier::from_str_value("orders").expect("valid topic id");
+        let message = iggy_common::IggyMessage::builder()
+            .payload(Bytes::from_static(b"no-id"))
+            .build()
+            .expect("valid message");
+        assert_eq!(message.header.id, 0, "builder default id must be zero");
+        let command = SendMessages {
+            partitioning: Partitioning::partition_id(1),
+            batch: IggyMessagesBatch::from(&vec![message]),
+            ..Default::default()
+        };
+
+        let bytes = encode_send_messages(&stream_id, &topic_id, &command).expect("encodes");
+        let metadata_length =
+            u32::from_le_bytes(bytes[..4].try_into().expect("length prefix")) as usize;
+        let batch = iggy_binary_protocol::batch::decode_batch_slice(&bytes[4 + metadata_length..])
+            .expect("valid producer batch");
+        let views: Vec<_> = batch.iter().collect();
+        assert_ne!(views[0].header.id, 0, "zero id must be minted at encode");
     }
 
     #[test]
@@ -402,7 +450,7 @@
         let bytes = request.to_bytes();
         assert_eq!(*bytes.last().expect("non-empty"), AckLevel::Quorum.as_u8());
         let (decoded, consumed) =
-            StoreConsumerOffset2Request::decode(&bytes).expect("decodes as the server does");
+            StoreConsumerOffsetRequest::decode(&bytes).expect("decodes as the server does");
         assert_eq!(consumed, bytes.len());
         assert_eq!(decoded, request);
         assert_eq!(decoded.consumer.kind, 1);
@@ -423,7 +471,7 @@
         let request = store_offset_wire_request(&stream_id, &topic_id, &command).expect("maps");
         let bytes = request.to_bytes();
         let (decoded, consumed) =
-            StoreConsumerOffset2Request::decode(&bytes).expect("decodes as the server does");
+            StoreConsumerOffsetRequest::decode(&bytes).expect("decodes as the server does");
         assert_eq!(consumed, bytes.len());
         assert_eq!(decoded.partition_id, None);
         assert_eq!(decoded.offset, u64::MAX);
@@ -445,7 +493,7 @@
             let bytes = request.to_bytes();
             assert_eq!(*bytes.last().expect("non-empty"), AckLevel::Quorum.as_u8());
             let (decoded, consumed) =
-                DeleteConsumerOffset2Request::decode(&bytes).expect("decodes as the server does");
+                DeleteConsumerOffsetRequest::decode(&bytes).expect("decodes as the server does");
             assert_eq!(consumed, bytes.len());
             assert_eq!(decoded, request);
             assert_eq!(decoded.consumer.kind, 1);
@@ -464,10 +512,10 @@
         assert_eq!(query.partition_id, None);
     }
 
-    /// Wrap one stored `SendMessages2` batch (`[256B header][blob]`) as the
+    /// Wrap one stored `SendMessages` batch (`[256B header][blob]`) as the
     /// poll fragment the owning shard replies, the shape
     /// `build_polled_messages_body` consumes.
-    fn fragment_from_stored_batch(header: &SendMessages2Header, blob: &[u8]) -> PollFragments {
+    fn fragment_from_stored_batch(header: &BatchHeader, blob: &[u8]) -> PollFragments {
         let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(COMMAND_HEADER_SIZE + blob.len());
         header.encode_into(buffer.as_mut_slice());
         buffer.as_mut_slice()[COMMAND_HEADER_SIZE..].copy_from_slice(blob);
@@ -477,14 +525,14 @@
     }
 
     /// Round-trip the poll route's encode/decode seam: the store's own batch
-    /// writer (`SendMessages2Owned::from_messages`) is the encoder oracle,
+    /// writer (`SendMessagesOwned::from_messages`) is the encoder oracle,
     /// `build_polled_messages_body` re-encodes to the legacy wire body, and
     /// the SDK's `PolledMessages::from_bytes` must read back every field.
     #[test]
     fn polled_messages_body_decodes_into_common_polled_messages() {
-        let mut messages = IggyMessages2::with_capacity(2);
-        messages.push(IggyMessage2 {
-            header: IggyMessage2Header {
+        let mut messages = IggyMessages::with_capacity(2);
+        messages.push(IggyMessage {
+            header: IggyMessageHeader {
                 id: 7,
                 origin_timestamp: 1_000,
                 ..Default::default()
@@ -492,8 +540,8 @@
             payload: Bytes::from_static(b"first"),
             user_headers: None,
         });
-        messages.push(IggyMessage2 {
-            header: IggyMessage2Header {
+        messages.push(IggyMessage {
+            header: IggyMessageHeader {
                 id: 8,
                 origin_timestamp: 1_050,
                 ..Default::default()
@@ -503,7 +551,7 @@
         });
         let namespace = server_common::sharding::IggyNamespace::new(0, 0, 3);
         let stored =
-            SendMessages2Owned::from_messages(namespace, &messages).expect("encodes stored batch");
+            SendMessagesOwned::from_messages(namespace, &messages).expect("encodes stored batch");
         // The store stamps these on append; `from_messages` leaves them zero.
         let mut header = stored.header;
         header.base_offset = 41;
diff --git a/core/server/src/partition_helpers.rs b/core/server/src/partition_helpers.rs
index 76ddab4..8fb4a0e 100644
--- a/core/server/src/partition_helpers.rs
+++ b/core/server/src/partition_helpers.rs
@@ -341,26 +341,18 @@
     // this with a stale `.index` at offset 0 on disk. The `partitions`-side
     // writers with the same names do NOT truncate, so opening them directly
     // instead would read index entries from a previous generation.
-    let storage = SegmentStorage::new(
-        &messages_path,
-        &index_path,
-        0,
-        0,
-        enforce_fsync,
-        enforce_fsync,
-        false,
-    )
-    .await
-    .map_err(|source| {
-        error!(
-            stream_id,
-            topic_id,
-            partition_id,
-            error = %source,
-            "failed to create initial segment storage"
-        );
-        source
-    })?;
+    let storage = SegmentStorage::new(&messages_path, &index_path, 0, 0, false)
+        .await
+        .map_err(|source| {
+            error!(
+                stream_id,
+                topic_id,
+                partition_id,
+                error = %source,
+                "failed to create initial segment storage"
+            );
+            source
+        })?;
     // Share the storage's size counters so reads observe persisted bytes;
     // a writer with a private counter grows the file invisibly to readers.
     let messages_size_counter = storage
diff --git a/core/server/src/partition_reconciler.rs b/core/server/src/partition_reconciler.rs
index 4c3aeec..b456a30 100644
--- a/core/server/src/partition_reconciler.rs
+++ b/core/server/src/partition_reconciler.rs
@@ -1183,7 +1183,7 @@
         PurgeTopicRequest,
     };
     use iggy_binary_protocol::{
-        Command2, Operation, PrepareHeader, ReplyHeader, RoutedRequestHeader, WireIdentifier,
+        Command, Operation, PrepareHeader, ReplyHeader, RoutedRequestHeader, WireIdentifier,
         WireOptions,
     };
     use message_bus::IggyMessageBus;
@@ -1250,7 +1250,7 @@
             &mut msg.as_mut_slice()[..header_size],
         )
         .expect("zeroed bytes form a valid PrepareHeader");
-        header.command = Command2::Prepare;
+        header.command = Command::Prepare;
         header.size = u32::try_from(total_size).expect("prepare size fits u32");
         header.op = op;
         header.operation = operation;
@@ -1267,7 +1267,7 @@
             &mut msg.as_mut_slice()[..header_size],
         )
         .expect("zeroed bytes form a valid PrepareHeader");
-        header.command = Command2::Prepare;
+        header.command = Command::Prepare;
         header.size = u32::try_from(header_size).expect("prepare size fits u32");
         header.operation = Operation::SendMessages;
         header.group = namespace.inner();
@@ -1306,7 +1306,7 @@
             &mut msg.as_mut_slice()[..header_size],
         )
         .expect("zeroed bytes form a valid RoutedRequestHeader");
-        header.command = Command2::Request;
+        header.command = Command::Request;
         header.size = u32::try_from(total_size).expect("request size fits u32");
         header.operation = Operation::SendMessages;
         header.group = namespace.inner();
diff --git a/core/server/src/responses.rs b/core/server/src/responses.rs
index f0edd5d..6b7fc4e 100644
--- a/core/server/src/responses.rs
+++ b/core/server/src/responses.rs
@@ -42,8 +42,7 @@
     GetConsumerGroupRequest, GetConsumerGroupsRequest,
 };
 use iggy_binary_protocol::requests::consumer_offsets::{
-    DeleteConsumerOffset2Request, DeleteConsumerOffsetRequest, StoreConsumerOffset2Request,
-    StoreConsumerOffsetRequest,
+    DeleteConsumerOffsetRequest, StoreConsumerOffsetRequest,
 };
 use iggy_binary_protocol::requests::messages::SendMessagesHeader;
 use iggy_binary_protocol::requests::personal_access_tokens::GetPersonalAccessTokensRequest;
@@ -79,7 +78,7 @@
 use iggy_binary_protocol::responses::users::get_users::GetUsersResponse;
 use iggy_binary_protocol::responses::users::user_response::UserResponse;
 use iggy_binary_protocol::{
-    Command2, GenericHeader, IGGY_PROTOCOL_VERSION, KIND_CONSUMER_GROUP, Operation, ReplyHeader,
+    Command, GenericHeader, IGGY_PROTOCOL_VERSION, KIND_CONSUMER_GROUP, Operation, ReplyHeader,
     RoutedRequestHeader, WireDecode, WireEncode, WireIdentifier, WireName, WirePartitioning,
 };
 use iggy_common::wire_conversions::{resource_options_to_wire, resource_options_to_wire_split};
@@ -92,7 +91,7 @@
 use metadata::impls::metadata::StreamsFrontend;
 use partitions::PollFragments;
 use server_common::Message;
-use server_common::send_messages2::{COMMAND_HEADER_SIZE, SendMessages2Header};
+use server_common::send_messages;
 use server_common::sharding::IggyNamespace;
 use shard::ConnectedClientInfo;
 use std::cell::RefCell;
@@ -381,30 +380,6 @@
                 client_id,
             )?
         }
-        Operation::StoreConsumerOffset2 => {
-            let request = StoreConsumerOffset2Request::decode_from(body)
-                .map_err(|_| IggyError::InvalidCommand)?;
-            fence_and_resolve_offset_namespace(
-                shard,
-                &request.consumer,
-                &request.stream_id,
-                &request.topic_id,
-                request.partition_id,
-                client_id,
-            )?
-        }
-        Operation::DeleteConsumerOffset2 => {
-            let request = DeleteConsumerOffset2Request::decode_from(body)
-                .map_err(|_| IggyError::InvalidCommand)?;
-            fence_and_resolve_offset_namespace(
-                shard,
-                &request.consumer,
-                &request.stream_id,
-                &request.topic_id,
-                request.partition_id,
-                client_id,
-            )?
-        }
         Operation::DeleteSegments => {
             let request =
                 DeleteSegmentsRequest::decode_from(body).map_err(|_| IggyError::InvalidCommand)?;
@@ -1549,7 +1524,7 @@
     // both swallow the eviction and ship a raw token whose hash never
     // committed. Only rewrite a genuine committed `Reply`; pass anything else
     // (the eviction) through untouched so the client learns its session died.
-    if committed.header().command != Command2::Reply {
+    if committed.header().command != Command::Reply {
         return Ok(committed);
     }
     let header_len = std::mem::size_of::<ReplyHeader>();
@@ -1609,7 +1584,7 @@
         size: header_size,
         view: request_header.view,
         release: request_header.release,
-        command: Command2::Reply,
+        command: Command::Reply,
         replica: request_header.replica,
         request_checksum: request_header.request_checksum,
         client: client_id,
@@ -1640,24 +1615,17 @@
         .map_or(0, VsrConsensus::commit_max)
 }
 
-/// Size of the in-storage (`IggyMessage2`) per-message header inside a
-/// `SendMessages2` batch blob: `checksum`(8) + `id`(16) + `offset_delta`(4)
-/// + `timestamp_delta`(4) + `user_headers_length`(4) + `payload_length`(4)
-/// + reserved(8). See `server_common::send_messages2::SendMessages2Owned::from_messages`.
-const STORED_MESSAGE_HEADER_SIZE: usize = 48;
-
 /// Build the `PolledMessages` reply body from the owning shard's poll
 /// fragments.
 ///
-/// Fragments carry the stored `SendMessages2` batches: a 256-byte command
-/// header followed by `IggyMessage2`-format messages
-/// (`[48B header][payload][user_headers]`, offsets/timestamps delta-encoded
-/// against the batch). The SDK decodes the legacy wire format
-/// (`[64B header][payload][user_headers]`, absolute offsets); the message
-/// sections share the legacy order, so only the header is re-encoded here
-/// and the section bytes copy through contiguously.
+/// Fragments carry the stored batch records (a 256-byte batch header plus
+/// `[48B header][payload][user_headers]` frames, deltas resolved against the
+/// stamped bases) and are served to the client as they are - the reply's
+/// message encoding IS the storage encoding. The one rewrite left is at-rest
+/// decryption: stored sections are ciphertext, and this reply is the single
+/// decrypt point, so encrypted records are rebuilt over the plaintext.
 ///
-/// Body layout: `[partition_id:4][current_offset:8][count:4][messages...]`.
+/// Body layout: `[partition_id:4][current_offset:8][count:4][batch records...]`.
 pub(crate) fn build_polled_messages_body(
     partition_id: u32,
     current_offset: u64,
@@ -1668,17 +1636,14 @@
     // COUNT_OFFSET and is backpatched once the walk below knows it.
     const HEAD_LEN: usize = 16;
     const COUNT_OFFSET: usize = 12;
-    // Batches may arrive split across fragments (rewritten command header +
-    // sliced blob); concatenate into one stream before walking batches.
+    // Batches may arrive split across fragments (rewritten batch header +
+    // sliced blob); concatenate into one stream before walking records.
     let mut stream: Vec<u8> = Vec::new();
     for fragment in fragments {
         let frozen = fragment.into_frozen();
         stream.extend_from_slice(frozen.as_slice());
     }
 
-    // Reserve the head and encode the messages straight into `body`; encoding
-    // directly avoids a separate messages buffer and its copy-through into the
-    // head.
     let mut body: Vec<u8> = Vec::with_capacity(HEAD_LEN + stream.len());
     body.extend_from_slice(&partition_id.to_le_bytes());
     body.extend_from_slice(&current_offset.to_le_bytes());
@@ -1686,104 +1651,24 @@
     let mut count: u32 = 0;
     let mut position = 0usize;
     while position < stream.len() {
-        let batch = SendMessages2Header::decode(&stream[position..])?;
+        let batch = send_messages::BatchHeader::decode(&stream[position..])
+            .map_err(|_| IggyError::InvalidCommand)?;
         let batch_end = position
-            .checked_add(
-                usize::try_from(batch.batch_length).map_err(|_| IggyError::InvalidCommand)?,
-            )
+            .checked_add(batch.total_size())
             .ok_or(IggyError::InvalidCommand)?;
         if batch_end > stream.len() {
             return Err(IggyError::InvalidCommand);
         }
-        let mut cursor = position + COMMAND_HEADER_SIZE;
-        while cursor < batch_end {
-            if cursor + STORED_MESSAGE_HEADER_SIZE > batch_end {
-                return Err(IggyError::InvalidCommand);
-            }
-            let header = &stream[cursor..cursor + STORED_MESSAGE_HEADER_SIZE];
-            let checksum = &header[0..8];
-            let id = &header[8..24];
-            let offset_delta = u32::from_le_bytes(header[24..28].try_into().expect("4-byte slice"));
-            let timestamp_delta =
-                u32::from_le_bytes(header[28..32].try_into().expect("4-byte slice"));
-            let user_headers_length =
-                u32::from_le_bytes(header[32..36].try_into().expect("4-byte slice")) as usize;
-            let payload_length =
-                u32::from_le_bytes(header[36..40].try_into().expect("4-byte slice")) as usize;
-
-            let sections_start = cursor + STORED_MESSAGE_HEADER_SIZE;
-            let sections_end = sections_start + payload_length + user_headers_length;
-            if sections_end > batch_end {
-                return Err(IggyError::InvalidCommand);
-            }
-
-            let offset = batch.base_offset + u64::from(offset_delta);
-            // `base_timestamp` is the flat broker append time stamped once per
-            // batch; `timestamp_delta` is a per-message delta against the
-            // producer origin, so it only applies to `origin_timestamp`. Adding
-            // it to the broker base would mix two clocks.
-            let timestamp = batch.base_timestamp;
-            let origin_timestamp = batch.origin_timestamp + u64::from(timestamp_delta);
-
-            body.extend_from_slice(checksum);
-            body.extend_from_slice(id);
-            body.extend_from_slice(&offset.to_le_bytes());
-            body.extend_from_slice(&timestamp.to_le_bytes());
-            body.extend_from_slice(&origin_timestamp.to_le_bytes());
-            if let Some(encryptor) = encryptor {
-                // At-rest encryption: stored sections are ciphertext (encrypted
-                // once at ingestion, replicated verbatim); this reply is the
-                // single decrypt point, so lengths are rewritten to the
-                // plaintext sizes. The stored per-message checksum still covers
-                // the ciphertext and is passed through untouched (the SDK does
-                // not re-validate it against the reply layout).
-                let payload_end = sections_start + payload_length;
-                let payload = encryptor
-                    .decrypt(&stream[sections_start..payload_end])
-                    .map_err(|_| IggyError::CannotDecryptData)?;
-                let user_headers = if user_headers_length > 0 {
-                    Some(
-                        encryptor
-                            .decrypt(&stream[payload_end..sections_end])
-                            .map_err(|_| IggyError::CannotDecryptData)?,
-                    )
-                } else {
-                    None
-                };
-                let user_headers_bytes: &[u8] = user_headers.as_deref().unwrap_or_default();
-                body.extend_from_slice(
-                    &u32::try_from(user_headers_bytes.len())
-                        .map_err(|_| IggyError::InvalidCommand)?
-                        .to_le_bytes(),
-                );
-                body.extend_from_slice(
-                    &u32::try_from(payload.len())
-                        .map_err(|_| IggyError::InvalidCommand)?
-                        .to_le_bytes(),
-                );
-                body.extend_from_slice(&0u64.to_le_bytes()); // reserved
-                body.extend_from_slice(&payload);
-                body.extend_from_slice(user_headers_bytes);
-            } else {
-                body.extend_from_slice(
-                    &u32::try_from(user_headers_length)
-                        .expect("length came from u32")
-                        .to_le_bytes(),
-                );
-                body.extend_from_slice(
-                    &u32::try_from(payload_length)
-                        .expect("length came from u32")
-                        .to_le_bytes(),
-                );
-                body.extend_from_slice(&0u64.to_le_bytes()); // reserved
-                // Stored sections are already in legacy order
-                // (`[payload][user_headers]`): copy through contiguously.
-                body.extend_from_slice(&stream[sections_start..sections_end]);
-            }
-
-            count += 1;
-            cursor = sections_end;
+        let record = &stream[position..batch_end];
+        if let Some(encryptor) = encryptor {
+            let decrypted = send_messages::decrypt_batch_record(record, encryptor)?;
+            body.extend_from_slice(&decrypted);
+        } else {
+            body.extend_from_slice(record);
         }
+        count = count
+            .checked_add(batch.message_count)
+            .ok_or(IggyError::InvalidCommand)?;
         position = batch_end;
     }
 
@@ -1813,7 +1698,7 @@
         let zeroed = [0u8; std::mem::size_of::<RoutedRequestHeader>()];
         let mut header = *bytemuck::checked::try_from_bytes::<RoutedRequestHeader>(&zeroed)
             .expect("zeroed bytes form a valid RoutedRequestHeader");
-        header.command = Command2::Request;
+        header.command = Command::Request;
         header.operation = Operation::CreatePersonalAccessToken;
         header.client = 42;
         header.session = 7;
diff --git a/core/server/src/segment_recovery.rs b/core/server/src/segment_recovery.rs
index b909b78..fcaa57a 100644
--- a/core/server/src/segment_recovery.rs
+++ b/core/server/src/segment_recovery.rs
@@ -33,7 +33,7 @@
 use partitions::state_transfer::STAGING_SUFFIX;
 use partitions::{IggyIndexReader, Segment};
 use server_common::SegmentStorage;
-use server_common::send_messages2::{COMMAND_HEADER_SIZE, SendMessages2Header, decode_batch_slice};
+use server_common::send_messages::{BatchHeader, COMMAND_HEADER_SIZE, decode_batch_slice};
 use std::fs;
 use std::os::unix::fs::FileExt;
 use std::path::PathBuf;
@@ -66,7 +66,6 @@
     topic_id: usize,
     partition_id: usize,
     segment_size: IggyByteSize,
-    enforce_fsync: bool,
     stats: &PartitionStats,
 ) -> Result<Vec<RecoveredSegment>, ServerError> {
     let partition_path = config
@@ -140,8 +139,6 @@
             &index_path,
             effective_messages_size,
             effective_index_size,
-            enforce_fsync,
-            enforce_fsync,
             true,
         )
         .await
@@ -521,7 +518,7 @@
 fn batch_verifies(
     messages: &fs::File,
     position: u64,
-    header: &SendMessages2Header,
+    header: &BatchHeader,
     scratch: &mut Vec<u8>,
 ) -> bool {
     scratch.clear();
@@ -536,11 +533,11 @@
     messages: &fs::File,
     position: u64,
     messages_size: u64,
-) -> Option<SendMessages2Header> {
+) -> Option<BatchHeader> {
     if position.checked_add(COMMAND_HEADER_SIZE as u64)? > messages_size {
         return None;
     }
     let mut header_bytes = [0u8; COMMAND_HEADER_SIZE];
     messages.read_exact_at(&mut header_bytes, position).ok()?;
-    SendMessages2Header::decode(&header_bytes).ok()
+    BatchHeader::decode(&header_bytes).ok()
 }
diff --git a/core/server/src/wire.rs b/core/server/src/wire.rs
index 02e21a5..d869a21 100644
--- a/core/server/src/wire.rs
+++ b/core/server/src/wire.rs
@@ -100,7 +100,7 @@
 mod tests {
     use super::{request_body, rewrite_request_body};
     use bytes::Bytes;
-    use iggy_binary_protocol::{Command2, Operation, RoutedRequestHeader};
+    use iggy_binary_protocol::{Command, Operation, RoutedRequestHeader};
     use server_common::Message;
     use std::mem::size_of;
 
@@ -108,7 +108,7 @@
         let total_size = size_of::<RoutedRequestHeader>() + body.len();
         let mut message = Message::<RoutedRequestHeader>::new(total_size).transmute_header(
             |_, header: &mut RoutedRequestHeader| {
-                header.command = Command2::Request;
+                header.command = Command::Request;
                 header.operation = Operation::CreateStream;
                 header.client = 1;
                 header.session = 1;
diff --git a/core/server_common/src/consensus_message.rs b/core/server_common/src/consensus_message.rs
index 79cd2f4..cd4cd91 100644
--- a/core/server_common/src/consensus_message.rs
+++ b/core/server_common/src/consensus_message.rs
@@ -18,7 +18,7 @@
 use crate::iobuf::{Frozen, Owned};
 use crate::sharding::METADATA_GROUP;
 use iggy_binary_protocol::{
-    Command2, CommitHeader, ConsensusError, ConsensusHeader, DoViewChangeHeader,
+    Command, CommitHeader, ConsensusError, ConsensusHeader, DoViewChangeHeader,
     ForwardLogoutHeader, ForwardLogoutResultHeader, ForwardRegisterHeader,
     ForwardRegisterResultHeader, GenericHeader, Operation, PrepareHeader, PrepareOkHeader,
     RepairPrepareHeader, RepairRangeReplyHeader, RequestHeader, RequestPreparesHeader,
@@ -228,7 +228,7 @@
         if self.total_len() < size_of::<T>() {
             return Err(ConsensusError::InvalidCommand {
                 expected: T::COMMAND,
-                found: Command2::Reserved,
+                found: Command::Reserved,
             });
         }
 
@@ -267,7 +267,7 @@
         if self.total_len() < size_of::<T>() {
             return Err(ConsensusError::InvalidCommand {
                 expected: T::COMMAND,
-                found: Command2::Reserved,
+                found: Command::Reserved,
             });
         }
 
@@ -472,7 +472,7 @@
         if bytes.len() < size_of::<H>() {
             return Err(ConsensusError::InvalidCommand {
                 expected: H::COMMAND,
-                found: Command2::Reserved,
+                found: Command::Reserved,
             });
         }
 
@@ -487,14 +487,14 @@
         if (header.size() as usize) < size_of::<H>() {
             return Err(ConsensusError::InvalidCommand {
                 expected: H::COMMAND,
-                found: Command2::Reserved,
+                found: Command::Reserved,
             });
         }
 
         if bytes.len() < header.size() as usize {
             return Err(ConsensusError::InvalidCommand {
                 expected: H::COMMAND,
-                found: Command2::Reserved,
+                found: Command::Reserved,
             });
         }
 
@@ -512,14 +512,14 @@
         let Some(first) = fragments.first() else {
             return Err(ConsensusError::InvalidCommand {
                 expected: H::COMMAND,
-                found: Command2::Reserved,
+                found: Command::Reserved,
             });
         };
 
         if first.len() < size_of::<H>() {
             return Err(ConsensusError::InvalidCommand {
                 expected: H::COMMAND,
-                found: Command2::Reserved,
+                found: Command::Reserved,
             });
         }
 
@@ -532,7 +532,7 @@
         if (header.size() as usize) < size_of::<H>() {
             return Err(ConsensusError::InvalidCommand {
                 expected: H::COMMAND,
-                found: Command2::Reserved,
+                found: Command::Reserved,
             });
         }
 
@@ -540,7 +540,7 @@
         if total_len < header.size() as usize {
             return Err(ConsensusError::InvalidCommand {
                 expected: H::COMMAND,
-                found: Command2::Reserved,
+                found: Command::Reserved,
             });
         }
 
@@ -699,7 +699,7 @@
     }
 
     #[must_use]
-    pub fn command(&self) -> Command2 {
+    pub fn command(&self) -> Command {
         match self {
             Self::Request(message) => message.header().command,
             Self::Prepare(message) => message.header().command,
@@ -788,66 +788,64 @@
         let command = value.as_generic().header().command;
 
         match command {
-            Command2::Prepare => Ok(Self::Prepare(value.try_into_typed::<PrepareHeader>()?)),
-            Command2::Request => Ok(Self::Request(
+            Command::Prepare => Ok(Self::Prepare(value.try_into_typed::<PrepareHeader>()?)),
+            Command::Request => Ok(Self::Request(
                 value.try_into_typed::<RoutedRequestHeader>()?,
             )),
-            Command2::PrepareOk => Ok(Self::PrepareOk(value.try_into_typed::<PrepareOkHeader>()?)),
-            Command2::StartViewChange => Ok(Self::StartViewChange(
+            Command::PrepareOk => Ok(Self::PrepareOk(value.try_into_typed::<PrepareOkHeader>()?)),
+            Command::StartViewChange => Ok(Self::StartViewChange(
                 value.try_into_typed::<StartViewChangeHeader>()?,
             )),
-            Command2::DoViewChange => Ok(Self::DoViewChange(
+            Command::DoViewChange => Ok(Self::DoViewChange(
                 value.try_into_typed::<DoViewChangeHeader>()?,
             )),
-            Command2::StartView => Ok(Self::StartView(value.try_into_typed::<StartViewHeader>()?)),
-            Command2::Commit => Ok(Self::Commit(value.try_into_typed::<CommitHeader>()?)),
-            Command2::RequestStartView => Ok(Self::RequestStartView(
+            Command::StartView => Ok(Self::StartView(value.try_into_typed::<StartViewHeader>()?)),
+            Command::Commit => Ok(Self::Commit(value.try_into_typed::<CommitHeader>()?)),
+            Command::RequestStartView => Ok(Self::RequestStartView(
                 value.try_into_typed::<RequestStartViewHeader>()?,
             )),
-            Command2::RequestPrepares => Ok(Self::RequestPrepares(
+            Command::RequestPrepares => Ok(Self::RequestPrepares(
                 value.try_into_typed::<RequestPreparesHeader>()?,
             )),
             // A repaired prepare is a stored PrepareHeader frame whose command
             // byte was rewritten; typed validation would reject the byte, so
             // parse through the generic backing and trust the prepare-shaped
             // layout the way the journal that produced it did.
-            Command2::RepairPrepare => Ok(Self::RepairPrepare(
+            Command::RepairPrepare => Ok(Self::RepairPrepare(
                 value.try_into_typed::<RepairPrepareHeader>()?,
             )),
-            Command2::RepairDone | Command2::RangeEvicted => Ok(Self::RepairRangeReply(
+            Command::RepairDone | Command::RangeEvicted => Ok(Self::RepairRangeReply(
                 value.try_into_typed::<RepairRangeReplyHeader>()?,
             )),
-            Command2::RequestStateTransfer => Ok(Self::RequestStateTransfer(
+            Command::RequestStateTransfer => Ok(Self::RequestStateTransfer(
                 value.try_into_typed::<RequestStateTransferHeader>()?,
             )),
-            Command2::StateTransferTarget => Ok(Self::StateTransferTarget(
+            Command::StateTransferTarget => Ok(Self::StateTransferTarget(
                 value.try_into_typed::<StateTransferTargetHeader>()?,
             )),
-            Command2::RequestStateChunk => Ok(Self::RequestStateChunk(
+            Command::RequestStateChunk => Ok(Self::RequestStateChunk(
                 value.try_into_typed::<RequestStateChunkHeader>()?,
             )),
-            Command2::StateChunk => Ok(Self::StateChunk(
+            Command::StateChunk => Ok(Self::StateChunk(
                 value.try_into_typed::<StateChunkHeader>()?,
             )),
-            Command2::ForwardRegister => Ok(Self::ForwardRegister(
+            Command::ForwardRegister => Ok(Self::ForwardRegister(
                 value.try_into_typed::<ForwardRegisterHeader>()?,
             )),
-            Command2::ForwardRegisterResult => Ok(Self::ForwardRegisterResult(
+            Command::ForwardRegisterResult => Ok(Self::ForwardRegisterResult(
                 value.try_into_typed::<ForwardRegisterResultHeader>()?,
             )),
-            Command2::ForwardLogout => Ok(Self::ForwardLogout(
+            Command::ForwardLogout => Ok(Self::ForwardLogout(
                 value.try_into_typed::<ForwardLogoutHeader>()?,
             )),
-            Command2::ForwardLogoutResult => Ok(Self::ForwardLogoutResult(
+            Command::ForwardLogoutResult => Ok(Self::ForwardLogoutResult(
                 value.try_into_typed::<ForwardLogoutResultHeader>()?,
             )),
             // Reply / Eviction are server-to-client frames; they do not
             // appear on the inbound dispatch path.
-            Command2::Reply | Command2::Eviction => {
-                Err(ConsensusError::ClientBoundCommand(command))
-            }
+            Command::Reply | Command::Eviction => Err(ConsensusError::ClientBoundCommand(command)),
             other => Err(ConsensusError::InvalidCommand {
-                expected: Command2::Reserved,
+                expected: Command::Reserved,
                 found: other,
             }),
         }
@@ -872,11 +870,11 @@
     const REQUEST_SESSION_OFF: usize = std::mem::offset_of!(RoutedRequestHeader, session);
     const REQUEST_REQUEST_OFF: usize = std::mem::offset_of!(RoutedRequestHeader, request);
 
-    fn header_bytes(command: Command2, size: u32) -> Owned<MESSAGE_ALIGN> {
+    fn header_bytes(command: Command, size: u32) -> Owned<MESSAGE_ALIGN> {
         header_bytes_sized(command, size, 256)
     }
 
-    fn header_bytes_sized(command: Command2, size: u32, buffer_len: usize) -> Owned<MESSAGE_ALIGN> {
+    fn header_bytes_sized(command: Command, size: u32, buffer_len: usize) -> Owned<MESSAGE_ALIGN> {
         let mut o = Owned::<MESSAGE_ALIGN>::zeroed(buffer_len);
         {
             let buf = o.as_mut_slice();
@@ -915,7 +913,7 @@
         {
             let buf = owned.as_mut_slice();
             buf[SIZE_OFF..SIZE_OFF + 4].copy_from_slice(&(DVC_SIZE as u32).to_le_bytes());
-            buf[COMMAND_OFF] = Command2::DoViewChange as u8;
+            buf[COMMAND_OFF] = Command::DoViewChange as u8;
             seal_header_bytes(buf);
         }
         owned
@@ -960,14 +958,14 @@
     #[test]
     fn probe_and_repair_commands_round_trip_into_bag() {
         for command in [
-            Command2::RequestStartView,
-            Command2::RequestPrepares,
-            Command2::RepairPrepare,
-            Command2::RepairDone,
-            Command2::RangeEvicted,
+            Command::RequestStartView,
+            Command::RequestPrepares,
+            Command::RepairPrepare,
+            Command::RepairDone,
+            Command::RangeEvicted,
         ] {
             let mut owned = header_bytes(command, 256);
-            if command == Command2::RequestPrepares {
+            if command == Command::RequestPrepares {
                 // validate() demands a non-empty 1-based range.
                 const FROM_OP_OFF: usize =
                     std::mem::offset_of!(iggy_binary_protocol::RequestPreparesHeader, from_op);
@@ -985,12 +983,12 @@
                 .unwrap_or_else(|e| panic!("{command:?} failed bag parse: {e}"));
             let routed = matches!(
                 (&bag, command),
-                (MessageBag::RequestStartView(_), Command2::RequestStartView)
-                    | (MessageBag::RequestPrepares(_), Command2::RequestPrepares)
-                    | (MessageBag::RepairPrepare(_), Command2::RepairPrepare)
+                (MessageBag::RequestStartView(_), Command::RequestStartView)
+                    | (MessageBag::RequestPrepares(_), Command::RequestPrepares)
+                    | (MessageBag::RepairPrepare(_), Command::RepairPrepare)
                     | (
                         MessageBag::RepairRangeReply(_),
-                        Command2::RepairDone | Command2::RangeEvicted
+                        Command::RepairDone | Command::RangeEvicted
                     )
             );
             assert!(routed, "{command:?} parsed into the wrong bag variant");
@@ -1002,7 +1000,7 @@
     fn forward_logout_commands_round_trip_into_bag() {
         let forward = Message::<ForwardLogoutHeader>::new(HEADER_SIZE).transmute_header(
             |_, header: &mut ForwardLogoutHeader| {
-                header.command = Command2::ForwardLogout;
+                header.command = Command::ForwardLogout;
                 header.size = HEADER_SIZE as u32;
                 header.client = 7;
                 header.nonce = 8;
@@ -1013,7 +1011,7 @@
         );
         let result = Message::<ForwardLogoutResultHeader>::new(HEADER_SIZE).transmute_header(
             |_, header: &mut ForwardLogoutResultHeader| {
-                header.command = Command2::ForwardLogoutResult;
+                header.command = Command::ForwardLogoutResult;
                 header.size = HEADER_SIZE as u32;
                 header.client = 7;
                 header.nonce = 8;
@@ -1028,8 +1026,8 @@
             MessageBag::try_from(result.into_generic()).expect("parse ForwardLogoutResult");
         assert!(matches!(forward, MessageBag::ForwardLogout(_)));
         assert!(matches!(result, MessageBag::ForwardLogoutResult(_)));
-        assert_eq!(forward.command(), Command2::ForwardLogout);
-        assert_eq!(result.command(), Command2::ForwardLogoutResult);
+        assert_eq!(forward.command(), Command::ForwardLogout);
+        assert_eq!(result.command(), Command::ForwardLogoutResult);
         assert_eq!(forward.operation(), Operation::Reserved);
         assert_eq!(result.size(), HEADER_SIZE as u32);
     }
@@ -1054,7 +1052,7 @@
     #[test]
     fn try_from_owned_invalid_bit_pattern_returns_err() {
         let mut owned = Owned::<MESSAGE_ALIGN>::zeroed(256);
-        owned.as_mut_slice()[COMMAND_OFF] = 99; // outside Command2's discriminant range
+        owned.as_mut_slice()[COMMAND_OFF] = 99; // outside Command's discriminant range
         let result = Message::<RoutedRequestHeader>::try_from(owned);
         assert!(matches!(result, Err(ConsensusError::InvalidBitPattern)));
     }
@@ -1068,7 +1066,7 @@
         // "buffer shorter than `size_of::<H>`" case currently surface as
         // the same `InvalidCommand` variant; promoting them to distinct
         // `ConsensusError` variants is a separate hardening pass.)
-        let owned = header_bytes(Command2::Request, 999);
+        let owned = header_bytes(Command::Request, 999);
         // header_bytes already produces a 256-byte buffer; size=999 > 256,
         // so try_from rejects via `bytes.len() < header.size()`.
         let result = Message::<RoutedRequestHeader>::try_from(owned);
@@ -1082,7 +1080,7 @@
         // buffer-length check passes). Guards the `[size_of::<H>()..size]`
         // underflow at every downstream call site.
         let owned = header_bytes(
-            Command2::Request,
+            Command::Request,
             size_of::<RoutedRequestHeader>() as u32 - 1,
         );
         let result = Message::<RoutedRequestHeader>::try_from(owned);
@@ -1093,10 +1091,10 @@
 
     #[test]
     fn as_generic_view_reads_command_byte() {
-        let owned = header_bytes(Command2::Request, 256);
+        let owned = header_bytes(Command::Request, 256);
         let typed = Message::<RoutedRequestHeader>::try_from(owned).expect("valid");
         let generic = typed.as_generic();
-        assert_eq!(generic.header().command, Command2::Request);
+        assert_eq!(generic.header().command, Command::Request);
         assert_eq!(generic.total_len(), 256);
     }
 
@@ -1108,7 +1106,7 @@
         // header's `size`, never the buffer that happens to hold it.
         const BODY: [u8; 4] = [1, 2, 3, 4];
         let frame_size = size_of::<GenericHeader>() + BODY.len();
-        let mut owned = header_bytes_sized(Command2::Prepare, frame_size as u32, 512);
+        let mut owned = header_bytes_sized(Command::Prepare, frame_size as u32, 512);
         owned.as_mut_slice()[size_of::<GenericHeader>()..frame_size].copy_from_slice(&BODY);
 
         let message = Message::<GenericHeader>::try_from(owned).expect("valid generic");
@@ -1119,7 +1117,7 @@
     fn body_is_empty_for_a_header_only_frame() {
         // Header-only commands go through the same accessor, so `size` equal to the
         // header must yield an empty slice rather than an inverted-range panic.
-        let owned = header_bytes_sized(Command2::Prepare, size_of::<GenericHeader>() as u32, 512);
+        let owned = header_bytes_sized(Command::Prepare, size_of::<GenericHeader>() as u32, 512);
         let message = Message::<GenericHeader>::try_from(owned).expect("valid generic");
         assert!(message.body().is_empty());
     }
@@ -1130,14 +1128,14 @@
     fn try_as_typed_command_mismatch_returns_err_without_unsafe_cast() {
         // bytes are a valid Prepare; asking for RoutedRequestHeader must fail
         // *before* the unsafe ptr-cast inside try_as_typed.
-        let owned = header_bytes(Command2::Prepare, 256);
+        let owned = header_bytes(Command::Prepare, 256);
         let generic = Message::<GenericHeader>::try_from(owned).expect("valid");
         let result = generic.try_as_typed::<RoutedRequestHeader>();
         assert!(matches!(
             result,
             Err(ConsensusError::InvalidCommand {
-                expected: Command2::Request,
-                found: Command2::Prepare,
+                expected: Command::Request,
+                found: Command::Prepare,
             })
         ));
     }
@@ -1146,7 +1144,7 @@
     fn try_as_typed_invalid_validation_returns_err() {
         // `RequestHeader::validate` rejects operation=Register with non-zero
         // session; the routed shape shares the same field rules.
-        let mut owned = header_bytes(Command2::Request, 256);
+        let mut owned = header_bytes(Command::Request, 256);
         {
             let buf = owned.as_mut_slice();
             buf[REQUEST_OPERATION_OFF] = Operation::Register as u8;
@@ -1161,21 +1159,21 @@
 
     #[test]
     fn try_into_typed_command_mismatch_returns_err() {
-        let owned = header_bytes(Command2::Prepare, 256);
+        let owned = header_bytes(Command::Prepare, 256);
         let generic = Message::<GenericHeader>::try_from(owned).expect("valid");
         let result = generic.try_into_typed::<RoutedRequestHeader>();
         assert!(matches!(
             result,
             Err(ConsensusError::InvalidCommand {
-                expected: Command2::Request,
-                found: Command2::Prepare,
+                expected: Command::Request,
+                found: Command::Prepare,
             })
         ));
     }
 
     // MessageBag dispatch: 7 unsafe `from_backing_unchecked` arms
 
-    fn dispatch(command: Command2, size: u32) -> Result<MessageBag, ConsensusError> {
+    fn dispatch(command: Command, size: u32) -> Result<MessageBag, ConsensusError> {
         let owned = header_bytes(command, size);
         let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
         MessageBag::try_from(generic)
@@ -1183,8 +1181,8 @@
 
     #[test]
     fn messagebag_dispatch_unsupported_command_returns_err() {
-        // Ping is a valid Command2 bit pattern but is not a MessageBag variant.
-        let owned = header_bytes(Command2::Ping, 256);
+        // Ping is a valid Command bit pattern but is not a MessageBag variant.
+        let owned = header_bytes(Command::Ping, 256);
         let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
         let result = MessageBag::try_from(generic);
         assert!(matches!(result, Err(ConsensusError::InvalidCommand { .. })));
@@ -1193,13 +1191,13 @@
     #[test]
     fn messagebag_command_method_round_trips() {
         for cmd in [
-            Command2::Request,
-            Command2::Prepare,
-            Command2::PrepareOk,
-            Command2::StartViewChange,
-            Command2::DoViewChange,
-            Command2::StartView,
-            Command2::Commit,
+            Command::Request,
+            Command::Prepare,
+            Command::PrepareOk,
+            Command::StartViewChange,
+            Command::DoViewChange,
+            Command::StartView,
+            Command::Commit,
         ] {
             let bag = dispatch(cmd, 256).expect("dispatch");
             assert_eq!(bag.command(), cmd, "round-trip for {cmd:?}");
@@ -1217,7 +1215,7 @@
         // `size` floor, reaching typed dispatch. A size below the header size is
         // now rejected earlier by the floor (see
         // `try_from_owned_size_below_header_size_returns_err`).
-        let owned = header_bytes_sized(Command2::Commit, 300, 512);
+        let owned = header_bytes_sized(Command::Commit, 300, 512);
         let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
         let result = MessageBag::try_from(generic);
         assert!(matches!(
@@ -1229,7 +1227,7 @@
     #[test]
     fn client_wire_decode_of_request_with_invalid_register_session_returns_err() {
         // `RequestHeader::validate` rejects Register with non-zero session.
-        let mut owned = header_bytes(Command2::Request, 256);
+        let mut owned = header_bytes(Command::Request, 256);
         {
             let buf = owned.as_mut_slice();
             buf[REQUEST_OPERATION_OFF] = Operation::Register as u8;
@@ -1266,7 +1264,7 @@
         ];
 
         for (operation, session, request, must_pass) in cases {
-            let mut owned = header_bytes(Command2::Request, 256);
+            let mut owned = header_bytes(Command::Request, 256);
             {
                 let buf = owned.as_mut_slice();
                 buf[REQUEST_OPERATION_OFF] = operation as u8;
@@ -1288,7 +1286,7 @@
     // it, so ingress must reject it before it can reach the preflight.
     #[test]
     fn ingress_validation_rejects_zero_client() {
-        let mut owned = header_bytes(Command2::Request, 256);
+        let mut owned = header_bytes(Command::Request, 256);
         {
             let buf = owned.as_mut_slice();
             buf[REQUEST_OPERATION_OFF] = Operation::CreateStream as u8;
@@ -1307,7 +1305,7 @@
 
     #[test]
     fn request_message_deep_copy_independent() {
-        let owned = header_bytes(Command2::Request, 256);
+        let owned = header_bytes(Command::Request, 256);
         let mut msg = Message::<RoutedRequestHeader>::try_from(owned).expect("valid");
         let copy = msg.deep_copy();
         // Mutate the original's bytes; the deep copy must be untouched.
@@ -1320,14 +1318,14 @@
 
     #[test]
     fn transmute_header_request_to_prepare() {
-        let owned = header_bytes(Command2::Request, 256);
+        let owned = header_bytes(Command::Request, 256);
         let msg = Message::<RoutedRequestHeader>::try_from(owned).expect("valid");
         let prepared: Message<PrepareHeader> =
             msg.transmute_header::<PrepareHeader>(|_old, new| {
-                new.command = Command2::Prepare;
+                new.command = Command::Prepare;
                 new.size = 256;
             });
-        assert_eq!(prepared.header().command, Command2::Prepare);
+        assert_eq!(prepared.header().command, Command::Prepare);
     }
 
     // into_routed: in-place client-wire -> routed retype
@@ -1339,7 +1337,7 @@
     fn into_routed_keeps_reserved_prefix_and_unsets_group() {
         const RESERVED_OFF: usize = std::mem::offset_of!(RequestHeader, reserved);
 
-        let mut owned = header_bytes(Command2::Request, 256);
+        let mut owned = header_bytes(Command::Request, 256);
         {
             let buf = owned.as_mut_slice();
             for (index, byte) in buf[RESERVED_OFF..RESERVED_OFF + 60].iter_mut().enumerate() {
@@ -1367,14 +1365,14 @@
         assert_eq!(header.user_id, client_header.user_id);
     }
 
-    // A peer-wire `Command2::Request` decodes as `RoutedRequestHeader`, so its
+    // A peer-wire `Command::Request` decodes as `RoutedRequestHeader`, so its
     // validate must enforce the client-boundary field rules: a forged
     // `client = 0` frame would otherwise reach the client table's hard assert
     // and abort the metadata primary, and a `Reserved` operation would replay
     // that client's cached register reply.
     #[test]
     fn messagebag_dispatch_rejects_request_with_zero_client() {
-        let mut owned = header_bytes(Command2::Request, 256);
+        let mut owned = header_bytes(Command::Request, 256);
         owned.as_mut_slice()[REQUEST_CLIENT_OFF..REQUEST_CLIENT_OFF + 16]
             .copy_from_slice(&0u128.to_le_bytes());
         let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
@@ -1386,7 +1384,7 @@
 
     #[test]
     fn messagebag_dispatch_rejects_request_with_reserved_operation() {
-        let mut owned = header_bytes(Command2::Request, 256);
+        let mut owned = header_bytes(Command::Request, 256);
         owned.as_mut_slice()[REQUEST_OPERATION_OFF] = Operation::Reserved as u8;
         let generic = Message::<GenericHeader>::try_from(owned).expect("valid generic");
         assert!(matches!(
@@ -1399,11 +1397,11 @@
 
     #[test]
     fn response_backing_single_fragment_roundtrip() {
-        let owned = header_bytes(Command2::Reply, 256);
+        let owned = header_bytes(Command::Reply, 256);
         let frozen: Frozen<MESSAGE_ALIGN> = owned.into();
         let fragments: smallvec::SmallVec<[Frozen<MESSAGE_ALIGN>; 4]> = smallvec![frozen];
         let msg = Message::<ReplyHeader, ResponseBacking>::try_from(fragments).expect("valid");
-        assert_eq!(msg.header().command, Command2::Reply);
+        assert_eq!(msg.header().command, Command::Reply);
         assert_eq!(msg.fragments().len(), 1);
     }
 
@@ -1427,7 +1425,7 @@
     fn response_backing_size_below_header_size_returns_err() {
         // First fragment is full-size, but its `size` field claims less than
         // the header; the floor must reject before any consumer slices a body.
-        let owned = header_bytes(Command2::Reply, size_of::<ReplyHeader>() as u32 - 1);
+        let owned = header_bytes(Command::Reply, size_of::<ReplyHeader>() as u32 - 1);
         let frozen: Frozen<MESSAGE_ALIGN> = owned.into();
         let fragments: smallvec::SmallVec<[Frozen<MESSAGE_ALIGN>; 4]> = smallvec![frozen];
         let result = Message::<ReplyHeader, ResponseBacking>::try_from(fragments);
diff --git a/core/server_common/src/in_flight.rs b/core/server_common/src/in_flight.rs
deleted file mode 100644
index 9466429..0000000
--- a/core/server_common/src/in_flight.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use iggy_common::IggyMessagesBatch;
-
-/// Holds batches that are being written to disk.
-///
-/// During async I/O, messages are transferred from the journal to disk.
-/// This buffer holds frozen (immutable, Arc-backed) copies so consumers
-/// can still read them during the write operation.
-#[derive(Debug, Default)]
-pub struct IggyMessagesBatchSetInFlight {
-    batches: Vec<IggyMessagesBatch>,
-    first_offset: u64,
-    last_offset: u64,
-}
-
-impl IggyMessagesBatchSetInFlight {
-    pub fn is_empty(&self) -> bool {
-        self.batches.is_empty()
-    }
-
-    pub fn first_offset(&self) -> u64 {
-        self.first_offset
-    }
-
-    pub fn last_offset(&self) -> u64 {
-        self.last_offset
-    }
-
-    pub fn set(&mut self, batches: Vec<IggyMessagesBatch>) {
-        if batches.is_empty() {
-            self.clear();
-            return;
-        }
-        self.first_offset = batches.first().and_then(|b| b.first_offset()).unwrap_or(0);
-        self.last_offset = batches.last().and_then(|b| b.last_offset()).unwrap_or(0);
-        self.batches = batches;
-    }
-
-    pub fn clear(&mut self) {
-        self.batches.clear();
-        self.first_offset = 0;
-        self.last_offset = 0;
-    }
-
-    /// Get batches that overlap with the requested offset range.
-    /// Returns only the batches that contain messages within [start_offset, start_offset + count).
-    pub fn get_by_offset(&self, start_offset: u64, count: u32) -> &[IggyMessagesBatch] {
-        if self.is_empty() || start_offset > self.last_offset || count == 0 {
-            return &[];
-        }
-
-        let end_offset = start_offset + count as u64 - 1;
-        if end_offset < self.first_offset {
-            return &[];
-        }
-
-        let start_idx = self
-            .batches
-            .iter()
-            .position(|b| {
-                b.last_offset()
-                    .map(|last| last >= start_offset)
-                    .unwrap_or(false)
-            })
-            .unwrap_or(0);
-
-        let end_idx = self
-            .batches
-            .iter()
-            .rposition(|b| {
-                b.first_offset()
-                    .map(|first| first <= end_offset)
-                    .unwrap_or(false)
-            })
-            .map(|i| i + 1)
-            .unwrap_or(self.batches.len());
-
-        &self.batches[start_idx..end_idx]
-    }
-
-    pub fn batches(&self) -> &[IggyMessagesBatch] {
-        &self.batches
-    }
-}
diff --git a/core/server_common/src/indexes_mut.rs b/core/server_common/src/indexes_mut.rs
deleted file mode 100644
index 394b234..0000000
--- a/core/server_common/src/indexes_mut.rs
+++ /dev/null
@@ -1,388 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use crate::PooledBuffer;
-use iggy_common::{INDEX_SIZE, IggyIndexView, IggyIndexes};
-use std::fmt;
-use std::ops::{Deref, Index as StdIndex};
-
-/// A container for binary-encoded index data.
-/// Optimized for efficient storage and I/O operations.
-#[derive(Default)]
-pub struct IggyIndexesMut {
-    buffer: PooledBuffer,
-    saved_count: u32,
-    base_position: u32,
-}
-
-impl IggyIndexesMut {
-    /// Creates a new empty container
-    pub fn empty() -> Self {
-        Self {
-            buffer: PooledBuffer::empty(),
-            saved_count: 0,
-            base_position: 0,
-        }
-    }
-
-    /// Creates indexes from bytes
-    pub fn from_bytes(indexes: PooledBuffer, base_position: u32) -> Self {
-        Self {
-            buffer: indexes,
-            saved_count: 0,
-            base_position,
-        }
-    }
-
-    /// Decompose the container into its components
-    pub fn decompose(mut self) -> (u32, PooledBuffer) {
-        let base_position = self.base_position;
-        let buffer = std::mem::take(&mut self.buffer);
-        (base_position, buffer)
-    }
-
-    /// Freezes the indexes buffer, converting to an immutable `IggyIndexes`.
-    ///
-    /// The returned `IggyIndexes` uses Arc-backed `Bytes`, allowing cheap clones.
-    pub fn freeze(&mut self) -> IggyIndexes {
-        let base_position = self.base_position;
-        let buffer = self.buffer.freeze();
-        self.saved_count = 0;
-        self.base_position = 0;
-        IggyIndexes::new(buffer, base_position)
-    }
-
-    /// Gets the size of all indexes messages
-    pub fn messages_size(&self) -> u32 {
-        self.last_position() - self.base_position
-    }
-
-    /// Gets the base position of the indexes
-    pub fn base_position(&self) -> u32 {
-        self.base_position
-    }
-
-    /// Sets the base position of the indexes
-    pub fn set_base_position(&mut self, base_position: u32) {
-        self.base_position = base_position;
-    }
-
-    /// Helper method to get the last index position
-    pub fn last_position(&self) -> u32 {
-        self.get(self.count() - 1)
-            .map(|idx| idx.position())
-            .unwrap_or(0)
-    }
-
-    /// Creates a new container with the specified capacity
-    pub fn with_capacity(capacity: usize, base_position: u32) -> Self {
-        Self {
-            buffer: PooledBuffer::with_capacity(capacity * INDEX_SIZE),
-            saved_count: 0,
-            base_position,
-        }
-    }
-
-    /// Gets the capacity of the buffer
-    pub fn capacity(&self) -> usize {
-        self.buffer.capacity()
-    }
-
-    /// Inserts a new index at the end of buffer
-    pub fn insert(&mut self, offset: u32, position: u32, timestamp: u64) {
-        self.buffer.put_u32_le(offset);
-        self.buffer.put_u32_le(position);
-        self.buffer.put_u64_le(timestamp);
-    }
-
-    /// Appends another slice of indexes to this one.
-    pub fn append_slice(&mut self, other: &[u8]) {
-        self.buffer.put_slice(other);
-    }
-
-    /// Gets the number of indexes in the container
-    pub fn count(&self) -> u32 {
-        self.buffer.len() as u32 / INDEX_SIZE as u32
-    }
-
-    /// Checks if the container is empty
-    pub fn is_empty(&self) -> bool {
-        self.count() == 0
-    }
-
-    /// Gets the size of the buffer in bytes
-    pub fn size(&self) -> u32 {
-        self.buffer.len() as u32
-    }
-
-    /// Gets a view of the Index at the specified index
-    pub fn get(&self, index: u32) -> Option<IggyIndexView<'_>> {
-        if index >= self.count() {
-            return None;
-        }
-
-        let start = index as usize * INDEX_SIZE;
-        let end = start + INDEX_SIZE;
-
-        if end <= self.buffer.len() {
-            Some(IggyIndexView::new(&self.buffer[start..end]))
-        } else {
-            None
-        }
-    }
-
-    // Set the offset at the given index position
-    pub fn set_offset_at(&mut self, index: u32, offset: u32) {
-        let pos = index as usize * INDEX_SIZE;
-        self.buffer[pos..pos + 4].copy_from_slice(&offset.to_le_bytes());
-    }
-
-    // Set the position at the given index
-    pub fn set_position_at(&mut self, index: u32, position: u32) {
-        let pos = (index as usize * INDEX_SIZE) + 4;
-        self.buffer[pos..pos + 4].copy_from_slice(&position.to_le_bytes());
-    }
-
-    // Set the timestamp at the given index
-    pub fn set_timestamp_at(&mut self, index: u32, timestamp: u64) {
-        let pos = (index as usize * INDEX_SIZE) + 8;
-        self.buffer[pos..pos + 8].copy_from_slice(&timestamp.to_le_bytes());
-    }
-
-    /// Gets a last index
-    pub fn last(&self) -> Option<IggyIndexView<'_>> {
-        if self.count() == 0 {
-            return None;
-        }
-
-        // Bound to exactly one entry rather than to the buffer end: a file
-        // whose length is not a whole multiple of INDEX_SIZE (e.g. a 24-byte
-        // server sparse index read back through this 16-byte reader on a
-        // mixed-format recovery) would otherwise hand an oversized slice to the
-        // view and trip its length assertion.
-        let start = (self.count() - 1) as usize * INDEX_SIZE;
-        Some(IggyIndexView::new(&self.buffer[start..start + INDEX_SIZE]))
-    }
-
-    /// Finds an index by timestamp using binary search
-    /// If an exact match isn't found, returns the index with the nearest timestamp
-    /// that is greater than or equal to the requested timestamp
-    pub fn find_by_timestamp(&self, timestamp: u64) -> Option<IggyIndexView<'_>> {
-        if self.count() == 0 {
-            return None;
-        }
-
-        let first_idx = self.get(0)?;
-        if timestamp <= first_idx.timestamp() {
-            return Some(first_idx);
-        }
-
-        let last_saved_idx = self.get(self.count() - 1)?;
-        if timestamp > last_saved_idx.timestamp() {
-            return None;
-        }
-
-        let mut left = 0;
-        let mut right = self.count() as isize - 1;
-        let mut result: Option<IggyIndexView<'_>> = None;
-
-        while left <= right {
-            let mid = left + (right - left) / 2;
-            let view = self.get(mid as u32).unwrap();
-            let current_timestamp = view.timestamp();
-
-            match current_timestamp.cmp(&timestamp) {
-                std::cmp::Ordering::Equal => {
-                    result = Some(view);
-                    right = mid - 1;
-                }
-                std::cmp::Ordering::Less => {
-                    left = mid + 1;
-                }
-                std::cmp::Ordering::Greater => {
-                    result = Some(view);
-                    right = mid - 1;
-                }
-            }
-        }
-
-        result
-    }
-
-    /// Clears the container, removing all indexes but preserving already allocated buffer capacity
-    pub fn clear(&mut self) {
-        self.saved_count = 0;
-        self.buffer.clear();
-    }
-
-    /// Gets the unsaved part of the index buffer
-    pub fn unsaved_slice(&self) -> PooledBuffer {
-        let start_pos = self.saved_count as usize * INDEX_SIZE;
-        // TODO: Dunno how to handle this better, maybe we should have a `split` method,
-        // That splits the underlying Indexes buffer into two parts
-        // saved on disk and not saved yet.
-        PooledBuffer::from(&self.buffer[start_pos..])
-    }
-
-    /// Mark all indexes as saved to disk
-    pub fn mark_saved(&mut self) {
-        self.saved_count = self.count();
-    }
-
-    /// Slices the container to return a view of a specific range of indexes
-    pub fn slice_by_offset(
-        &self,
-        relative_start_offset: u32,
-        count: u32,
-    ) -> Option<IggyIndexesMut> {
-        let available_count = self.count().saturating_sub(relative_start_offset);
-        let actual_count = std::cmp::min(count, available_count);
-
-        if actual_count == 0 || relative_start_offset >= self.count() {
-            return None;
-        }
-
-        let end_pos = relative_start_offset + actual_count;
-
-        let start_byte = relative_start_offset as usize * INDEX_SIZE;
-        let end_byte = end_pos as usize * INDEX_SIZE;
-        let slice = PooledBuffer::from(&self.buffer[start_byte..end_byte]);
-
-        if relative_start_offset == 0 {
-            Some(IggyIndexesMut::from_bytes(slice, self.base_position))
-        } else {
-            let position_offset: u32 = self.get(relative_start_offset - 1).unwrap().position();
-            Some(IggyIndexesMut::from_bytes(slice, position_offset))
-        }
-    }
-
-    /// Loads indexes from cache based on timestamp
-    pub fn slice_by_timestamp(&self, timestamp: u64, count: u32) -> Option<IggyIndexesMut> {
-        if self.count() == 0 {
-            return None;
-        }
-
-        let start_index_pos = self.binary_search_position_for_timestamp_sync(timestamp)?;
-
-        let available_count = self.count().saturating_sub(start_index_pos);
-        let actual_count = std::cmp::min(count, available_count);
-
-        if actual_count == 0 {
-            return None;
-        }
-
-        let end_pos = start_index_pos + actual_count;
-
-        let start_byte = start_index_pos as usize * INDEX_SIZE;
-        let end_byte = end_pos as usize * INDEX_SIZE;
-        let slice = PooledBuffer::from(&self.buffer[start_byte..end_byte]);
-
-        let base_position = if start_index_pos > 0 {
-            self.get(start_index_pos - 1).unwrap().position()
-        } else {
-            0
-        };
-
-        Some(IggyIndexesMut::from_bytes(slice, base_position))
-    }
-
-    /// Find the position of the index with timestamp closest to (but not exceeding) the target
-    fn binary_search_position_for_timestamp_sync(&self, target_timestamp: u64) -> Option<u32> {
-        if self.count() == 0 {
-            return None;
-        }
-
-        let last_index = self.get(self.count() - 1)?;
-        if target_timestamp > last_index.timestamp() {
-            return Some(self.count() - 1);
-        }
-
-        let first_index = self.get(0)?;
-        if target_timestamp <= first_index.timestamp() {
-            return Some(0);
-        }
-
-        let mut low = 0;
-        let mut high = self.count() - 1;
-
-        while low <= high {
-            let mid = low + (high - low) / 2;
-            let mid_index = self.get(mid)?;
-            let mid_timestamp = mid_index.timestamp();
-
-            match mid_timestamp.cmp(&target_timestamp) {
-                std::cmp::Ordering::Equal => return Some(mid),
-                std::cmp::Ordering::Less => low = mid + 1,
-                std::cmp::Ordering::Greater => {
-                    if mid == 0 {
-                        break;
-                    }
-                    high = mid - 1;
-                }
-            }
-        }
-
-        Some(low)
-    }
-}
-
-impl StdIndex<usize> for IggyIndexesMut {
-    type Output = [u8];
-
-    fn index(&self, index: usize) -> &Self::Output {
-        let start = index * INDEX_SIZE;
-        let end = start + INDEX_SIZE;
-        &self.buffer[start..end]
-    }
-}
-
-impl Deref for IggyIndexesMut {
-    type Target = [u8];
-
-    fn deref(&self) -> &Self::Target {
-        &self.buffer
-    }
-}
-
-impl fmt::Debug for IggyIndexesMut {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let count = self.count();
-
-        if count == 0 {
-            return write!(f, "IggyIndexesMut {{ count: 0, indexes: [] }}");
-        }
-
-        writeln!(f, "IggyIndexesMut {{")?;
-        writeln!(f, "    count: {count},")?;
-        writeln!(f, "    indexes: [")?;
-
-        for i in 0..count {
-            if let Some(index) = self.get(i) {
-                writeln!(
-                    f,
-                    "        {{ offset: {}, position: {}, timestamp: {} }},",
-                    index.offset(),
-                    index.position(),
-                    index.timestamp()
-                )?;
-            }
-        }
-
-        writeln!(f, "    ]")?;
-        write!(f, "}}")
-    }
-}
diff --git a/core/server_common/src/lib.rs b/core/server_common/src/lib.rs
index 0e94ce3..2aca6f2 100644
--- a/core/server_common/src/lib.rs
+++ b/core/server_common/src/lib.rs
@@ -23,8 +23,6 @@
 pub mod diagnostics;
 pub mod executor;
 pub mod fs_utils;
-mod in_flight;
-mod indexes_mut;
 // TODO(hubcio): iobuf was relocated verbatim from `core/binary_protocol/src/consensus/iobuf.rs`
 // during the sans-io split. Its implementation is intentionally untouched; the
 // lints below predate the move and are tracked as tech debt for a 0.9.x cleanup pass.
@@ -41,10 +39,8 @@
 pub mod iobuf;
 pub mod log;
 mod memory_pool;
-mod messages_batch_mut;
-mod messages_batch_set;
 mod segment_storage;
-pub mod send_messages2;
+pub mod send_messages;
 pub mod sharding;
 
 pub use bootstrap::create_directories;
@@ -55,11 +51,7 @@
     MutableBacking, RequestBacking, RequestBackingKind, ResponseBacking, ResponseBackingKind,
 };
 pub use executor::create_shard_executor;
-pub use in_flight::IggyMessagesBatchSetInFlight;
-pub use indexes_mut::IggyIndexesMut;
 pub use memory_pool::{MEMORY_POOL, MemoryPool, MemoryPoolConfigOther, memory_pool};
-pub use messages_batch_mut::IggyMessagesBatchMut;
-pub use messages_batch_set::IggyMessagesBatchSet;
 pub use segment_storage::{
     IndexReader, IndexWriter, MessagesReader, MessagesWriter, SegmentStorage,
 };
diff --git a/core/server_common/src/messages_batch_mut.rs b/core/server_common/src/messages_batch_mut.rs
deleted file mode 100644
index bf7bbd1..0000000
--- a/core/server_common/src/messages_batch_mut.rs
+++ /dev/null
@@ -1,616 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use crate::PooledBuffer;
-use crate::indexes_mut::IggyIndexesMut;
-use iggy_common::{
-    IGGY_MESSAGE_HEADER_SIZE, INDEX_SIZE, IggyByteSize, IggyError, IggyIndexView, IggyMessage,
-    IggyMessageBoundaries, IggyMessageView, IggyMessageViewIterator, IggyMessageViewMutIterator,
-    IggyMessagesBatch, MAX_PAYLOAD_SIZE, MAX_USER_HEADERS_SIZE, Sizeable, Validatable,
-};
-use std::ops::Index;
-use tracing::error;
-
-/// A container for mutable messages that are being prepared for persistence.
-///
-/// `IggyMessagesBatchMut` holds both the raw message data in a `BytesMut` buffer
-/// and the corresponding index data that allows for efficient message lookup.
-#[derive(Debug, Default)]
-pub struct IggyMessagesBatchMut {
-    /// The index data for all messages in the buffer
-    indexes: IggyIndexesMut,
-
-    /// The buffer containing the serialized message data
-    messages: PooledBuffer,
-}
-
-impl Sizeable for IggyMessagesBatchMut {
-    fn get_size_bytes(&self) -> IggyByteSize {
-        IggyByteSize::from(self.messages.len() as u64)
-    }
-}
-
-impl IggyMessagesBatchMut {
-    /// Creates a new empty messages container
-    pub fn empty() -> Self {
-        Self {
-            indexes: IggyIndexesMut::empty(),
-            messages: PooledBuffer::empty(),
-        }
-    }
-
-    /// Creates a new messages container from existing index and message buffers.
-    ///
-    /// # Arguments
-    ///
-    /// * `indexes` - Preprocessed index data
-    /// * `messages` - Serialized message data
-    pub fn from_indexes_and_messages(indexes: IggyIndexesMut, messages: PooledBuffer) -> Self {
-        Self { indexes, messages }
-    }
-
-    /// Creates a new messages container from a slice of IggyMessage objects.
-    ///
-    /// # Note
-    /// This function should be used only for testing purposes,
-    /// because it creates deep copies of the messages.
-    ///
-    /// # Arguments
-    ///
-    /// * `messages` - Slice of message objects to store
-    /// * `messages_size` - Total size of all messages in bytes
-    pub fn from_messages(messages: &[IggyMessage], messages_size: u32) -> Self {
-        let mut messages_buffer = PooledBuffer::with_capacity(messages_size as usize);
-        let mut indexes_buffer = IggyIndexesMut::with_capacity(messages.len(), 0);
-        let mut position = 0;
-
-        for message in messages {
-            let bytes = message.to_bytes();
-            messages_buffer.put_slice(&bytes);
-            position += message.get_size_bytes().as_bytes_u32();
-            indexes_buffer.insert(0, position, 0);
-        }
-
-        Self::from_indexes_and_messages(indexes_buffer, messages_buffer)
-    }
-
-    /// Creates a lending iterator that yields mutable views of messages.
-    pub fn iter_mut(&mut self) -> IggyMessageViewMutIterator<'_> {
-        IggyMessageViewMutIterator::new(&mut self.messages)
-    }
-
-    /// Creates an iterator that yields immutable views of messages.
-    pub fn iter(&self) -> IggyMessageViewIterator<'_> {
-        IggyMessageViewIterator::new_with_boundaries(
-            &self.messages,
-            &self.indexes,
-            self.indexes.base_position(),
-            self.count(),
-        )
-    }
-
-    /// Returns the number of messages in the batch.
-    pub fn count(&self) -> u32 {
-        self.indexes.len() as u32 / INDEX_SIZE as u32
-    }
-
-    /// Returns the total size of all messages in bytes.
-    pub fn size(&self) -> u32 {
-        self.messages.len() as u32
-    }
-
-    /// Returns the raw message bytes as a slice.
-    pub fn as_bytes(&self) -> &[u8] {
-        &self.messages
-    }
-
-    /// Returns the first offset in the batch
-    pub fn first_offset(&self) -> Option<u64> {
-        if self.is_empty() {
-            return None;
-        }
-        Some(IggyMessageView::new(&self.messages).ok()?.header().offset())
-    }
-
-    /// Returns the first timestamp in the batch
-    pub fn first_timestamp(&self) -> Option<u64> {
-        if self.is_empty() {
-            return None;
-        }
-        Some(
-            IggyMessageView::new(&self.messages)
-                .ok()?
-                .header()
-                .timestamp(),
-        )
-    }
-
-    /// Returns the last timestamp in the batch
-    pub fn last_timestamp(&self) -> Option<u64> {
-        if self.is_empty() {
-            return None;
-        }
-
-        let last_index = self.count() as usize - 1;
-        let (start, end) = self.get_message_boundaries(last_index)?;
-        Some(
-            IggyMessageView::new(&self.messages[start..end])
-                .ok()?
-                .header()
-                .timestamp(),
-        )
-    }
-
-    /// Returns the last offset in the batch
-    pub fn last_offset(&self) -> Option<u64> {
-        if self.is_empty() {
-            return None;
-        }
-        let last_index = self.count() as usize - 1;
-        let (start, end) = self.get_message_boundaries(last_index)?;
-        Some(
-            IggyMessageView::new(&self.messages[start..end])
-                .ok()?
-                .header()
-                .offset(),
-        )
-    }
-
-    /// Checks if the batch is empty.
-    pub fn is_empty(&self) -> bool {
-        self.count() == 0
-    }
-
-    /// Return total size of all messages in bytes.
-    pub fn len(&self) -> usize {
-        self.messages.len()
-    }
-
-    /// Decomposes the batch into its constituent parts.
-    pub fn decompose(mut self) -> (IggyIndexesMut, PooledBuffer) {
-        let indexes = std::mem::replace(&mut self.indexes, IggyIndexesMut::empty());
-        let messages = std::mem::take(&mut self.messages);
-
-        (indexes, messages)
-    }
-
-    /// Freezes the batch, converting to an immutable `IggyMessagesBatch`.
-    ///
-    /// The returned batch uses Arc-backed `Bytes`, allowing cheap clones.
-    /// After calling this, the mutable batch becomes empty.
-    pub fn freeze(&mut self) -> IggyMessagesBatch {
-        let count = self.count();
-        let indexes = self.indexes.freeze();
-        let messages = self.messages.freeze();
-        IggyMessagesBatch::new(indexes, messages, count)
-    }
-
-    pub fn take_messages(&mut self) -> PooledBuffer {
-        std::mem::take(&mut self.messages)
-    }
-
-    /// Take the indexes from the batch
-    pub fn take_indexes(&mut self) -> IggyIndexesMut {
-        std::mem::take(&mut self.indexes)
-    }
-
-    /// Borrows the indexes from the batch
-    pub fn indexes(&self) -> &IggyIndexesMut {
-        &self.indexes
-    }
-
-    /// Get message position from the indexes at the given index
-    pub fn position_at(&self, index: u32) -> Option<u32> {
-        self.indexes.get(index).map(|index| index.position())
-    }
-
-    fn boundaries(&self) -> Option<IggyMessageBoundaries<'_>> {
-        IggyMessageBoundaries::new(
-            &self.indexes,
-            self.messages.len(),
-            self.indexes.base_position(),
-            self.count(),
-        )
-    }
-
-    /// Calculates the start position of a message at the given index in the buffer
-    fn message_start_position(&self, index: usize) -> Option<usize> {
-        self.get_message_boundaries(index).map(|(start, _)| start)
-    }
-
-    /// Calculates the end position of a message at the given index in the buffer
-    fn message_end_position(&self, index: usize) -> Option<usize> {
-        self.get_message_boundaries(index).map(|(_, end)| end)
-    }
-
-    /// Returns a contiguous slice (as a new `IggyMessagesBatch`) of up to `count` messages
-    /// whose message headers have an offset greater than or equal to the provided `start_offset`.
-    pub fn slice_by_offset(&self, start_offset: u64, count: u32) -> Option<Self> {
-        if self.is_empty() || count == 0 {
-            return None;
-        }
-
-        let first_offset = self.first_offset()?;
-
-        if start_offset < first_offset {
-            return None;
-        }
-
-        let last_offset = self.last_offset()?;
-        if start_offset > last_offset {
-            return None;
-        }
-
-        let offset_diff = start_offset - first_offset;
-        let first_message_index = offset_diff as usize;
-
-        if first_message_index >= self.count() as usize {
-            return None;
-        }
-
-        self.slice_by_index(first_message_index as u32, count)
-    }
-
-    /// Helper method to slice the batch starting from a specific index
-    fn slice_by_index(&self, start_index: u32, count: u32) -> Option<Self> {
-        if start_index >= self.count() {
-            return None;
-        }
-
-        let last_message_index =
-            std::cmp::min((start_index + count) as usize, self.count() as usize);
-
-        let sub_indexes = self.indexes.slice_by_offset(
-            start_index,
-            (last_message_index - start_index as usize) as u32,
-        )?;
-
-        let first_message_position = self.message_start_position(start_index as usize)?;
-        let last_message_position = self.message_end_position(last_message_index - 1)?;
-
-        // TODO(hubcio): messages from accumulator unfortunately are deep-copied
-        let mut sub_buffer =
-            PooledBuffer::with_capacity(last_message_position - first_message_position);
-        sub_buffer.put_slice(&self.messages[first_message_position..last_message_position]);
-
-        Some(IggyMessagesBatchMut {
-            indexes: sub_indexes,
-            messages: sub_buffer,
-        })
-    }
-
-    /// Returns a contiguous slice (as a new `IggyMessagesBatch`) of up to `count` messages
-    /// whose message headers have a timestamp greater than or equal to the provided `timestamp`.
-    ///
-    /// If no messages meet the criteria, returns `None`.
-    pub fn slice_by_timestamp(&self, timestamp: u64, count: u32) -> Option<Self> {
-        if self.is_empty() || count == 0 {
-            return None;
-        }
-
-        // Use binary search to find the first message with timestamp >= the target
-        let first_message_index = self.binary_search_timestamp(timestamp)?;
-
-        self.slice_by_index(first_message_index, count)
-    }
-
-    /// Find the position of the index with timestamp closest to (but not exceeding) the target
-    fn binary_search_timestamp(&self, target_timestamp: u64) -> Option<u32> {
-        if self.count() == 0 {
-            return None;
-        }
-
-        let last_timestamp = self.get(self.count() as usize - 1)?.header().timestamp();
-        if target_timestamp > last_timestamp {
-            return Some(self.count() - 1);
-        }
-
-        let first_timestamp = self.get(0)?.header().timestamp();
-        if target_timestamp <= first_timestamp {
-            return Some(0);
-        }
-
-        let mut low = 0;
-        let mut high = self.count() - 1;
-
-        while low <= high {
-            let mid = low + (high - low) / 2;
-            let mid_index = self.get(mid as usize)?;
-            let mid_timestamp = mid_index.header().timestamp();
-
-            match mid_timestamp.cmp(&target_timestamp) {
-                std::cmp::Ordering::Equal => return Some(mid),
-                std::cmp::Ordering::Less => low = mid + 1,
-                std::cmp::Ordering::Greater => {
-                    if mid == 0 {
-                        break;
-                    }
-                    high = mid - 1;
-                }
-            }
-        }
-
-        Some(low)
-    }
-
-    /// Validates that all messages have correct checksums and offsets.
-    /// This function should be called after messages have been read from disk.
-    ///
-    /// # Arguments
-    ///
-    /// * `absolute_start_offset` - The absolute offset of the first message in the batch.
-    ///
-    /// # Returns
-    ///
-    /// * `Ok(())` - If all messages have correct checksums and offsets.
-    /// * `Err(IggyError)` - If any message has an invalid checksum or offset.
-    pub fn validate_checksums_and_offsets(
-        &self,
-        absolute_start_offset: u64,
-    ) -> Result<(), IggyError> {
-        for (current_offset, message) in (absolute_start_offset..).zip(self.iter()) {
-            let calculated_checksum = message.calculate_checksum();
-            let actual_checksum = message.header().checksum();
-            let offset = message.header().offset();
-            if offset != current_offset {
-                return Err(IggyError::InvalidOffset(offset));
-            }
-            if calculated_checksum != actual_checksum {
-                return Err(IggyError::InvalidMessageChecksum(
-                    actual_checksum,
-                    calculated_checksum,
-                    offset,
-                ));
-            }
-        }
-        Ok(())
-    }
-
-    /// Gets the byte range for a message at the given index
-    fn get_message_boundaries(&self, index: usize) -> Option<(usize, usize)> {
-        let (start, end) = self.boundaries()?.boundaries(index)?;
-
-        if start > self.messages.len()
-            || end > self.messages.len()
-            || start > end
-            || end - start < IGGY_MESSAGE_HEADER_SIZE
-        {
-            return None;
-        }
-
-        Some((start, end))
-    }
-
-    /// Get the message at the specified index.
-    /// Returns None if the index is out of bounds or the message cannot be found.
-    pub fn get(&self, index: usize) -> Option<IggyMessageView<'_>> {
-        let (start, end) = self.get_message_boundaries(index)?;
-        IggyMessageView::new(&self.messages[start..end]).ok()
-    }
-
-    /// Validates that all messages in batch have correct checksums.
-    pub fn validate_checksums(&self) -> Result<(), IggyError> {
-        for message in self.iter() {
-            let calculated_checksum = message.calculate_checksum();
-            let actual_checksum = message.header().checksum();
-            let offset = message.header().offset();
-            if calculated_checksum != actual_checksum {
-                return Err(IggyError::InvalidMessageChecksum(
-                    actual_checksum,
-                    calculated_checksum,
-                    offset,
-                ));
-            }
-        }
-        Ok(())
-    }
-
-    /// Validates the structure of the indexes (sizes, counts, etc.)
-    fn validate_indexes_structure(&self) -> Result<(), IggyError> {
-        let indexes_count = self.indexes.count();
-        let indexes_size = self.indexes.size();
-
-        if !indexes_size.is_multiple_of(INDEX_SIZE as u32) {
-            error!(
-                "Indexes size {} is not a multiple of index size {}",
-                indexes_size, INDEX_SIZE
-            );
-            return Err(IggyError::InvalidIndexesByteSize(indexes_size));
-        }
-
-        if indexes_count != self.count() {
-            error!(
-                "Indexes count {} does not match messages count {}",
-                indexes_count,
-                self.count()
-            );
-            return Err(IggyError::InvalidIndexesCount(indexes_count, self.count()));
-        }
-
-        Ok(())
-    }
-
-    fn validate_message_contents(&self) -> Result<(), IggyError> {
-        let mut messages_count = 0;
-        let mut messages_size = 0;
-        let mut prev_offset = 0;
-        let mut prev_position = 0;
-
-        for i in 0..self.count() {
-            let index = self.validate_index_at(i)?;
-            let message = self.validate_message_at(i)?;
-
-            if message.header().offset() < prev_offset {
-                error!(
-                    "Offset of previous message: {} is smaller than current message {} at offset {}",
-                    prev_offset,
-                    message.header().offset(),
-                    i
-                );
-                return Err(IggyError::InvalidOffset(message.header().offset()));
-            }
-
-            if index.position() < prev_position {
-                error!(
-                    "Position of previous message: {} is smaller than current message {} at offset {}",
-                    prev_position,
-                    index.position(),
-                    i
-                );
-                return Err(IggyError::CannotReadIndexPosition);
-            }
-
-            prev_offset = message.header().offset();
-            prev_position = index.position();
-            messages_size += message.size();
-            messages_count += 1;
-        }
-
-        let indexes_count = self.indexes.count();
-        if indexes_count != messages_count {
-            error!(
-                "Indexes count {} does not match messages count {}",
-                indexes_count, messages_count
-            );
-            return Err(IggyError::InvalidMessagesCount);
-        }
-
-        if messages_size != self.messages.len() {
-            error!(
-                "Messages size {} does not match messages buffer size {}",
-                messages_size,
-                self.messages.len() as u64
-            );
-            return Err(IggyError::InvalidMessagesSize(
-                messages_size as u32,
-                self.messages.len() as u32,
-            ));
-        }
-
-        Ok(())
-    }
-
-    /// Validates a specific index entry
-    fn validate_index_at(&self, i: u32) -> Result<IggyIndexView<'_>, IggyError> {
-        let index_view = match self.indexes.get(i) {
-            Some(view) => view,
-            None => {
-                error!("Index {} is missing", i);
-                return Err(IggyError::MissingIndex(i));
-            }
-        };
-
-        if index_view.offset() != 0 {
-            error!(
-                "Non-zero offset {} at index: {}, messages coming from network always have offset 0",
-                index_view.offset(),
-                i
-            );
-            return Err(IggyError::NonZeroOffset(index_view.offset() as u64, i));
-        }
-
-        if index_view.timestamp() != 0 {
-            error!(
-                "Non-zero timestamp {} at index: {}, messages coming from network always have timestamp 0",
-                index_view.timestamp(),
-                i
-            );
-            return Err(IggyError::NonZeroTimestamp(index_view.timestamp(), i));
-        }
-
-        Ok(index_view)
-    }
-
-    /// Validates a specific message
-    fn validate_message_at(&self, i: u32) -> Result<IggyMessageView<'_>, IggyError> {
-        let message = match self.get(i as usize) {
-            Some(msg) => msg,
-            None => {
-                error!(
-                    "Message at index {} is missing, or message size is less than minimum message size {} B (header)",
-                    i, IGGY_MESSAGE_HEADER_SIZE
-                );
-                return Err(IggyError::MissingIndex(i));
-            }
-        };
-
-        if message.payload().len() as u32 > MAX_PAYLOAD_SIZE {
-            error!(
-                "Message payload size {} B exceeds maximum payload size {} B",
-                message.payload().len(),
-                MAX_PAYLOAD_SIZE
-            );
-            return Err(IggyError::TooBigMessagePayload);
-        }
-
-        if message.size() < IGGY_MESSAGE_HEADER_SIZE {
-            error!(
-                "Message size {} B is less than minimum message size {} B (header)",
-                message.size(),
-                IGGY_MESSAGE_HEADER_SIZE
-            );
-            return Err(IggyError::TooSmallMessage(
-                message.size() as u32,
-                IGGY_MESSAGE_HEADER_SIZE as u32,
-            ));
-        }
-
-        if let Some(user_headers) = message.user_headers()
-            && user_headers.len() as u32 > MAX_USER_HEADERS_SIZE
-        {
-            error!(
-                "Message user headers size {} B exceeds maximum size {} B",
-                user_headers.len(),
-                MAX_USER_HEADERS_SIZE
-            );
-            return Err(IggyError::TooBigUserHeaders);
-        }
-
-        Ok(message)
-    }
-}
-
-impl Validatable<IggyError> for IggyMessagesBatchMut {
-    fn validate(&self) -> Result<(), IggyError> {
-        if self.is_empty() {
-            return Err(IggyError::InvalidMessagesCount);
-        }
-
-        self.validate_indexes_structure()?;
-        self.validate_message_contents()?;
-        Ok(())
-    }
-}
-
-impl Index<usize> for IggyMessagesBatchMut {
-    type Output = [u8];
-
-    fn index(&self, index: usize) -> &Self::Output {
-        assert!(
-            index < self.count() as usize,
-            "Index out of bounds: the len is {} but the index is {}",
-            self.count(),
-            index
-        );
-
-        let (start, end) = self
-            .get_message_boundaries(index)
-            .expect("Invalid message boundaries");
-
-        &self.messages[start..end]
-    }
-}
diff --git a/core/server_common/src/messages_batch_set.rs b/core/server_common/src/messages_batch_set.rs
deleted file mode 100644
index a10bda3..0000000
--- a/core/server_common/src/messages_batch_set.rs
+++ /dev/null
@@ -1,378 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use crate::{IggyIndexesMut, IggyMessagesBatchMut, PooledBuffer};
-use bytes::Bytes;
-use iggy_common::{
-    IggyByteSize, IggyMessage, IggyMessageView, IggyMessagesBatch, IggyPollMetadata,
-    PolledMessages, Sizeable,
-};
-use std::ops::Index;
-use tracing::trace;
-
-/// A container for multiple IggyMessagesBatch objects
-#[derive(Debug, Default)]
-pub struct IggyMessagesBatchSet {
-    /// The collection of message containers
-    batches: Vec<IggyMessagesBatchMut>,
-    /// Total number of messages across all containers
-    count: u32,
-    /// Total size in bytes across all containers
-    size: u32,
-}
-
-impl IggyMessagesBatchSet {
-    /// Create a new empty batch
-    pub fn empty() -> Self {
-        Self {
-            batches: Vec::new(),
-            count: 0,
-            size: 0,
-        }
-    }
-
-    /// Create a new empty batch set with a specified initial capacity of message containers
-    pub fn with_capacity(capacity: usize) -> Self {
-        Self {
-            batches: Vec::with_capacity(capacity),
-            count: 0,
-            size: 0,
-        }
-    }
-
-    /// Create a batch set from an existing vector of IggyMessages
-    pub fn from_vec(messages: Vec<IggyMessagesBatchMut>) -> Self {
-        let mut batch = Self::with_capacity(messages.len());
-        for msg in messages {
-            batch.add_batch(msg);
-        }
-        batch
-    }
-
-    /// Add another batch of messages to the batch set
-    pub fn add_batch(&mut self, batch: IggyMessagesBatchMut) {
-        self.count += batch.count();
-        self.size += batch.size();
-        self.batches.push(batch);
-    }
-
-    /// Add another batch set of messages to the batch set
-    pub fn add_batch_set(&mut self, mut other_batch_set: IggyMessagesBatchSet) {
-        self.count += other_batch_set.count();
-        self.size += other_batch_set.size();
-        let other_batches = std::mem::take(&mut other_batch_set.batches);
-        self.batches.extend(other_batches);
-    }
-
-    /// Add immutable batches by copying them into mutable form.
-    ///
-    /// This is used when reading from the in-flight buffer (which holds
-    /// frozen/immutable batches) and needs to convert them for the read path.
-    pub fn add_immutable_batches(&mut self, batches: &[IggyMessagesBatch]) {
-        for batch in batches {
-            let mutable_batch = Self::immutable_to_mutable(batch);
-            self.add_batch(mutable_batch);
-        }
-    }
-
-    /// Convert an immutable IggyMessagesBatch to a mutable IggyMessagesBatchMut.
-    ///
-    /// This requires copying the Bytes into a PooledBuffer.
-    fn immutable_to_mutable(batch: &IggyMessagesBatch) -> IggyMessagesBatchMut {
-        let base_position = batch.indexes().base_position();
-        let indexes_buffer = PooledBuffer::from(batch.indexes_slice());
-        let indexes = IggyIndexesMut::from_bytes(indexes_buffer, base_position);
-        let messages = PooledBuffer::from(batch.buffer());
-
-        IggyMessagesBatchMut::from_indexes_and_messages(indexes, messages)
-    }
-
-    /// Extract indexes from all batches in the set
-    pub fn append_indexes_to(&self, target: &mut IggyIndexesMut) {
-        for batch in self.iter() {
-            let indexes = batch.indexes();
-            target.append_slice(indexes);
-        }
-    }
-
-    /// Get the total number of messages in the batch
-    pub fn count(&self) -> u32 {
-        self.count
-    }
-
-    /// Get the total size of all messages in bytes
-    pub fn size(&self) -> u32 {
-        self.size
-    }
-
-    /// Get the number of message containers in the batch
-    pub fn containers_count(&self) -> usize {
-        self.batches.len()
-    }
-
-    /// Check if the batch is empty
-    pub fn is_empty(&self) -> bool {
-        self.batches.is_empty() || self.count == 0
-    }
-
-    /// Get timestamp of first message in first batch
-    pub fn first_timestamp(&self) -> Option<u64> {
-        if self.is_empty() {
-            return None;
-        }
-        self.batches.first().map(|batch| batch.first_timestamp())?
-    }
-
-    /// Get offset of first message in first batch
-    pub fn first_offset(&self) -> Option<u64> {
-        if self.is_empty() {
-            return None;
-        }
-        self.batches.first().map(|batch| batch.first_offset())?
-    }
-
-    /// Get timestamp of last message in last batch
-    pub fn last_timestamp(&self) -> Option<u64> {
-        if self.is_empty() {
-            return None;
-        }
-        self.batches.last().map(|batch| batch.last_timestamp())?
-    }
-
-    /// Get offset of last message in last batch
-    pub fn last_offset(&self) -> Option<u64> {
-        self.batches.last().map(|batch| batch.last_offset())?
-    }
-
-    /// Get a reference to the underlying vector of message containers
-    pub fn inner(&self) -> &Vec<IggyMessagesBatchMut> {
-        &self.batches
-    }
-
-    /// Consume the batch, returning the underlying vector of message containers
-    pub fn into_inner(mut self) -> Vec<IggyMessagesBatchMut> {
-        std::mem::take(&mut self.batches)
-    }
-
-    /// Iterate over all message containers in the batch
-    pub fn iter(&self) -> impl Iterator<Item = &IggyMessagesBatchMut> {
-        self.batches.iter()
-    }
-
-    /// Iterate over all mutable message containers in the batch
-    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut IggyMessagesBatchMut> {
-        self.batches.iter_mut()
-    }
-
-    /// Convert this batch and poll metadata into a vector of fully-formed IggyMessage objects
-    ///
-    /// This method transforms the internal message views into complete IggyMessage objects
-    /// that can be returned to clients. It should only be used by server http implementation.
-    ///
-    /// # Arguments
-    ///
-    /// * `poll_metadata` - Metadata about the partition and current offset
-    ///
-    /// # Returns
-    ///
-    /// A vector of IggyMessage objects with proper metadata
-    pub fn into_polled_messages(&self, poll_metadata: IggyPollMetadata) -> PolledMessages {
-        if self.is_empty() {
-            return PolledMessages::empty();
-        }
-
-        let mut messages = Vec::with_capacity(self.count() as usize);
-
-        for batch in self.iter() {
-            for message in batch.iter() {
-                let header = message.header().to_header();
-                let payload = Bytes::copy_from_slice(message.payload());
-                let user_headers = message.user_headers().map(Bytes::copy_from_slice);
-                let message = IggyMessage {
-                    header,
-                    payload,
-                    user_headers,
-                };
-                messages.push(message);
-            }
-        }
-
-        trace!(
-            "Converted batch of {} messages from partition {} with current offset {}",
-            messages.len(),
-            poll_metadata.partition_id,
-            poll_metadata.current_offset
-        );
-
-        PolledMessages {
-            partition_id: poll_metadata.partition_id,
-            current_offset: poll_metadata.current_offset,
-            count: messages.len() as u32,
-            messages,
-        }
-    }
-
-    /// Returns a new IggyMessagesBatch containing only messages with offsets greater than or equal to the specified offset,
-    /// up to the specified count.
-    ///
-    /// If no messages match the criteria, returns an empty batch.
-    pub fn get_by_offset(&self, start_offset: u64, count: u32) -> Self {
-        if self.is_empty() || count == 0 {
-            return Self::empty();
-        }
-
-        let mut result = Self::with_capacity(self.containers_count());
-        let mut remaining_count = count;
-        let mut current_offset = start_offset;
-
-        for container in self.iter() {
-            if remaining_count == 0 {
-                break;
-            }
-
-            let Some(batch_first) = container.first_offset() else {
-                continue;
-            };
-            if batch_first + container.count() as u64 <= current_offset {
-                continue;
-            }
-
-            // When current_offset is below this batch's range (cross-batch
-            // reads), start from the batch's first offset instead.
-            let effective_start = current_offset.max(batch_first);
-
-            if let Some(sliced) = container.slice_by_offset(effective_start, remaining_count)
-                && sliced.count() > 0
-            {
-                if let Some(last) = sliced.last_offset() {
-                    current_offset = last + 1;
-                }
-                remaining_count -= sliced.count();
-                result.add_batch(sliced);
-            }
-        }
-
-        result
-    }
-
-    /// Returns a new IggyMessagesBatch containing only messages with timestamps greater than or equal
-    /// to the specified timestamp, up to the specified count.
-    ///
-    /// If no messages match the criteria, returns an empty batch.
-    pub fn get_by_timestamp(&self, timestamp: u64, count: u32) -> Self {
-        if self.is_empty() || count == 0 {
-            return Self::empty();
-        }
-
-        let mut result = Self::with_capacity(self.containers_count());
-        let mut remaining_count = count;
-
-        for container in self.iter() {
-            if remaining_count == 0 {
-                break;
-            }
-
-            let first_timestamp = container.first_timestamp();
-            if first_timestamp.is_none() || first_timestamp.unwrap() < timestamp {
-                continue;
-            }
-
-            if let Some(sliced) = container.slice_by_timestamp(timestamp, remaining_count)
-                && sliced.count() > 0
-            {
-                remaining_count -= sliced.count();
-                result.add_batch(sliced);
-            }
-        }
-
-        result
-    }
-
-    /// Get the message at the specified index.
-    /// Returns None if the index is out of bounds.
-    pub fn get(&self, index: usize) -> Option<IggyMessageView<'_>> {
-        if index >= self.count as usize {
-            return None;
-        }
-
-        let mut seen_messages = 0;
-
-        for batch in &self.batches {
-            let batch_count = batch.count() as usize;
-
-            if index < seen_messages + batch_count {
-                let local_index = index - seen_messages;
-                return batch.get(local_index);
-            }
-
-            seen_messages += batch_count;
-        }
-
-        None
-    }
-}
-
-impl Index<usize> for IggyMessagesBatchSet {
-    type Output = [u8];
-
-    /// Get the message bytes at the specified index across all batches
-    ///
-    /// # Panics
-    ///
-    /// Panics if the index is out of bounds (>= total number of messages)
-    fn index(&self, index: usize) -> &Self::Output {
-        assert!(
-            index < self.count as usize,
-            "Index out of bounds: the len is {} but the index is {}",
-            self.count,
-            index
-        );
-
-        let mut seen_messages = 0;
-
-        for batch in &self.batches {
-            let batch_count = batch.count() as usize;
-
-            if index < seen_messages + batch_count {
-                let local_index = index - seen_messages;
-                return &batch[local_index];
-            }
-
-            seen_messages += batch_count;
-        }
-
-        unreachable!("Failed to find message at index {}", index);
-    }
-}
-
-impl Sizeable for IggyMessagesBatchSet {
-    fn get_size_bytes(&self) -> IggyByteSize {
-        IggyByteSize::from(self.size as u64)
-    }
-}
-
-impl From<Vec<IggyMessagesBatchMut>> for IggyMessagesBatchSet {
-    fn from(messages: Vec<IggyMessagesBatchMut>) -> Self {
-        Self::from_vec(messages)
-    }
-}
-
-impl From<IggyMessagesBatchMut> for IggyMessagesBatchSet {
-    fn from(messages: IggyMessagesBatchMut) -> Self {
-        Self::from_vec(vec![messages])
-    }
-}
diff --git a/core/server_common/src/segment_storage/index_reader.rs b/core/server_common/src/segment_storage/index_reader.rs
index 7f45c84..b4fb651 100644
--- a/core/server_common/src/segment_storage/index_reader.rs
+++ b/core/server_common/src/segment_storage/index_reader.rs
@@ -15,375 +15,37 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::{IggyIndexesMut, PooledBuffer};
-use compio::{
-    buf::{IntoInner, IoBuf},
-    fs::{File, OpenOptions},
-    io::AsyncReadAtExt,
-};
+use compio::fs::OpenOptions;
 use err_trail::ErrContext;
-use iggy_common::{INDEX_SIZE, IggyError, IggyIndex, IggyIndexView};
-use std::{
-    io::ErrorKind,
-    rc::Rc,
-    sync::atomic::{AtomicU64, Ordering},
-};
-use tracing::{error, trace};
+use iggy_common::IggyError;
+use tracing::trace;
 
-/// A dedicated struct for reading from the index file.
+/// Path handle for a segment's index file, validated openable at segment
+/// build. Reads go through the partition's own index reader; this exists so
+/// storage plumbing (bootstrap, state transfer) can resolve the index path.
 #[derive(Debug)]
 pub struct IndexReader {
     file_path: String,
-    file: File,
-    index_size_bytes: Rc<AtomicU64>,
 }
 
-// Safety: We are guaranteeing that IndexReader will never be used from multiple threads
-unsafe impl Send for IndexReader {}
-
 impl IndexReader {
-    /// Opens the index file in read-only mode.
-    pub async fn new(file_path: &str, index_size_bytes: Rc<AtomicU64>) -> Result<Self, IggyError> {
-        let file = OpenOptions::new()
+    /// Opens the index file read-only to prove it exists, then drops the
+    /// descriptor: nothing reads through this type.
+    pub async fn new(file_path: &str) -> Result<Self, IggyError> {
+        OpenOptions::new()
             .read(true)
             .open(file_path)
             .await
             .error(|e: &std::io::Error| format!("Failed to open index file: {file_path}. {e}"))
             .map_err(|_| IggyError::CannotReadFile)?;
 
-        trace!(
-            "Opened index file for reading: {file_path}, size: {}",
-            index_size_bytes.load(Ordering::Acquire)
-        );
+        trace!("Validated index file for reading: {file_path}");
         Ok(Self {
             file_path: file_path.to_string(),
-            file,
-            index_size_bytes,
         })
     }
 
     pub fn path(&self) -> String {
         self.file_path.clone()
     }
-
-    /// Loads all indexes from the index file into the optimized binary format.
-    /// Note that this function does not use the pool, as the messages are not cached.
-    /// This is expected - this method is called at startup and we want to preserve
-    /// memory pool usage.
-    pub async fn load_all_indexes_from_disk(&self) -> Result<IggyIndexesMut, IggyError> {
-        let file_size = self.file_size();
-        if file_size == 0 {
-            return Ok(IggyIndexesMut::empty());
-        }
-
-        let buf = match self.read_at(0, file_size, false).await {
-            Ok(buf) => buf,
-            Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
-                return Ok(IggyIndexesMut::empty());
-            }
-            Err(e) => {
-                error!(
-                    "Error reading batch header at offset 0 in file {}: {e}",
-                    self.file_path
-                );
-                return Err(IggyError::CannotReadFile);
-            }
-        };
-        let index_count = file_size / INDEX_SIZE as u32;
-        let mut indexes = IggyIndexesMut::from_bytes(buf, 0);
-        if indexes.count() != index_count {
-            error!(
-                "Loaded {} indexes from disk, expected {}, file {} is probably corrupted!",
-                indexes.count(),
-                index_count,
-                self.file_path
-            );
-        }
-        indexes.mark_saved();
-        Ok(indexes)
-    }
-
-    /// Loads a specific range of indexes from disk based on offset.
-    ///
-    /// Returns a slice of indexes starting at the relative_start_offset with the specified count,
-    /// or None if the requested range is not available.
-    pub async fn load_from_disk_by_offset(
-        &self,
-        relative_start_offset: u32,
-        count: u32,
-    ) -> Result<Option<IggyIndexesMut>, IggyError> {
-        let file_size = self.file_size();
-        let total_indexes = file_size / INDEX_SIZE as u32;
-
-        if file_size == 0 || total_indexes == 0 {
-            trace!(
-                "Index file {} is empty, cannot load indexes",
-                self.file_path
-            );
-            return Ok(None);
-        }
-
-        if relative_start_offset >= total_indexes {
-            trace!(
-                "Start offset {} is out of bounds. Total indexes: {}",
-                relative_start_offset, total_indexes
-            );
-            return Ok(None);
-        }
-
-        let available_count = total_indexes.saturating_sub(relative_start_offset);
-        let actual_count = std::cmp::min(count, available_count);
-
-        if actual_count == 0 {
-            trace!(
-                "No available indexes to load. Start offset: {}, requested count: {}",
-                relative_start_offset, count
-            );
-            return Ok(None);
-        }
-
-        let start_byte = relative_start_offset as usize * INDEX_SIZE;
-        let end_byte = start_byte + (actual_count as usize * INDEX_SIZE);
-
-        let indexes_bytes = match self
-            .read_at(start_byte as u32, (end_byte - start_byte) as u32, true)
-            .await
-        {
-            Ok(buf) => buf,
-            Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
-                error!("Unexpected EOF while reading indexes");
-                return Ok(None);
-            }
-            Err(e) => {
-                error!(
-                    "Error reading {actual_count} indexes at position {relative_start_offset} in file {} of size {file_size}: {e}",
-                    self.file_path
-                );
-                return Err(IggyError::CannotReadFile);
-            }
-        };
-
-        let base_position = if relative_start_offset > 0 {
-            match self.load_nth_index(relative_start_offset - 1).await? {
-                Some(prev_index) => prev_index.position,
-                None => {
-                    trace!(
-                        "Failed to load previous index at position {}",
-                        relative_start_offset - 1
-                    );
-                    0
-                }
-            }
-        } else {
-            0
-        };
-
-        let indexes = IggyIndexesMut::from_bytes(indexes_bytes, base_position);
-
-        trace!(
-            "Loaded {actual_count} indexes from disk starting at offset {relative_start_offset}, base position: {base_position}, last position: {}",
-            indexes.last_position()
-        );
-
-        Ok(Some(indexes))
-    }
-
-    /// Loads a specific range of indexes from disk based on timestamp.
-    ///
-    /// Returns a slice of indexes starting from the index with timestamp closest to
-    /// (but not exceeding) the requested timestamp, with the specified count.
-    pub async fn load_from_disk_by_timestamp(
-        &self,
-        timestamp: u64,
-        count: u32,
-    ) -> Result<Option<IggyIndexesMut>, IggyError> {
-        let file_size = self.file_size();
-        let total_indexes = file_size / INDEX_SIZE as u32;
-
-        if file_size == 0 || total_indexes == 0 {
-            trace!("Index file is empty");
-            return Ok(None);
-        }
-
-        let start_index_pos = match self
-            .binary_search_position_for_timestamp_async(timestamp)
-            .await?
-        {
-            Some(pos) => pos,
-            None => return Ok(None),
-        };
-
-        let available_count = total_indexes.saturating_sub(start_index_pos);
-        let actual_count = std::cmp::min(count, available_count);
-
-        if actual_count == 0 {
-            trace!(
-                "No available indexes to load. Start index pos: {}, requested count: {}",
-                start_index_pos, count
-            );
-            return Ok(None);
-        }
-
-        let start_byte = start_index_pos as usize * INDEX_SIZE;
-        let end_byte = start_byte + (actual_count as usize * INDEX_SIZE);
-
-        let indexes_bytes = match self
-            .read_at(start_byte as u32, (end_byte - start_byte) as u32, true)
-            .await
-        {
-            Ok(buf) => buf,
-            Err(e) if e.kind() == ErrorKind::UnexpectedEof => return Ok(None),
-            Err(e) => {
-                error!(
-                    "Error reading {actual_count} indexes at position {start_index_pos} in file {}: {e}",
-                    self.file_path
-                );
-                return Err(IggyError::CannotReadFile);
-            }
-        };
-
-        let base_position = if start_index_pos > 0 {
-            match self.load_nth_index(start_index_pos - 1).await? {
-                Some(prev_index) => prev_index.position,
-                None => {
-                    trace!(
-                        "Failed to load previous index at position {}",
-                        start_index_pos - 1
-                    );
-                    0
-                }
-            }
-        } else {
-            0
-        };
-
-        trace!(
-            "Loaded {} indexes from disk starting at timestamp {}, base position: {}",
-            actual_count, timestamp, base_position
-        );
-
-        Ok(Some(IggyIndexesMut::from_bytes(
-            indexes_bytes,
-            base_position,
-        )))
-    }
-
-    /// Finds the position of the index with timestamp closest to (but not exceeding) the target
-    async fn binary_search_position_for_timestamp_async(
-        &self,
-        target_timestamp: u64,
-    ) -> Result<Option<u32>, IggyError> {
-        let file_size = self.file_size();
-        if file_size == 0 {
-            return Ok(None);
-        }
-
-        let total_indexes = file_size / INDEX_SIZE as u32;
-        if total_indexes == 0 {
-            return Ok(None);
-        }
-
-        let last_idx = match self.load_nth_index(total_indexes - 1).await? {
-            Some(idx) => idx,
-            None => return Ok(None),
-        };
-
-        if target_timestamp > last_idx.timestamp {
-            return Ok(Some(total_indexes - 1));
-        }
-
-        let first_idx = match self.load_nth_index(0).await? {
-            Some(idx) => idx,
-            None => return Ok(None),
-        };
-
-        if target_timestamp <= first_idx.timestamp {
-            return Ok(Some(0));
-        }
-
-        let mut low = 0;
-        let mut high = total_indexes - 1;
-
-        while low <= high {
-            let mid = low + (high - low) / 2;
-            let mid_index = match self.load_nth_index(mid).await? {
-                Some(idx) => idx,
-                None => break,
-            };
-
-            match mid_index.timestamp.cmp(&target_timestamp) {
-                std::cmp::Ordering::Equal => return Ok(Some(mid)),
-                std::cmp::Ordering::Less => low = mid + 1,
-                std::cmp::Ordering::Greater => {
-                    if mid == 0 {
-                        break;
-                    }
-                    high = mid - 1;
-                }
-            }
-        }
-
-        Ok(Some(low))
-    }
-
-    /// Returns the size of the index file in bytes.
-    fn file_size(&self) -> u32 {
-        self.index_size_bytes.load(Ordering::Acquire) as u32
-    }
-
-    /// Reads a specified number of bytes from the index file at a given offset.
-    async fn read_at(
-        &self,
-        offset: u32,
-        len: u32,
-        _use_pool: bool,
-    ) -> Result<PooledBuffer, std::io::Error> {
-        let buf = PooledBuffer::with_capacity(len as usize);
-
-        let (result, buf) = self
-            .file
-            .read_exact_at(buf.slice(..len as usize), offset as u64)
-            .await
-            .into();
-        let buf = buf.into_inner();
-        result?;
-        Ok(buf)
-    }
-
-    /// Gets the nth index from the index file.
-    ///
-    /// The index position is 0-based (first index is at position 0).
-    /// Returns None if the specified position is out of bounds.
-    async fn load_nth_index(&self, position: u32) -> Result<Option<IggyIndex>, IggyError> {
-        let file_size = self.file_size();
-        let total_indexes = file_size / INDEX_SIZE as u32;
-
-        if position >= total_indexes {
-            trace!(
-                "Index position {} is out of bounds. Total indexes: {}",
-                position, total_indexes
-            );
-            return Ok(None);
-        }
-
-        let offset = position * INDEX_SIZE as u32;
-
-        let buf = match self.read_at(offset, INDEX_SIZE as u32, true).await {
-            Ok(buf) => buf,
-            Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
-                return Ok(None);
-            }
-            Err(e) => {
-                error!(
-                    "Error reading index at position {} (offset {}) in file {}: {e}",
-                    position, offset, self.file_path
-                );
-                return Err(IggyError::CannotReadFile);
-            }
-        };
-
-        let index = IggyIndexView::new(&buf).to_index();
-
-        Ok(Some(index))
-    }
 }
diff --git a/core/server_common/src/segment_storage/index_writer.rs b/core/server_common/src/segment_storage/index_writer.rs
index 0ea1ce8..f0603fb 100644
--- a/core/server_common/src/segment_storage/index_writer.rs
+++ b/core/server_common/src/segment_storage/index_writer.rs
@@ -15,12 +15,9 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::PooledBuffer;
 use compio::fs::File;
 use compio::fs::OpenOptions;
-use compio::io::AsyncWriteAtExt;
 use err_trail::ErrContext;
-use iggy_common::INDEX_SIZE;
 use iggy_common::IggyError;
 use std::rc::Rc;
 use std::sync::atomic::{AtomicU64, Ordering};
@@ -32,7 +29,6 @@
     file_path: String,
     file: File,
     index_size_bytes: Rc<AtomicU64>,
-    fsync: bool,
 }
 
 // Safety: We are guaranteeing that IndexWriter will never be used from multiple threads
@@ -43,7 +39,6 @@
     pub async fn new(
         file_path: &str,
         index_size_bytes: Rc<AtomicU64>,
-        fsync: bool,
         file_exists: bool,
     ) -> Result<Self, IggyError> {
         let mut opts = OpenOptions::new();
@@ -82,48 +77,9 @@
             file_path: file_path.to_string(),
             file,
             index_size_bytes,
-            fsync,
         })
     }
 
-    /// Appends multiple index buffer to the index file in a single operation.
-    pub async fn save_indexes(&self, indexes: PooledBuffer) -> Result<(), IggyError> {
-        if indexes.is_empty() {
-            return Ok(());
-        }
-
-        let count = indexes.len() / INDEX_SIZE;
-        let len = indexes.len();
-
-        let position = self.index_size_bytes.load(Ordering::Relaxed);
-        let file = &self.file;
-        (&*file)
-            .write_all_at(indexes, position)
-            .await
-            .0
-            .error(|e: &std::io::Error| {
-                format!(
-                    "Failed to write {} indexes to file: {}. {e}",
-                    count, self.file_path
-                )
-            })
-            .map_err(|_| IggyError::CannotSaveIndexToSegment)?;
-
-        self.index_size_bytes
-            .fetch_add(len as u64, Ordering::Release);
-
-        if self.fsync {
-            let _ = self.fsync().await;
-        }
-        trace!(
-            "Saved {count} indexes of size {} to file: {}",
-            INDEX_SIZE * count,
-            self.file_path
-        );
-
-        Ok(())
-    }
-
     pub fn size_counter(&self) -> Rc<AtomicU64> {
         self.index_size_bytes.clone()
     }
diff --git a/core/server_common/src/segment_storage/messages_reader.rs b/core/server_common/src/segment_storage/messages_reader.rs
index ecfe7e2..6c07e45 100644
--- a/core/server_common/src/segment_storage/messages_reader.rs
+++ b/core/server_common/src/segment_storage/messages_reader.rs
@@ -15,136 +15,39 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::{IggyIndexesMut, IggyMessagesBatchMut, PooledBuffer};
-use compio::buf::{IntoInner, IoBuf};
-use compio::fs::{File, OpenOptions};
-use compio::io::AsyncReadAtExt;
+use compio::fs::OpenOptions;
 use err_trail::ErrContext;
 use iggy_common::IggyError;
-use std::rc::Rc;
-use std::{
-    io::ErrorKind,
-    sync::atomic::{AtomicU64, Ordering},
-};
-use tracing::{error, trace};
+use tracing::trace;
 
-/// A dedicated struct for reading from the messages file.
+/// Path handle for a segment's messages file, validated openable at segment
+/// build. Reads go through the partition's own sealed-segment handles; this
+/// exists so storage plumbing (bootstrap, state transfer) can resolve the
+/// segment path.
 #[derive(Debug)]
 pub struct MessagesReader {
     file_path: String,
-    file: File,
-    messages_size_bytes: Rc<AtomicU64>,
 }
 
-// Safety: We are guaranteeing that MessagesReader will never be used from multiple threads
-unsafe impl Send for MessagesReader {}
-
 impl MessagesReader {
-    /// Opens the messages file in read mode.
-    pub async fn new(
-        file_path: &str,
-        messages_size_bytes: Rc<AtomicU64>,
-    ) -> Result<Self, IggyError> {
-        let file = OpenOptions::new()
+    /// Opens the messages file read-only to prove it exists, then drops the
+    /// descriptor: nothing reads through this type.
+    pub async fn new(file_path: &str) -> Result<Self, IggyError> {
+        OpenOptions::new()
             .read(true)
             .open(file_path)
             .await
             .error(|e: &std::io::Error| format!("Failed to open messages file: {file_path}. {e}"))
             .map_err(|_| IggyError::CannotReadFile)?;
 
-        // posix_fadvise() is Linux-only in the nix crate
-        #[cfg(target_os = "linux")]
-        {
-            let _ = nix::fcntl::posix_fadvise(
-                &file,
-                0,
-                0, // 0 means the entire file
-                nix::fcntl::PosixFadviseAdvice::POSIX_FADV_SEQUENTIAL,
-            )
-            .info(|e: &nix::errno::Errno| {
-                format!(
-                    "Failed to set sequential access pattern on messages file: {file_path}. {e}"
-                )
-            });
-        }
-
-        let size_bytes = messages_size_bytes.load(Ordering::Relaxed);
-        trace!(
-            "Opened messages file for reading: {file_path}, size: {}",
-            size_bytes
-        );
+        trace!("Validated messages file for reading: {file_path}");
 
         Ok(Self {
             file_path: file_path.to_string(),
-            file,
-            messages_size_bytes,
         })
     }
 
     pub fn path(&self) -> String {
         self.file_path.clone()
     }
-
-    /// Loads and returns a batch of messages from the messages file.
-    pub async fn load_messages_from_disk(
-        &self,
-        indexes: IggyIndexesMut,
-    ) -> Result<IggyMessagesBatchMut, IggyError> {
-        let file_size = self.file_size();
-        if file_size == 0 {
-            return Ok(IggyMessagesBatchMut::empty());
-        }
-
-        let start_pos = indexes.base_position();
-        let count_bytes = indexes.messages_size();
-        let messages_count = indexes.count();
-
-        if start_pos + count_bytes > file_size {
-            return Ok(IggyMessagesBatchMut::empty());
-        }
-
-        let messages_bytes = match self.read_at(start_pos, count_bytes, true).await {
-            Ok(buf) => buf,
-            Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
-                return Ok(IggyMessagesBatchMut::empty());
-            }
-            Err(e) => {
-                error!(
-                    "Error reading {messages_count} messages at position {start_pos} in file {} of size {}: {e}",
-                    self.file_path, file_size
-                );
-                return Err(IggyError::CannotReadMessage);
-            }
-        };
-
-        Ok(IggyMessagesBatchMut::from_indexes_and_messages(
-            indexes,
-            messages_bytes,
-        ))
-    }
-
-    /// Returns the size of the messages file in bytes.
-    pub fn file_size(&self) -> u32 {
-        self.messages_size_bytes.load(Ordering::Acquire) as u32
-    }
-
-    /// Reads `len` bytes from the messages file at the specified `offset`.
-    async fn read_at(
-        &self,
-        offset: u32,
-        len: u32,
-        _use_pool: bool,
-    ) -> Result<PooledBuffer, std::io::Error> {
-        let buf = PooledBuffer::with_capacity(len as usize);
-
-        let (result, buf) = self
-            .file
-            .read_exact_at(buf.slice(..len as usize), offset as u64)
-            .await
-            .into();
-
-        let buf = buf.into_inner();
-        result?;
-        Ok(buf)
-    }
 }
diff --git a/core/server_common/src/segment_storage/messages_writer.rs b/core/server_common/src/segment_storage/messages_writer.rs
index 214c719..1d0094b 100644
--- a/core/server_common/src/segment_storage/messages_writer.rs
+++ b/core/server_common/src/segment_storage/messages_writer.rs
@@ -15,10 +15,9 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use super::write_batch_frozen;
 use compio::fs::{File, OpenOptions};
 use err_trail::ErrContext;
-use iggy_common::{IggyByteSize, IggyError, IggyMessagesBatch};
+use iggy_common::IggyError;
 use std::{
     rc::Rc,
     sync::atomic::{AtomicU64, Ordering},
@@ -31,7 +30,6 @@
     file_path: String,
     file: File,
     messages_size_bytes: Rc<AtomicU64>,
-    fsync: bool,
 }
 
 // Safety: We are guaranteeing that MessagesWriter will never be used from multiple threads
@@ -46,7 +44,6 @@
     pub async fn new(
         file_path: &str,
         messages_size_bytes: Rc<AtomicU64>,
-        fsync: bool,
         file_exists: bool,
     ) -> Result<Self, IggyError> {
         let mut opts = OpenOptions::new();
@@ -90,38 +87,9 @@
             file_path: file_path.to_string(),
             file,
             messages_size_bytes,
-            fsync,
         })
     }
 
-    /// Append frozen (immutable) batches to the messages file.
-    /// The caller retains the batches (for use in in-flight buffer) while disk I/O proceeds.
-    pub async fn save_frozen_batches(
-        &self,
-        batches: &[IggyMessagesBatch],
-    ) -> Result<IggyByteSize, IggyError> {
-        let messages_size: u64 = batches.iter().map(|b| b.size() as u64).sum();
-
-        let position = self.messages_size_bytes.load(Ordering::Relaxed);
-        let file = &self.file;
-        write_batch_frozen(file, position, batches)
-            .await
-            .error(|e: &IggyError| {
-                format!(
-                    "Failed to write frozen batch to messages file: {}. {e}",
-                    self.file_path
-                )
-            })?;
-
-        if self.fsync {
-            let _ = self.fsync().await;
-        }
-
-        self.messages_size_bytes
-            .fetch_add(messages_size, Ordering::Release);
-
-        Ok(IggyByteSize::from(messages_size))
-    }
     pub fn path(&self) -> String {
         self.file_path.clone()
     }
diff --git a/core/server_common/src/segment_storage/mod.rs b/core/server_common/src/segment_storage/mod.rs
index 953b359..e88d90f 100644
--- a/core/server_common/src/segment_storage/mod.rs
+++ b/core/server_common/src/segment_storage/mod.rs
@@ -20,64 +20,14 @@
 mod messages_reader;
 mod messages_writer;
 
-use bytes::Bytes;
-use compio::{fs::File, io::AsyncWriteAtExt};
-use iggy_common::{IggyError, IggyMessagesBatch};
+use iggy_common::IggyError;
 use std::rc::Rc;
-use tracing::error;
 
 pub use index_reader::IndexReader;
 pub use index_writer::IndexWriter;
 pub use messages_reader::MessagesReader;
 pub use messages_writer::MessagesWriter;
 
-/// Maximum number of IO vectors for a single writev() call.
-/// Linux typically has IOV_MAX=1024, but we use a conservative value to ensure
-/// cross-platform compatibility and leave room for any internal overhead.
-const MAX_IOV_COUNT: usize = 1024;
-
-/// Vectored write frozen (immutable) batches to file.
-pub async fn write_batch_frozen(
-    file: &File,
-    position: u64,
-    batches: &[IggyMessagesBatch],
-) -> Result<usize, IggyError> {
-    let (total_written, buffers) = batches.iter().fold(
-        (0usize, Vec::with_capacity(batches.len())),
-        |(size, mut bufs), batch| {
-            bufs.push(batch.messages_bytes());
-            (size + batch.size() as usize, bufs)
-        },
-    );
-
-    write_vectored_chunked_bytes(file, position, buffers).await?;
-    Ok(total_written)
-}
-
-/// Writes Bytes buffers to file using vectored I/O, chunking to respect IOV_MAX limits.
-async fn write_vectored_chunked_bytes(
-    file: &File,
-    mut position: u64,
-    buffers: Vec<Bytes>,
-) -> Result<(), IggyError> {
-    for chunk in buffers.chunks(MAX_IOV_COUNT) {
-        let chunk_size: usize = chunk.iter().map(|b| b.len()).sum();
-        let chunk_vec: Vec<Bytes> = chunk.to_vec();
-
-        let (result, _) = (&*file)
-            .write_vectored_all_at(chunk_vec, position)
-            .await
-            .into();
-        result.map_err(|e| {
-            error!("Failed to write frozen batch to messages file: {e}");
-            IggyError::CannotWriteToFile
-        })?;
-
-        position += chunk_size as u64;
-    }
-    Ok(())
-}
-
 unsafe impl Send for SegmentStorage {}
 
 #[derive(Debug, Clone, Default)]
@@ -94,27 +44,21 @@
         index_path: &str,
         messages_size: u64,
         indexes_size: u64,
-        log_fsync: bool,
-        index_fsync: bool,
         file_exists: bool,
     ) -> Result<Self, IggyError> {
         let size = Rc::new(std::sync::atomic::AtomicU64::new(messages_size));
         let indexes_size = Rc::new(std::sync::atomic::AtomicU64::new(indexes_size));
-        let messages_writer = Rc::new(
-            MessagesWriter::new(messages_path, size.clone(), log_fsync, file_exists).await?,
-        );
+        let messages_writer = Rc::new(MessagesWriter::new(messages_path, size, file_exists).await?);
 
-        let index_writer = Rc::new(
-            IndexWriter::new(index_path, indexes_size.clone(), index_fsync, file_exists).await?,
-        );
+        let index_writer = Rc::new(IndexWriter::new(index_path, indexes_size, file_exists).await?);
 
         if file_exists {
             messages_writer.fsync().await?;
             index_writer.fsync().await?;
         }
 
-        let messages_reader = Rc::new(MessagesReader::new(messages_path, size).await?);
-        let index_reader = Rc::new(IndexReader::new(index_path, indexes_size).await?);
+        let messages_reader = Rc::new(MessagesReader::new(messages_path).await?);
+        let index_reader = Rc::new(IndexReader::new(index_path).await?);
         Ok(Self {
             messages_writer: Some(messages_writer),
             messages_reader: Some(messages_reader),
diff --git a/core/server_common/src/send_messages.rs b/core/server_common/src/send_messages.rs
new file mode 100644
index 0000000..c0a51eb
--- /dev/null
+++ b/core/server_common/src/send_messages.rs
@@ -0,0 +1,1307 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Server-side handling of the canonical message batch (layout in
+//! [`iggy_binary_protocol::batch`]).
+//!
+//! Producers put the batch on the wire themselves; admission
+//! ([`convert_request_message`]) verifies the producer's checksums, strips the
+//! routing metadata, and stamps the partition. From there the same bytes are
+//! journaled, replicated, persisted, and served back to polls.
+
+use crate::consensus_message::{MESSAGE_ALIGN, Message};
+use crate::iobuf::Owned;
+use crate::sharding::IggyNamespace;
+use bytes::{Bytes, BytesMut};
+use iggy_binary_protocol::batch;
+use iggy_binary_protocol::requests::messages::SendMessagesHeader as SendMessagesMetadata;
+use iggy_binary_protocol::{PrepareHeader, RoutedRequestHeader, WireDecode, WireError};
+use iggy_common::{EncryptorKind, IggyError, random_id};
+use twox_hash::XxHash3_64;
+
+pub use iggy_binary_protocol::batch::{
+    BATCH_CHECKSUM_OFFSET, BATCH_HEADER_SIZE, BATCH_MESSAGE_HEADER_SIZE, BatchHeader,
+    BatchIntegrity, BatchIterator, BatchIteratorWithOffsets, BatchMessageHeader, BatchMessageView,
+    BatchMessageViewWithOffsets, BatchRef, MAX_TIMESTAMP_DELTA_MICROS, calculate_batch_checksum,
+};
+
+/// Size of the batch header at the front of a `SendMessages` prepare body.
+pub const COMMAND_HEADER_SIZE: usize = BATCH_HEADER_SIZE;
+
+/// Offset of the blob inside a prepare frame:
+/// `[256B PrepareHeader][256B batch header][blob]`.
+pub const PREPARE_SPLIT_POINT: usize = 512;
+
+/// Map a wire-level batch error onto the typed `IggyError` the server paths
+/// key on. The two integrity variants keep their payloads; every structural
+/// error is an invalid command.
+fn batch_error(error: &WireError) -> IggyError {
+    match *error {
+        WireError::InvalidBatchChecksum {
+            stored,
+            computed,
+            base_offset,
+        } => IggyError::InvalidBatchChecksum(stored, computed, base_offset),
+        WireError::InvalidMessageChecksum {
+            stored,
+            computed,
+            offset,
+        } => IggyError::InvalidMessageChecksum(stored, computed, offset),
+        _ => IggyError::InvalidCommand,
+    }
+}
+
+/// Decode one batch record, verifying the batch checksum and every
+/// per-message checksum.
+///
+/// See [`batch::decode_batch_slice`]; this wrapper types the error for the
+/// server paths.
+///
+/// # Errors
+/// [`IggyError::InvalidCommand`] for a short or inconsistent record, and
+/// [`IggyError::InvalidBatchChecksum`] / [`IggyError::InvalidMessageChecksum`]
+/// on an integrity mismatch.
+pub fn decode_batch_slice(body: &[u8]) -> Result<BatchRef<'_>, IggyError> {
+    batch::decode_batch_slice(body).map_err(|error| batch_error(&error))
+}
+
+/// [`decode_batch_slice`] with the integrity level chosen by the caller.
+///
+/// The one caller that passes anything but [`BatchIntegrity::Verify`] is the
+/// disk poll under its operator knob; layout checks are not optional either
+/// way.
+///
+/// # Errors
+/// See [`decode_batch_slice`].
+pub fn decode_batch_slice_with(
+    body: &[u8],
+    integrity: BatchIntegrity,
+) -> Result<BatchRef<'_>, IggyError> {
+    batch::decode_batch_slice_with(body, integrity).map_err(|error| batch_error(&error))
+}
+
+#[derive(Debug, Clone)]
+pub struct SendMessagesOwned {
+    pub header: BatchHeader,
+    pub blob: Bytes,
+}
+
+impl SendMessagesOwned {
+    pub fn from_messages(
+        namespace: IggyNamespace,
+        messages: &IggyMessages,
+    ) -> Result<Self, IggyError> {
+        let message_count = messages.count();
+        let mut origin_timestamp = u64::MAX;
+        for message in messages {
+            origin_timestamp = origin_timestamp.min(message.header.origin_timestamp);
+        }
+
+        if origin_timestamp == u64::MAX {
+            origin_timestamp = 0;
+        }
+
+        let mut blob = BytesMut::new();
+        for (index, message) in messages.iter().enumerate() {
+            let id = if message.header.id == 0 {
+                random_id::get_uuid()
+            } else {
+                message.header.id
+            };
+            let offset_delta = u32::try_from(index).map_err(|_| IggyError::InvalidCommand)?;
+            let timestamp_delta = message
+                .header
+                .origin_timestamp
+                .checked_sub(origin_timestamp)
+                .ok_or(IggyError::InvalidCommand)?;
+            if timestamp_delta > MAX_TIMESTAMP_DELTA_MICROS {
+                return Err(IggyError::InvalidMessageTimestampDelta(timestamp_delta));
+            }
+            let timestamp_delta =
+                u32::try_from(timestamp_delta).map_err(|_| IggyError::InvalidCommand)?;
+            let user_headers = message.user_headers.as_deref().unwrap_or_default();
+            let user_headers_length =
+                u32::try_from(user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
+            let payload_length =
+                u32::try_from(message.payload.len()).map_err(|_| IggyError::InvalidCommand)?;
+
+            let mut header = [0u8; BATCH_MESSAGE_HEADER_SIZE];
+            header[8..24].copy_from_slice(&id.to_le_bytes());
+            header[24..28].copy_from_slice(&offset_delta.to_le_bytes());
+            header[28..32].copy_from_slice(&timestamp_delta.to_le_bytes());
+            header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
+            header[36..40].copy_from_slice(&payload_length.to_le_bytes());
+
+            let msg_start = blob.len();
+            blob.extend_from_slice(&header);
+            blob.extend_from_slice(&message.payload);
+            blob.extend_from_slice(user_headers);
+            let checksum = XxHash3_64::oneshot(&blob[msg_start + 8..]);
+            blob[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
+        }
+
+        let blob = blob.freeze();
+        let mut header = BatchHeader::new(
+            namespace.partition_id() as u64,
+            origin_timestamp,
+            u64::try_from(COMMAND_HEADER_SIZE + blob.len())
+                .map_err(|_| IggyError::InvalidCommand)?,
+            message_count,
+        );
+        header.batch_checksum = calculate_batch_checksum(&header, &blob);
+
+        Ok(Self { header, blob })
+    }
+
+    pub fn encode_request(
+        self,
+        mut request_header: RoutedRequestHeader,
+    ) -> Result<Message<RoutedRequestHeader>, IggyError> {
+        let total_size = std::mem::size_of::<RoutedRequestHeader>() + self.header.total_size();
+        // The rebuilt body differs in size from the wire body the header
+        // described; a stale `size` truncates the blob for every downstream
+        // slice (stamping, journal reads).
+        request_header.size = u32::try_from(total_size).map_err(|_| IggyError::InvalidCommand)?;
+        let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total_size);
+        let bytes = buffer.as_mut_slice();
+        bytes[0..std::mem::size_of::<RoutedRequestHeader>()]
+            .copy_from_slice(bytemuck::bytes_of(&request_header));
+        self.header.encode_into(
+            &mut bytes[std::mem::size_of::<RoutedRequestHeader>()
+                ..std::mem::size_of::<RoutedRequestHeader>() + COMMAND_HEADER_SIZE],
+        );
+        bytes[PREPARE_SPLIT_POINT..PREPARE_SPLIT_POINT + self.blob.len()]
+            .copy_from_slice(&self.blob);
+
+        Message::try_from(buffer).map_err(|_| IggyError::InvalidCommand)
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub struct IggyMessageHeader {
+    pub checksum: u64,
+    pub id: u128,
+    pub offset: u64,
+    pub timestamp: u64,
+    pub origin_timestamp: u64,
+    pub user_headers_length: u32,
+    pub payload_length: u32,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct IggyMessage {
+    pub header: IggyMessageHeader,
+    pub payload: Bytes,
+    pub user_headers: Option<Bytes>,
+}
+
+#[derive(Debug, Clone, Default, PartialEq, Eq)]
+pub struct IggyMessages {
+    messages: Vec<IggyMessage>,
+}
+
+impl IggyMessages {
+    #[must_use]
+    pub fn empty() -> Self {
+        Self::default()
+    }
+
+    #[must_use]
+    pub fn with_capacity(capacity: usize) -> Self {
+        Self {
+            messages: Vec::with_capacity(capacity),
+        }
+    }
+
+    pub fn push(&mut self, message: IggyMessage) {
+        self.messages.push(message);
+    }
+
+    #[must_use]
+    pub fn count(&self) -> u32 {
+        u32::try_from(self.messages.len()).unwrap_or(u32::MAX)
+    }
+
+    #[must_use]
+    pub const fn is_empty(&self) -> bool {
+        self.messages.is_empty()
+    }
+
+    #[must_use]
+    pub fn first_offset(&self) -> Option<u64> {
+        self.messages.first().map(|message| message.header.offset)
+    }
+
+    #[must_use]
+    pub fn last_offset(&self) -> Option<u64> {
+        self.messages.last().map(|message| message.header.offset)
+    }
+
+    #[must_use]
+    pub fn limit(self, count: u32) -> Self {
+        let mut messages = self.messages;
+        messages.truncate(usize::try_from(count).unwrap_or(usize::MAX));
+        Self { messages }
+    }
+
+    pub fn iter(&self) -> std::slice::Iter<'_, IggyMessage> {
+        self.messages.iter()
+    }
+}
+
+impl IntoIterator for IggyMessages {
+    type Item = IggyMessage;
+    type IntoIter = std::vec::IntoIter<IggyMessage>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.messages.into_iter()
+    }
+}
+
+impl<'a> IntoIterator for &'a IggyMessages {
+    type Item = &'a IggyMessage;
+    type IntoIter = std::slice::Iter<'a, IggyMessage>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.messages.iter()
+    }
+}
+
+/// Encode `header` into its own frozen 256-byte buffer (e.g. the rewritten
+/// header fragment of a server-sliced poll batch).
+#[must_use]
+pub fn frozen_batch_header(header: &BatchHeader) -> crate::iobuf::Frozen<MESSAGE_ALIGN> {
+    let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(COMMAND_HEADER_SIZE);
+    header.encode_into(buffer.as_mut_slice());
+    buffer.into()
+}
+
+/// Re-encode a canonical `SendMessages` request with every message's payload
+/// and user headers encrypted, per-message checksums and lengths recomputed,
+/// and the batch header (length + checksum) restamped.
+///
+/// Runs ONCE, on the primary at ingestion (after [`convert_request_message`]
+/// admitted the wire form), so the ciphertext is what replicates: every
+/// replica journals and persists identical bytes, and the poll path decrypts
+/// uniformly regardless of which replica or tier served the fragment.
+///
+/// # Errors
+///
+/// [`IggyError::InvalidCommand`] on an undecodable batch; encryption errors
+/// propagate from the encryptor.
+pub fn encrypt_batch_request(
+    message: Message<RoutedRequestHeader>,
+    encryptor: &EncryptorKind,
+) -> Result<Message<RoutedRequestHeader>, IggyError> {
+    let request_header = *message.header();
+    let total_size = request_header.size as usize;
+    let body = &message.as_slice()[std::mem::size_of::<RoutedRequestHeader>()..total_size];
+    let batch = decode_batch_slice(body)?;
+
+    let mut blob = BytesMut::with_capacity(batch.blob().len() * 2);
+    for view in batch.iter() {
+        let encrypted_payload = encryptor.encrypt(view.payload)?;
+        let encrypted_user_headers = if view.user_headers.is_empty() {
+            None
+        } else {
+            Some(encryptor.encrypt(view.user_headers)?)
+        };
+        let user_headers: &[u8] = encrypted_user_headers.as_deref().unwrap_or_default();
+        let payload_length =
+            u32::try_from(encrypted_payload.len()).map_err(|_| IggyError::InvalidCommand)?;
+        let user_headers_length =
+            u32::try_from(user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
+
+        let mut header = [0u8; BATCH_MESSAGE_HEADER_SIZE];
+        header[8..24].copy_from_slice(&view.header.id.to_le_bytes());
+        header[24..28].copy_from_slice(&view.header.offset_delta.to_le_bytes());
+        header[28..32].copy_from_slice(&view.header.timestamp_delta.to_le_bytes());
+        header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
+        header[36..40].copy_from_slice(&payload_length.to_le_bytes());
+        let msg_start = blob.len();
+        blob.extend_from_slice(&header);
+        blob.extend_from_slice(&encrypted_payload);
+        blob.extend_from_slice(user_headers);
+        let checksum = XxHash3_64::oneshot(&blob[msg_start + 8..]);
+        blob[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
+    }
+
+    let blob = blob.freeze();
+    let mut header = batch.header;
+    header.batch_length =
+        u64::try_from(COMMAND_HEADER_SIZE + blob.len()).map_err(|_| IggyError::InvalidCommand)?;
+    header.batch_checksum = calculate_batch_checksum(&header, &blob);
+
+    SendMessagesOwned { header, blob }.encode_request(request_header)
+}
+
+/// Rebuild one stored batch record with every message's payload and user
+/// headers decrypted: the poll reply's single decrypt point, mirroring
+/// [`encrypt_batch_request`]. Lengths, per-message checksums, and the batch
+/// header (length + checksum) are restamped over the plaintext.
+///
+/// Framing is validated layout-only: the read path already applied its
+/// integrity knob to the stored bytes, and a server-sliced fragment's header
+/// was rewritten checksum-consistent at slice time.
+///
+/// # Errors
+/// [`IggyError::InvalidCommand`] on a malformed record;
+/// [`IggyError::CannotDecryptData`] when a section fails to decrypt.
+pub fn decrypt_batch_record(
+    record: &[u8],
+    encryptor: &EncryptorKind,
+) -> Result<Vec<u8>, IggyError> {
+    let batch = decode_batch_slice_with(record, BatchIntegrity::LayoutOnly)?;
+    if record.len() != batch.header.total_size() {
+        return Err(IggyError::InvalidCommand);
+    }
+
+    let mut blob = BytesMut::with_capacity(batch.blob().len());
+    for view in batch.iter() {
+        let payload = encryptor
+            .decrypt(view.payload)
+            .map_err(|_| IggyError::CannotDecryptData)?;
+        let decrypted_user_headers = if view.user_headers.is_empty() {
+            None
+        } else {
+            Some(
+                encryptor
+                    .decrypt(view.user_headers)
+                    .map_err(|_| IggyError::CannotDecryptData)?,
+            )
+        };
+        let user_headers: &[u8] = decrypted_user_headers.as_deref().unwrap_or_default();
+        let payload_length = u32::try_from(payload.len()).map_err(|_| IggyError::InvalidCommand)?;
+        let user_headers_length =
+            u32::try_from(user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
+
+        let mut header = [0u8; BATCH_MESSAGE_HEADER_SIZE];
+        header[8..24].copy_from_slice(&view.header.id.to_le_bytes());
+        header[24..28].copy_from_slice(&view.header.offset_delta.to_le_bytes());
+        header[28..32].copy_from_slice(&view.header.timestamp_delta.to_le_bytes());
+        header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
+        header[36..40].copy_from_slice(&payload_length.to_le_bytes());
+        let msg_start = blob.len();
+        blob.extend_from_slice(&header);
+        blob.extend_from_slice(&payload);
+        blob.extend_from_slice(user_headers);
+        let checksum = XxHash3_64::oneshot(&blob[msg_start + 8..]);
+        blob[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
+    }
+
+    let mut header = batch.header;
+    header.batch_length =
+        u64::try_from(COMMAND_HEADER_SIZE + blob.len()).map_err(|_| IggyError::InvalidCommand)?;
+    header.batch_checksum = calculate_batch_checksum(&header, &blob);
+
+    let mut out = vec![0u8; COMMAND_HEADER_SIZE + blob.len()];
+    header.encode_into(&mut out[..COMMAND_HEADER_SIZE]);
+    out[COMMAND_HEADER_SIZE..].copy_from_slice(&blob);
+    Ok(out)
+}
+
+/// Whether admission stamps a batch checksum onto its output.
+///
+/// The recompute is an `XxHash3` batch-checksum pass, needed only when a reader
+/// validates the admitted batch before [`stamp_prepare_for_persistence`]
+/// recomputes it: the encrypt ingest path re-decodes the admitted batch
+/// (`encrypt_batch_request`'s validating decode, then the second `convert` its
+/// output re-enters as the canonical-batch fast path). The partition ingest
+/// path has no such reader, so it skips the pass and the checksum stays zero
+/// until stamp.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ChecksumMode {
+    /// Compute the batch checksum for the admitted batch.
+    Compute,
+    /// Leave the batch checksum zero; `stamp_prepare_for_persistence` fills it.
+    Skip,
+}
+
+/// Admit a `SendMessages` request into the pipeline form
+/// `[RoutedRequestHeader][256B batch header][blob]`.
+///
+/// Two input shapes reach this:
+/// - The wire form `[metadata][batch]` from a producer. The metadata section
+///   is validated and stripped, the batch is checksum-verified exactly as the
+///   producer hashed it (`partition_id` still zero), and the partition is
+///   stamped afterwards.
+/// - A body that already IS one canonical batch: the encrypt ingest path
+///   re-enters here with its own output. Detected first via the
+///   checksum-verified decode - a wire-form body cannot pass it, since its
+///   leading metadata bytes cannot form a batch whose length and checksum
+///   both match. The only legitimate producer of this shape is the earlier
+///   convert, which stamped the resolved partition, so a `partition_id` that
+///   does not match the namespace is rejected rather than persisted verbatim.
+///
+/// Either way the batch must fill the body exactly: `size` and `batch_length`
+/// are independent client-supplied fields, and a suffix past `batch_length`
+/// is covered by no checksum, still rides the buffer to disk, and desyncs the
+/// segment walk that advances by `batch_length`.
+///
+/// # Errors
+/// [`IggyError::InvalidCommand`] on a malformed body, an empty batch, or a
+/// metadata/batch count mismatch; typed checksum errors from the validating
+/// decode.
+pub fn convert_request_message(
+    namespace: IggyNamespace,
+    message: Message<RoutedRequestHeader>,
+    checksum: ChecksumMode,
+) -> Result<Message<RoutedRequestHeader>, IggyError> {
+    let request_header = *message.header();
+    let total_size = request_header.size as usize;
+    let body = &message.as_slice()[std::mem::size_of::<RoutedRequestHeader>()..total_size];
+
+    if let Ok(batch) = decode_batch_slice(body) {
+        if batch.message_count() == 0
+            || body.len() != batch.header.total_size()
+            || batch.header.partition_id != namespace.partition_id() as u64
+        {
+            return Err(IggyError::InvalidCommand);
+        }
+        return Ok(message);
+    }
+
+    admit_wire_request(namespace, body, request_header, checksum)
+}
+
+/// Validate a producer's `[metadata][batch]` body and rebuild it as the
+/// pipeline form with the partition stamped.
+fn admit_wire_request(
+    namespace: IggyNamespace,
+    body: &[u8],
+    mut request_header: RoutedRequestHeader,
+    checksum: ChecksumMode,
+) -> Result<Message<RoutedRequestHeader>, IggyError> {
+    if body.len() < 4 {
+        return Err(IggyError::InvalidCommand);
+    }
+    let metadata_length = u32::from_le_bytes(
+        body[..4]
+            .try_into()
+            .map_err(|_| IggyError::InvalidNumberEncoding)?,
+    ) as usize;
+    let batch_start = 4usize
+        .checked_add(metadata_length)
+        .ok_or(IggyError::InvalidCommand)?;
+    if body.len() < batch_start {
+        return Err(IggyError::InvalidCommand);
+    }
+
+    let (metadata, consumed) = SendMessagesMetadata::decode(&body[4..batch_start])
+        .map_err(|_| IggyError::InvalidCommand)?;
+    if consumed != metadata_length {
+        return Err(IggyError::InvalidCommand);
+    }
+
+    let batch_bytes = &body[batch_start..];
+    let batch = decode_batch_slice(batch_bytes)?;
+    if batch.message_count() == 0
+        || batch.message_count() != metadata.messages_count
+        || batch_bytes.len() != batch.header.total_size()
+    {
+        return Err(IggyError::InvalidCommand);
+    }
+
+    let header_size = std::mem::size_of::<RoutedRequestHeader>();
+    let total_size = header_size + batch.header.total_size();
+    request_header.size = u32::try_from(total_size).map_err(|_| IggyError::InvalidCommand)?;
+    let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total_size);
+    let bytes = buffer.as_mut_slice();
+    bytes[0..header_size].copy_from_slice(bytemuck::bytes_of(&request_header));
+    bytes[header_size..total_size].copy_from_slice(batch_bytes);
+
+    // The producer hashed `partition_id = 0`; stamp the resolved partition
+    // and restamp (or clear, for the stamp-fills-it path) the batch checksum.
+    let mut stamped = batch.header;
+    stamped.partition_id = namespace.partition_id() as u64;
+    stamped.batch_checksum = match checksum {
+        ChecksumMode::Compute => {
+            calculate_batch_checksum(&stamped, &bytes[PREPARE_SPLIT_POINT..total_size])
+        }
+        ChecksumMode::Skip => 0,
+    };
+    stamped.encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
+
+    Message::try_from(buffer).map_err(|_| IggyError::InvalidCommand)
+}
+
+/// Decode a `Prepare` message from a slice of bytes, validating the batch
+/// checksum and every per-message checksum.
+///
+/// `bytes` must be 16-byte aligned (`PrepareHeader` has `u128` fields). Source
+/// from `Frozen<MESSAGE_ALIGN>` / `Owned<MESSAGE_ALIGN>` / `Message<H>`.
+/// Misalignment: `debug_assert!` in debug; `InvalidCommand` in release.
+///
+/// # Errors
+///
+/// `IggyError::InvalidCommand` on a short buffer, bad bit pattern, `size`
+/// outside `[header_size, bytes.len()]`, a `size` that does not describe the
+/// batch exactly, or frames that do not tile the batch;
+/// `InvalidBatchChecksum` / `InvalidMessageChecksum` on an integrity mismatch.
+pub fn decode_prepare_slice(bytes: &[u8]) -> Result<BatchRef<'_>, IggyError> {
+    decode_prepare_slice_inner(bytes, true)
+}
+
+/// Like [`decode_prepare_slice`] but skips the per-message checksum
+/// verification and batch-checksum recompute, extracting only the header meta.
+/// Every cheap structural check (length, 16-byte alignment, `size` bounds, and
+/// `size` describing the batch exactly) is still enforced.
+///
+/// INVARIANT: `bytes` MUST be node-local self-stamped -
+/// [`stamp_prepare_for_persistence`] recomputed the batch checksum over the
+/// exact blob on the local node - or already integrity-checked at network
+/// ingress. There is no consensus-layer blob validation: the `PrepareHeader`
+/// integrity fields are inert zeros. Replicated and repaired prepares are
+/// validated via [`decode_prepare_slice`] before the bytes reach any trusted
+/// decode. Calling this on unvalidated network bytes would let a corrupted blob
+/// pass undetected. The full-body per-message checksum pass dominates
+/// produce-path CPU, so trusted call sites that only read header meta skip it.
+///
+/// # Errors
+///
+/// Same structural errors as [`decode_prepare_slice`], minus
+/// `InvalidBatchChecksum` and `InvalidMessageChecksum`.
+pub fn decode_prepare_slice_trusted(bytes: &[u8]) -> Result<BatchRef<'_>, IggyError> {
+    decode_prepare_slice_inner(bytes, false)
+}
+
+fn decode_prepare_slice_inner(
+    bytes: &[u8],
+    validate_checksum: bool,
+) -> Result<BatchRef<'_>, IggyError> {
+    let header_size = std::mem::size_of::<PrepareHeader>();
+    if bytes.len() < header_size {
+        return Err(IggyError::InvalidCommand);
+    }
+
+    // Bytemuck enforces alignment in release (maps to InvalidCommand below);
+    // debug_assert surfaces the contract violation early in dev.
+    debug_assert_eq!(
+        bytes
+            .as_ptr()
+            .align_offset(std::mem::align_of::<PrepareHeader>()),
+        0,
+        "decode_prepare_slice: bytes must be at least 16-byte aligned",
+    );
+
+    let prepare = bytemuck::checked::try_from_bytes::<PrepareHeader>(&bytes[..header_size])
+        .map_err(|_| IggyError::InvalidCommand)?;
+    let total_size = prepare.size as usize;
+    // Wire-controllable `size`: reject < header_size to avoid slice OOB below.
+    if total_size < header_size || bytes.len() < total_size {
+        return Err(IggyError::InvalidCommand);
+    }
+
+    let body = &bytes[header_size..total_size];
+    if body.len() < COMMAND_HEADER_SIZE {
+        return Err(IggyError::InvalidCommand);
+    }
+
+    let header =
+        BatchHeader::decode(&body[..COMMAND_HEADER_SIZE]).map_err(|error| batch_error(&error))?;
+    let blob_len = header.blob_len().map_err(|error| batch_error(&error))?;
+    // Exact, not a lower bound: a prepare frame IS one batch, so bytes past
+    // `batch_length` belong to nobody - no checksum covers them, yet the flush
+    // writes them, desyncing the segment walk. Readers walking a multi-batch
+    // chunk use `decode_batch_slice`, which bounds the blob by design.
+    if body.len() != header.total_size() {
+        return Err(IggyError::InvalidCommand);
+    }
+
+    let blob = &body[COMMAND_HEADER_SIZE..COMMAND_HEADER_SIZE + blob_len];
+    let batch = BatchRef::new(header, blob);
+    if validate_checksum {
+        let expected_checksum = batch::verify_and_recompute_batch_checksum(&batch)
+            .map_err(|error| batch_error(&error))?;
+        if header.batch_checksum != expected_checksum {
+            return Err(IggyError::InvalidBatchChecksum(
+                header.batch_checksum,
+                expected_checksum,
+                header.base_offset,
+            ));
+        }
+    }
+
+    Ok(batch)
+}
+
+pub fn stamp_prepare_for_persistence(
+    mut message: Message<PrepareHeader>,
+    base_offset: u64,
+    base_timestamp: u64,
+) -> Result<(Message<PrepareHeader>, BatchHeader, u32), IggyError> {
+    let total_size = message.header().size as usize;
+    let bytes = message.as_mut_slice();
+    if bytes.len() < PREPARE_SPLIT_POINT || total_size < PREPARE_SPLIT_POINT {
+        return Err(IggyError::InvalidCommand);
+    }
+
+    let header_offset = std::mem::size_of::<PrepareHeader>();
+    let mut command =
+        BatchHeader::decode(&bytes[header_offset..header_offset + COMMAND_HEADER_SIZE])
+            .map_err(|error| batch_error(&error))?;
+    command.base_offset = base_offset;
+    command.base_timestamp = base_timestamp;
+    let blob = &bytes[PREPARE_SPLIT_POINT..total_size];
+    command.batch_checksum = calculate_batch_checksum(&command, blob);
+    command.encode_into(&mut bytes[header_offset..header_offset + COMMAND_HEADER_SIZE]);
+    Ok((message, command, command.message_count))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use bytes::BufMut;
+    use iggy_binary_protocol::requests::messages::{RawMessage, SendMessagesEncoder};
+    use iggy_binary_protocol::{Command, Operation, WireEncode, WireIdentifier, WirePartitioning};
+    use iggy_common::Aes256GcmEncryptor;
+    use std::hash::Hasher;
+
+    fn aligned_prepare_bytes(size: u32) -> Owned<MESSAGE_ALIGN> {
+        let mut owned = Owned::<MESSAGE_ALIGN>::zeroed(std::mem::size_of::<PrepareHeader>());
+        let header: &mut PrepareHeader =
+            bytemuck::checked::try_from_bytes_mut(owned.as_mut_slice())
+                .expect("zeroed bytes form a valid PrepareHeader");
+        header.command = Command::Prepare;
+        header.size = size;
+        owned
+    }
+
+    /// Assemble an already-stamped batch into a `Prepare`:
+    /// `[PrepareHeader][256B batch header][blob]`, copying `owned`'s header and
+    /// blob verbatim. Shared by every real-batch fixture.
+    fn prepare_from_owned(owned: &SendMessagesOwned) -> Owned<MESSAGE_ALIGN> {
+        let header_size = std::mem::size_of::<PrepareHeader>();
+        let total = header_size + owned.header.total_size();
+        let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
+        {
+            let prepare: &mut PrepareHeader =
+                bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
+                    .expect("zeroed bytes form a valid PrepareHeader");
+            prepare.command = Command::Prepare;
+            prepare.size = u32::try_from(total).expect("prepare size fits u32");
+        }
+        let bytes = buffer.as_mut_slice();
+        owned
+            .header
+            .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
+        bytes[PREPARE_SPLIT_POINT..PREPARE_SPLIT_POINT + owned.blob.len()]
+            .copy_from_slice(&owned.blob);
+        buffer
+    }
+
+    /// A checksum-consistent STAMPED `Prepare` carrying real per-message records,
+    /// stamped at a non-zero `base_offset` / `base_timestamp` with a
+    /// `batch_checksum` over the final header fields + per-message checksum fields.
+    fn valid_prepare_bytes() -> Owned<MESSAGE_ALIGN> {
+        let namespace = IggyNamespace::new(1, 1, 7);
+        let mut owned = SendMessagesOwned::from_messages(namespace, &sample_messages())
+            .expect("build send batch");
+        owned.header.base_offset = 10;
+        owned.header.base_timestamp = 20;
+        owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
+        prepare_from_owned(&owned)
+    }
+
+    #[test]
+    fn decode_prepare_slice_trusted_matches_validating_for_valid_batch() {
+        // The trusted variant must surface byte-identical header meta to the
+        // validating decode for a checksum-consistent batch; only the
+        // per-message and batch-checksum passes are skipped.
+        let owned = valid_prepare_bytes();
+
+        let validated = decode_prepare_slice(owned.as_slice()).expect("valid batch decodes");
+        let trusted =
+            decode_prepare_slice_trusted(owned.as_slice()).expect("valid batch decodes trusted");
+
+        assert_eq!(validated.header.base_offset, trusted.header.base_offset);
+        assert_eq!(
+            validated.header.base_timestamp,
+            trusted.header.base_timestamp
+        );
+        assert_eq!(
+            validated.header.origin_timestamp,
+            trusted.header.origin_timestamp
+        );
+        assert_eq!(validated.header.batch_length, trusted.header.batch_length);
+        assert_eq!(validated.message_count(), trusted.message_count());
+        assert_eq!(validated.header.total_size(), trusted.header.total_size());
+        assert_eq!(validated.blob(), trusted.blob());
+    }
+
+    #[test]
+    fn decode_prepare_slice_trusted_skips_batch_checksum() {
+        // A stored batch_checksum mutated after stamping fails the validating
+        // decode but passes the trusted one: exactly why the trusted variant is
+        // confined to locally-produced bytes (see its doc invariant).
+        let mut owned = valid_prepare_bytes();
+        let corrupt_index = std::mem::size_of::<PrepareHeader>() + BATCH_CHECKSUM_OFFSET;
+        owned.as_mut_slice()[corrupt_index] ^= 0xFF;
+
+        assert!(
+            matches!(
+                decode_prepare_slice(owned.as_slice()),
+                Err(IggyError::InvalidBatchChecksum(..))
+            ),
+            "validating decode must reject a mutated batch checksum",
+        );
+        assert!(
+            decode_prepare_slice_trusted(owned.as_slice()).is_ok(),
+            "trusted decode skips the batch-checksum recomputation",
+        );
+    }
+
+    #[test]
+    fn decode_prepare_slice_size_below_header_size_does_not_panic() {
+        // Regression: without the `total_size < header_size` guard,
+        // `&bytes[256..size]` panics for any size < 256.
+        for adversarial_size in [0u32, 255] {
+            let owned = aligned_prepare_bytes(adversarial_size);
+            let result = decode_prepare_slice(owned.as_slice());
+            assert!(
+                matches!(result, Err(IggyError::InvalidCommand)),
+                "size={adversarial_size} must be rejected, got {result:?}",
+            );
+        }
+    }
+
+    #[cfg(debug_assertions)]
+    #[test]
+    #[should_panic(expected = "must be at least 16-byte aligned")]
+    fn decode_prepare_slice_debug_asserts_on_misaligned_input() {
+        // `Vec<u8>` requests align=1; glibc returns a base that is a
+        // multiple of 16, so `&buf[1..]` has offset 1 mod 16, reliably
+        // misaligned.
+        let buf: Vec<u8> = vec![0u8; std::mem::size_of::<PrepareHeader>() + 1];
+        let misaligned = &buf[1..];
+        assert_ne!(
+            misaligned.as_ptr().align_offset(16),
+            0,
+            "test setup: allocator returned non-16k base",
+        );
+        let _ = decode_prepare_slice(misaligned);
+    }
+
+    fn sample_messages() -> IggyMessages {
+        let mut messages = IggyMessages::with_capacity(2);
+        messages.push(IggyMessage {
+            header: IggyMessageHeader {
+                id: 7,
+                origin_timestamp: 1_000,
+                ..Default::default()
+            },
+            payload: Bytes::from_static(b"first-payload"),
+            user_headers: None,
+        });
+        messages.push(IggyMessage {
+            header: IggyMessageHeader {
+                id: 8,
+                origin_timestamp: 1_050,
+                ..Default::default()
+            },
+            payload: Bytes::from_static(b"second-payload"),
+            user_headers: Some(Bytes::from_static(b"user-header-bytes")),
+        });
+        messages
+    }
+
+    /// `[PrepareHeader][256B batch header][blob]` carrying real per-message
+    /// records and checksums from the production encoder, with the initial zero
+    /// base offset and timestamp.
+    fn prepare_with_messages(messages: &IggyMessages) -> Owned<MESSAGE_ALIGN> {
+        let namespace = IggyNamespace::new(1, 1, 7);
+        let owned =
+            SendMessagesOwned::from_messages(namespace, messages).expect("build send batch");
+        prepare_from_owned(&owned)
+    }
+
+    #[test]
+    fn checksum_oneshot_matches_streaming_reference() {
+        // Formula pin: the per-message checksum is XxHash3-64 (default seed)
+        // over `header[8..48] || payload || user_headers` as one byte stream.
+        // The encoders hash the concatenation in a single oneshot pass; this
+        // streaming reference feeds the same parts separately. Both must agree
+        // for every shape, or checksums at rest stop verifying.
+        fn streaming_reference(header_tail: &[u8], payload: &[u8], user_headers: &[u8]) -> u64 {
+            let mut hasher = XxHash3_64::new();
+            hasher.write(header_tail);
+            hasher.write(payload);
+            hasher.write(user_headers);
+            hasher.finish()
+        }
+
+        let header_tail: Vec<u8> = (0u8..40).collect();
+        let kilobyte: Vec<u8> = (0..1024u32).map(|index| (index % 251) as u8).collect();
+        let cases: &[(&[u8], &[u8])] = &[
+            (&[], &[]),
+            (b"payload-bytes", &[]),
+            (b"payload-bytes", b"user-header-bytes"),
+            (&kilobyte, &[]),
+            (&kilobyte, &kilobyte[..7]),
+            (&kilobyte[..1023], &kilobyte[..7]),
+        ];
+        for (payload, user_headers) in cases {
+            let mut concatenated =
+                Vec::with_capacity(header_tail.len() + payload.len() + user_headers.len());
+            concatenated.extend_from_slice(&header_tail);
+            concatenated.extend_from_slice(payload);
+            concatenated.extend_from_slice(user_headers);
+            assert_eq!(
+                XxHash3_64::oneshot(&concatenated),
+                streaming_reference(&header_tail, payload, user_headers),
+                "oneshot must match the streaming reference for payload {} B, user headers {} B",
+                payload.len(),
+                user_headers.len(),
+            );
+        }
+    }
+
+    #[test]
+    fn batch_checksum_pins_header_fields_then_message_checksum_fields() {
+        // Formula pin for the batch checksum: XxHash3-64 (default seed) streaming
+        // over the six batch header meta fields (LE, in field order) then each
+        // message's stored 8-byte checksum field in message order - never the
+        // bodies. This reference walks the blob by the KNOWN input message sizes,
+        // independent of the production frame decoder, and must equal what the
+        // encoder stamped, or a stamp will not verify against a read-back
+        // recompute.
+        let namespace = IggyNamespace::new(1, 1, 7);
+        let messages = sample_messages();
+        let mut owned =
+            SendMessagesOwned::from_messages(namespace, &messages).expect("build batch");
+        owned.header.base_offset = 100;
+        owned.header.base_timestamp = 200;
+        owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
+
+        let mut hasher = XxHash3_64::new();
+        hasher.write(&owned.header.partition_id.to_le_bytes());
+        hasher.write(&owned.header.base_offset.to_le_bytes());
+        hasher.write(&owned.header.base_timestamp.to_le_bytes());
+        hasher.write(&owned.header.origin_timestamp.to_le_bytes());
+        hasher.write(&owned.header.batch_length.to_le_bytes());
+        hasher.write(&owned.header.message_count.to_le_bytes());
+        let mut frame_start = 0usize;
+        for message in messages.iter() {
+            hasher.write(&owned.blob[frame_start..frame_start + 8]);
+            let user_headers = message.user_headers.as_deref().unwrap_or_default();
+            frame_start += BATCH_MESSAGE_HEADER_SIZE + message.payload.len() + user_headers.len();
+        }
+        let reference = hasher.finish();
+
+        assert_eq!(
+            frame_start,
+            owned.blob.len(),
+            "reference walk must consume the whole blob",
+        );
+        assert_eq!(
+            owned.header.batch_checksum, reference,
+            "batch checksum must equal hash(6 header fields || per-message checksum fields)",
+        );
+    }
+
+    #[test]
+    fn decode_batch_slice_rejects_body_corruption_with_intact_checksum_field() {
+        // Equal-integrity: the batch value binds bodies only through the
+        // per-message checksum fields, so a flipped body byte that leaves the
+        // 8-byte checksum field intact keeps the batch value matching. The
+        // validating decode must still reject it via the per-message verify -
+        // the sole at-rest read-back check (the poll disk walk) decodes
+        // through here.
+        let namespace = IggyNamespace::new(1, 1, 7);
+        let owned =
+            SendMessagesOwned::from_messages(namespace, &sample_messages()).expect("build batch");
+        let mut body = vec![0u8; COMMAND_HEADER_SIZE + owned.blob.len()];
+        owned.header.encode_into(&mut body[..COMMAND_HEADER_SIZE]);
+        body[COMMAND_HEADER_SIZE..].copy_from_slice(&owned.blob);
+
+        decode_batch_slice(&body).expect("the clean batch decodes");
+
+        // First payload byte sits right after the command header and the first
+        // message's 48B frame header, leaving that frame's checksum field intact.
+        let payload_index = COMMAND_HEADER_SIZE + BATCH_MESSAGE_HEADER_SIZE;
+        body[payload_index] ^= 0xFF;
+        assert!(
+            matches!(
+                decode_batch_slice(&body),
+                Err(IggyError::InvalidMessageChecksum(..))
+            ),
+            "body corruption with an intact checksum field must fail the per-message verify",
+        );
+    }
+
+    #[test]
+    fn decode_prepare_slice_rejects_body_corruption_with_intact_checksum_field() {
+        // The same equal-integrity guarantee at the resident/repair validating
+        // decode, plus proof that the batch value alone is blind to it.
+        let mut owned = prepare_with_messages(&sample_messages());
+        decode_prepare_slice(owned.as_slice()).expect("the clean prepare decodes");
+
+        let payload_index = PREPARE_SPLIT_POINT + BATCH_MESSAGE_HEADER_SIZE;
+        owned.as_mut_slice()[payload_index] ^= 0xFF;
+        assert!(
+            matches!(
+                decode_prepare_slice(owned.as_slice()),
+                Err(IggyError::InvalidMessageChecksum(..))
+            ),
+            "body corruption with an intact checksum field must fail the per-message verify",
+        );
+        assert!(
+            decode_prepare_slice_trusted(owned.as_slice()).is_ok(),
+            "the intact checksum field leaves the batch value matching, so trusted still passes",
+        );
+    }
+
+    /// Wire-form `SendMessages` body built by the production client encoder:
+    /// `[metadata][256B batch header][blob]` with producer-computed checksums
+    /// and `partition_id = 0`.
+    fn wire_send_messages_body(messages: &IggyMessages) -> Vec<u8> {
+        let raw: Vec<RawMessage<'_>> = messages
+            .iter()
+            .map(|message| RawMessage {
+                id: message.header.id,
+                origin_timestamp: message.header.origin_timestamp,
+                headers: message.user_headers.as_deref(),
+                payload: &message.payload,
+            })
+            .collect();
+        let stream_id = WireIdentifier::numeric(1);
+        let topic_id = WireIdentifier::numeric(1);
+        let partitioning = WirePartitioning::Balanced;
+        let mut buf = BytesMut::with_capacity(SendMessagesEncoder::encoded_size(
+            &stream_id,
+            &topic_id,
+            &partitioning,
+            &raw,
+        ));
+        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &raw)
+            .expect("wire body encodes");
+        buf.to_vec()
+    }
+
+    fn wire_request_message(body: &[u8]) -> Message<RoutedRequestHeader> {
+        let header_size = std::mem::size_of::<RoutedRequestHeader>();
+        let total = header_size + body.len();
+        let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
+        {
+            let header: &mut RoutedRequestHeader =
+                bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
+                    .expect("zeroed bytes form a valid RoutedRequestHeader");
+            header.command = Command::Request;
+            header.operation = Operation::SendMessages;
+            header.client = 1;
+            header.session = 1;
+            header.request = 1;
+            header.size = u32::try_from(total).expect("size fits u32");
+        }
+        buffer.as_mut_slice()[header_size..].copy_from_slice(body);
+        Message::try_from(buffer).expect("wire request message is valid")
+    }
+
+    #[test]
+    fn convert_request_message_rejects_empty_batches() {
+        let namespace = IggyNamespace::new(1, 1, 3);
+        let messages = IggyMessages::with_capacity(0);
+        let canonical = SendMessagesOwned::from_messages(namespace, &messages)
+            .expect("build empty canonical batch");
+        let mut canonical_body = vec![0; canonical.header.total_size()];
+        canonical
+            .header
+            .encode_into(&mut canonical_body[..COMMAND_HEADER_SIZE]);
+        // The client encoder refuses an empty batch, so assemble the wire form
+        // from the metadata primitives directly to prove admission rejects one
+        // independently.
+        let stream_id = WireIdentifier::numeric(1);
+        let topic_id = WireIdentifier::numeric(1);
+        let partitioning = WirePartitioning::Balanced;
+        let metadata_length =
+            stream_id.encoded_size() + topic_id.encoded_size() + partitioning.encoded_size() + 4;
+        let mut wire_buf = BytesMut::new();
+        wire_buf.put_u32_le(u32::try_from(metadata_length).expect("metadata fits u32"));
+        stream_id.encode(&mut wire_buf);
+        topic_id.encode(&mut wire_buf);
+        partitioning.encode(&mut wire_buf);
+        wire_buf.put_u32_le(0);
+        wire_buf.extend_from_slice(&canonical_body);
+        let wire_body = wire_buf.to_vec();
+
+        for mode in [ChecksumMode::Compute, ChecksumMode::Skip] {
+            let canonical_result =
+                convert_request_message(namespace, wire_request_message(&canonical_body), mode);
+            assert!(matches!(canonical_result, Err(IggyError::InvalidCommand)));
+
+            let wire_result =
+                convert_request_message(namespace, wire_request_message(&wire_body), mode);
+            assert!(matches!(wire_result, Err(IggyError::InvalidCommand)));
+        }
+    }
+
+    #[test]
+    fn convert_request_message_admits_wire_body_and_stamps_partition() {
+        // Golden: admitting the producer's wire body must yield the exact
+        // canonical batch the server-side builder (`from_messages`) produces
+        // for the same messages - command header + blob, byte for byte.
+        // Explicit non-zero ids keep it deterministic.
+        let namespace = IggyNamespace::new(1, 1, 3);
+        let messages = sample_messages();
+
+        let owned =
+            SendMessagesOwned::from_messages(namespace, &messages).expect("build canonical batch");
+        let mut expected_body = vec![0u8; COMMAND_HEADER_SIZE + owned.blob.len()];
+        owned
+            .header
+            .encode_into(&mut expected_body[..COMMAND_HEADER_SIZE]);
+        expected_body[COMMAND_HEADER_SIZE..].copy_from_slice(&owned.blob);
+
+        let wire = wire_request_message(&wire_send_messages_body(&messages));
+        let converted = convert_request_message(namespace, wire, ChecksumMode::Compute)
+            .expect("wire body admits");
+        let header_size = std::mem::size_of::<RoutedRequestHeader>();
+        let actual_body = &converted.as_slice()[header_size..converted.header().size as usize];
+
+        assert_eq!(
+            actual_body, expected_body,
+            "admitted wire body must be byte-identical to the server-built canonical batch",
+        );
+
+        // And the admitted batch is self-consistent: it validates through the
+        // batch-checksum decode and yields the original messages.
+        let decoded = decode_batch_slice(actual_body).expect("admitted batch checksum is valid");
+        assert_eq!(decoded.header.partition_id, 3);
+        assert_eq!(decoded.message_count(), messages.count());
+        let payloads: Vec<&[u8]> = decoded.iter().map(|view| view.payload).collect();
+        assert_eq!(
+            payloads,
+            vec![&b"first-payload"[..], &b"second-payload"[..]]
+        );
+    }
+
+    #[test]
+    fn convert_request_message_rejects_tampered_wire_body() {
+        // A flipped payload byte invalidates the producer's per-message
+        // checksum; admission must refuse the batch instead of stamping it.
+        let namespace = IggyNamespace::new(1, 1, 3);
+        let mut body = wire_send_messages_body(&sample_messages());
+        let last = body.len() - 1;
+        body[last] ^= 0xFF;
+        let result =
+            convert_request_message(namespace, wire_request_message(&body), ChecksumMode::Skip);
+        assert!(matches!(result, Err(IggyError::InvalidMessageChecksum(..))));
+    }
+
+    #[test]
+    fn convert_request_message_rejects_metadata_count_mismatch() {
+        let namespace = IggyNamespace::new(1, 1, 3);
+        let mut body = wire_send_messages_body(&sample_messages());
+        // The metadata count is the 4 bytes right before the batch header:
+        // metadata = [stream][topic][partitioning][count].
+        let metadata_length =
+            u32::from_le_bytes(body[..4].try_into().expect("4-byte slice")) as usize;
+        let count_offset = 4 + metadata_length - 4;
+        body[count_offset..count_offset + 4].copy_from_slice(&9u32.to_le_bytes());
+        let result =
+            convert_request_message(namespace, wire_request_message(&body), ChecksumMode::Skip);
+        assert!(matches!(result, Err(IggyError::InvalidCommand)));
+    }
+
+    #[test]
+    fn convert_request_message_skip_leaves_batch_checksum_zero_until_stamp() {
+        // The partition ingest path passes Skip: the admitted batch must carry
+        // a zero checksum (stamp fills it) and be otherwise byte-identical to
+        // the Compute output - the flag toggles nothing but that one hash.
+        let namespace = IggyNamespace::new(1, 1, 3);
+        let messages = sample_messages();
+        let body = wire_send_messages_body(&messages);
+        let header_size = std::mem::size_of::<RoutedRequestHeader>();
+
+        let computed = convert_request_message(
+            namespace,
+            wire_request_message(&body),
+            ChecksumMode::Compute,
+        )
+        .expect("compute admission");
+        let skipped =
+            convert_request_message(namespace, wire_request_message(&body), ChecksumMode::Skip)
+                .expect("skip admission");
+
+        let computed_body = &computed.as_slice()[header_size..computed.header().size as usize];
+        let skipped_body = &skipped.as_slice()[header_size..skipped.header().size as usize];
+
+        let skipped_header = BatchHeader::decode(&skipped_body[..COMMAND_HEADER_SIZE])
+            .expect("decode skipped header");
+        assert_eq!(
+            skipped_header.batch_checksum, 0,
+            "skip leaves the batch checksum zero until stamp",
+        );
+
+        // Patch only the 8-byte batch_checksum field into the skipped body; it
+        // must then equal the computed body, proving nothing else diverges.
+        let mut patched = skipped_body.to_vec();
+        patched[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]
+            .copy_from_slice(&computed_body[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]);
+        assert_eq!(
+            patched.as_slice(),
+            computed_body,
+            "skip and compute differ only in the batch_checksum field",
+        );
+    }
+
+    #[test]
+    fn encrypt_ingest_path_stays_canonical_through_flag_split() {
+        // Mirror the plane encrypt ingest sequence: convert(Compute) -> the
+        // validating decode encrypt performs on its input -> encrypt -> the
+        // validating decode the second convert performs as its discriminator ->
+        // convert(Skip) (the partition convert), which sees an already-canonical
+        // batch and returns it unchanged. Every decode must succeed.
+        let namespace = IggyNamespace::new(1, 1, 3);
+        let messages = sample_messages();
+        let header_size = std::mem::size_of::<RoutedRequestHeader>();
+
+        let wire = wire_request_message(&wire_send_messages_body(&messages));
+        let canonical = convert_request_message(namespace, wire, ChecksumMode::Compute)
+            .expect("pre-encrypt admission");
+        let canonical_body = &canonical.as_slice()[header_size..canonical.header().size as usize];
+        decode_batch_slice(canonical_body).expect("encrypt input decode validates the checksum");
+
+        let encryptor =
+            EncryptorKind::Aes256Gcm(Aes256GcmEncryptor::new(&[7u8; 32]).expect("valid 32B key"));
+        let encrypted = encrypt_batch_request(canonical, &encryptor).expect("encrypt batch");
+        let encrypted_body: Vec<u8> =
+            encrypted.as_slice()[header_size..encrypted.header().size as usize].to_vec();
+        decode_batch_slice(&encrypted_body)
+            .expect("encrypt output drives the 2nd-convert discriminator");
+
+        let repassed = convert_request_message(namespace, encrypted, ChecksumMode::Skip)
+            .expect("second convert passes the canonical batch");
+        let repassed_body = &repassed.as_slice()[header_size..repassed.header().size as usize];
+        assert_eq!(
+            repassed_body,
+            encrypted_body.as_slice(),
+            "an already-canonical encrypted batch passes the partition convert untouched",
+        );
+    }
+
+    /// Junk suffixes that must be refused at both ingest boundaries: one below a
+    /// frame header (the frame walk stops on a short read) and one frame-sized
+    /// but undecodable (`reserved != 0`). Neither is covered by any checksum, so
+    /// a walk that stops at the last decodable frame cannot see them.
+    const TRAILING_JUNK_CASES: [&[u8]; 2] = [&[0xAA], &[0xFF; 64]];
+
+    /// Canonical `SendMessages` request carrying `junk` past `batch_length`, with
+    /// `RoutedRequestHeader.size` inflated to cover it. `size` and `batch_length` are
+    /// independent wire fields, so a non-conforming client can emit this.
+    fn canonical_request_with_trailing_bytes(junk: &[u8]) -> Message<RoutedRequestHeader> {
+        let namespace = IggyNamespace::new(1, 1, 3);
+        let owned =
+            SendMessagesOwned::from_messages(namespace, &sample_messages()).expect("build batch");
+        let header_size = std::mem::size_of::<RoutedRequestHeader>();
+        let total = header_size + owned.header.total_size() + junk.len();
+        let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
+        {
+            let header: &mut RoutedRequestHeader =
+                bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
+                    .expect("zeroed bytes form a valid RoutedRequestHeader");
+            header.command = Command::Request;
+            header.operation = Operation::SendMessages;
+            header.client = 1;
+            header.session = 1;
+            header.request = 1;
+            header.size = u32::try_from(total).expect("size fits u32");
+        }
+        let bytes = buffer.as_mut_slice();
+        owned
+            .header
+            .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
+        let blob_end = PREPARE_SPLIT_POINT + owned.blob.len();
+        bytes[PREPARE_SPLIT_POINT..blob_end].copy_from_slice(&owned.blob);
+        bytes[blob_end..].copy_from_slice(junk);
+        Message::try_from(buffer).expect("request message is valid")
+    }
+
+    /// A `Prepare` whose `size` covers `junk` past `batch_length`.
+    fn prepare_with_trailing_bytes(junk: &[u8]) -> Owned<MESSAGE_ALIGN> {
+        let namespace = IggyNamespace::new(1, 1, 7);
+        let owned =
+            SendMessagesOwned::from_messages(namespace, &sample_messages()).expect("build batch");
+        let header_size = std::mem::size_of::<PrepareHeader>();
+        let total = header_size + owned.header.total_size() + junk.len();
+        let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
+        {
+            let prepare: &mut PrepareHeader =
+                bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
+                    .expect("zeroed bytes form a valid PrepareHeader");
+            prepare.command = Command::Prepare;
+            prepare.size = u32::try_from(total).expect("prepare size fits u32");
+        }
+        let bytes = buffer.as_mut_slice();
+        owned
+            .header
+            .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
+        let blob_end = PREPARE_SPLIT_POINT + owned.blob.len();
+        bytes[PREPARE_SPLIT_POINT..blob_end].copy_from_slice(&owned.blob);
+        bytes[blob_end..].copy_from_slice(junk);
+        buffer
+    }
+
+    #[test]
+    fn convert_request_message_rejects_canonical_batch_with_trailing_bytes() {
+        // Client ingest boundary. Accepting the request would carry the suffix
+        // into the journal and onto disk: the flush writes the whole frame while
+        // every reader advances by `batch_length`, so the segment walk lands
+        // inside the junk and every later batch becomes unreadable.
+        let namespace = IggyNamespace::new(1, 1, 3);
+        for junk in TRAILING_JUNK_CASES {
+            for mode in [ChecksumMode::Compute, ChecksumMode::Skip] {
+                let message = canonical_request_with_trailing_bytes(junk);
+                let result = convert_request_message(namespace, message, mode);
+                assert!(
+                    matches!(result, Err(IggyError::InvalidCommand)),
+                    "{} trailing bytes ({mode:?}) must be rejected, got {result:?}",
+                    junk.len(),
+                );
+            }
+        }
+    }
+
+    #[test]
+    fn convert_request_message_accepts_exact_canonical_batch() {
+        // The same builder with no suffix must still pass untouched, so the
+        // rejection above is the suffix and not the fixture.
+        let namespace = IggyNamespace::new(1, 1, 3);
+        let message = canonical_request_with_trailing_bytes(&[]);
+        let expected = message.as_slice().to_vec();
+        let converted = convert_request_message(namespace, message, ChecksumMode::Skip)
+            .expect("an exact canonical batch passes untouched");
+        assert_eq!(converted.as_slice(), expected.as_slice());
+    }
+
+    #[test]
+    fn decode_prepare_slice_rejects_trailing_bytes_past_batch_length() {
+        // Replica ingest must reject bytes beyond `batch_length` because no
+        // per-message checksum covers them.
+        for junk in TRAILING_JUNK_CASES {
+            let owned = prepare_with_trailing_bytes(junk);
+            assert!(
+                matches!(
+                    decode_prepare_slice(owned.as_slice()),
+                    Err(IggyError::InvalidCommand)
+                ),
+                "{} trailing bytes must fail the validating decode",
+                junk.len(),
+            );
+        }
+    }
+}
diff --git a/core/server_common/src/send_messages2.rs b/core/server_common/src/send_messages2.rs
deleted file mode 100644
index 4dacba2..0000000
--- a/core/server_common/src/send_messages2.rs
+++ /dev/null
@@ -1,1643 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use crate::consensus_message::{MESSAGE_ALIGN, Message};
-use crate::iobuf::Owned;
-use crate::sharding::IggyNamespace;
-use bytes::{Bytes, BytesMut};
-use iggy_binary_protocol::{PrepareHeader, RoutedRequestHeader};
-use iggy_common::{EncryptorKind, INDEX_SIZE, IggyError, random_id};
-use std::hash::Hasher;
-use twox_hash::XxHash3_64;
-
-pub const COMMAND_HEADER_SIZE: usize = 256;
-pub const PREPARE_SPLIT_POINT: usize = 512;
-const MESSAGE_HEADER_SIZE: usize = 48;
-const LEGACY_MESSAGE_HEADER_SIZE: usize = 64;
-const BATCH_CHECKSUM_OFFSET: usize = 40;
-const MESSAGE_COUNT_OFFSET: usize = 48;
-const MAX_TIMESTAMP_DELTA_MICROS: u64 = u32::MAX as u64;
-
-#[derive(Debug, Clone, Copy, Default)]
-pub struct SendMessages2Header {
-    pub partition_id: u64,
-    pub base_offset: u64,
-    pub base_timestamp: u64,
-    pub origin_timestamp: u64,
-    pub batch_length: u64,
-    pub batch_checksum: u64,
-    pub message_count: u32,
-}
-
-impl SendMessages2Header {
-    pub const fn new(
-        partition_id: u64,
-        origin_timestamp: u64,
-        batch_length: u64,
-        message_count: u32,
-    ) -> Self {
-        Self {
-            partition_id,
-            base_offset: 0,
-            base_timestamp: 0,
-            origin_timestamp,
-            batch_length,
-            batch_checksum: 0,
-            message_count,
-        }
-    }
-
-    pub fn decode(bytes: &[u8]) -> Result<Self, IggyError> {
-        if bytes.len() < COMMAND_HEADER_SIZE {
-            return Err(IggyError::InvalidCommand);
-        }
-
-        let batch_length = read_u64(bytes, 32)?;
-        if batch_length < COMMAND_HEADER_SIZE as u64 {
-            return Err(IggyError::InvalidCommand);
-        }
-
-        Ok(Self {
-            partition_id: read_u64(bytes, 0)?,
-            base_offset: read_u64(bytes, 8)?,
-            base_timestamp: read_u64(bytes, 16)?,
-            origin_timestamp: read_u64(bytes, 24)?,
-            batch_length,
-            batch_checksum: read_u64(bytes, BATCH_CHECKSUM_OFFSET)?,
-            message_count: read_u32(bytes, MESSAGE_COUNT_OFFSET)?,
-        })
-    }
-
-    pub fn encode_into(&self, bytes: &mut [u8]) {
-        assert!(bytes.len() >= COMMAND_HEADER_SIZE);
-        bytes[..COMMAND_HEADER_SIZE].fill(0);
-        bytes[0..8].copy_from_slice(&self.partition_id.to_le_bytes());
-        bytes[8..16].copy_from_slice(&self.base_offset.to_le_bytes());
-        bytes[16..24].copy_from_slice(&self.base_timestamp.to_le_bytes());
-        bytes[24..32].copy_from_slice(&self.origin_timestamp.to_le_bytes());
-        bytes[32..40].copy_from_slice(&self.batch_length.to_le_bytes());
-        bytes[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]
-            .copy_from_slice(&self.batch_checksum.to_le_bytes());
-        bytes[MESSAGE_COUNT_OFFSET..MESSAGE_COUNT_OFFSET + 4]
-            .copy_from_slice(&self.message_count.to_le_bytes());
-    }
-
-    pub fn total_size(&self) -> usize {
-        usize::try_from(self.batch_length).expect("batch length exceeds usize::MAX")
-    }
-
-    pub fn blob_len(&self) -> Result<usize, IggyError> {
-        usize::try_from(
-            self.batch_length
-                .checked_sub(COMMAND_HEADER_SIZE as u64)
-                .ok_or(IggyError::InvalidCommand)?,
-        )
-        .map_err(|_| IggyError::InvalidCommand)
-    }
-
-    pub fn into_frozen(self) -> FrozenBatchHeader {
-        let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(COMMAND_HEADER_SIZE);
-        self.encode_into(buffer.as_mut_slice());
-        buffer.into()
-    }
-
-    #[must_use]
-    pub fn checksum_for_blob(&self, blob: &[u8]) -> u64 {
-        calculate_batch_checksum(self, blob)
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct SendMessages2Owned {
-    pub header: SendMessages2Header,
-    pub blob: Bytes,
-}
-
-impl SendMessages2Owned {
-    pub fn from_messages(
-        namespace: IggyNamespace,
-        messages: &IggyMessages2,
-    ) -> Result<Self, IggyError> {
-        let message_count = messages.count();
-        let mut origin_timestamp = u64::MAX;
-        for message in messages {
-            origin_timestamp = origin_timestamp.min(message.header.origin_timestamp);
-        }
-
-        if origin_timestamp == u64::MAX {
-            origin_timestamp = 0;
-        }
-
-        let mut blob = BytesMut::new();
-        for (index, message) in messages.iter().enumerate() {
-            let id = if message.header.id == 0 {
-                random_id::get_uuid()
-            } else {
-                message.header.id
-            };
-            let offset_delta = u32::try_from(index).map_err(|_| IggyError::InvalidCommand)?;
-            let timestamp_delta = message
-                .header
-                .origin_timestamp
-                .checked_sub(origin_timestamp)
-                .ok_or(IggyError::InvalidCommand)?;
-            if timestamp_delta > MAX_TIMESTAMP_DELTA_MICROS {
-                return Err(IggyError::InvalidMessageTimestampDelta(timestamp_delta));
-            }
-            let timestamp_delta =
-                u32::try_from(timestamp_delta).map_err(|_| IggyError::InvalidCommand)?;
-            let user_headers = message.user_headers.as_deref().unwrap_or_default();
-            let user_headers_length =
-                u32::try_from(user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
-            let payload_length =
-                u32::try_from(message.payload.len()).map_err(|_| IggyError::InvalidCommand)?;
-
-            let mut header = [0u8; MESSAGE_HEADER_SIZE];
-            header[8..24].copy_from_slice(&id.to_le_bytes());
-            header[24..28].copy_from_slice(&offset_delta.to_le_bytes());
-            header[28..32].copy_from_slice(&timestamp_delta.to_le_bytes());
-            header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
-            header[36..40].copy_from_slice(&payload_length.to_le_bytes());
-
-            let msg_start = blob.len();
-            blob.extend_from_slice(&header);
-            blob.extend_from_slice(&message.payload);
-            blob.extend_from_slice(user_headers);
-            let checksum = XxHash3_64::oneshot(&blob[msg_start + 8..]);
-            blob[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
-        }
-
-        let blob = blob.freeze();
-        let mut header = SendMessages2Header::new(
-            namespace.partition_id() as u64,
-            origin_timestamp,
-            u64::try_from(COMMAND_HEADER_SIZE + blob.len())
-                .map_err(|_| IggyError::InvalidCommand)?,
-            message_count,
-        );
-        header.batch_checksum = calculate_batch_checksum(&header, &blob);
-
-        Ok(Self { header, blob })
-    }
-
-    pub fn encode_request(
-        self,
-        mut request_header: RoutedRequestHeader,
-    ) -> Result<Message<RoutedRequestHeader>, IggyError> {
-        let total_size = std::mem::size_of::<RoutedRequestHeader>() + self.header.total_size();
-        // The converted body differs in size from the legacy wire body the
-        // header described; a stale `size` truncates the rebuilt blob for
-        // every downstream slice (stamping, journal reads).
-        request_header.size = u32::try_from(total_size).map_err(|_| IggyError::InvalidCommand)?;
-        let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total_size);
-        let bytes = buffer.as_mut_slice();
-        bytes[0..std::mem::size_of::<RoutedRequestHeader>()]
-            .copy_from_slice(bytemuck::bytes_of(&request_header));
-        self.header.encode_into(
-            &mut bytes[std::mem::size_of::<RoutedRequestHeader>()
-                ..std::mem::size_of::<RoutedRequestHeader>() + COMMAND_HEADER_SIZE],
-        );
-        bytes[PREPARE_SPLIT_POINT..PREPARE_SPLIT_POINT + self.blob.len()]
-            .copy_from_slice(&self.blob);
-
-        Message::try_from(buffer).map_err(|_| IggyError::InvalidCommand)
-    }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub struct IggyMessage2Header {
-    pub checksum: u64,
-    pub id: u128,
-    pub offset: u64,
-    pub timestamp: u64,
-    pub origin_timestamp: u64,
-    pub user_headers_length: u32,
-    pub payload_length: u32,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct IggyMessage2 {
-    pub header: IggyMessage2Header,
-    pub payload: Bytes,
-    pub user_headers: Option<Bytes>,
-}
-
-#[derive(Debug, Clone, Default, PartialEq, Eq)]
-pub struct IggyMessages2 {
-    messages: Vec<IggyMessage2>,
-}
-
-impl IggyMessages2 {
-    #[must_use]
-    pub fn empty() -> Self {
-        Self::default()
-    }
-
-    #[must_use]
-    pub fn with_capacity(capacity: usize) -> Self {
-        Self {
-            messages: Vec::with_capacity(capacity),
-        }
-    }
-
-    pub fn push(&mut self, message: IggyMessage2) {
-        self.messages.push(message);
-    }
-
-    #[must_use]
-    pub fn count(&self) -> u32 {
-        u32::try_from(self.messages.len()).unwrap_or(u32::MAX)
-    }
-
-    #[must_use]
-    pub const fn is_empty(&self) -> bool {
-        self.messages.is_empty()
-    }
-
-    #[must_use]
-    pub fn first_offset(&self) -> Option<u64> {
-        self.messages.first().map(|message| message.header.offset)
-    }
-
-    #[must_use]
-    pub fn last_offset(&self) -> Option<u64> {
-        self.messages.last().map(|message| message.header.offset)
-    }
-
-    #[must_use]
-    pub fn limit(self, count: u32) -> Self {
-        let mut messages = self.messages;
-        messages.truncate(usize::try_from(count).unwrap_or(usize::MAX));
-        Self { messages }
-    }
-
-    pub fn iter(&self) -> std::slice::Iter<'_, IggyMessage2> {
-        self.messages.iter()
-    }
-}
-
-impl IntoIterator for IggyMessages2 {
-    type Item = IggyMessage2;
-    type IntoIter = std::vec::IntoIter<IggyMessage2>;
-
-    fn into_iter(self) -> Self::IntoIter {
-        self.messages.into_iter()
-    }
-}
-
-impl<'a> IntoIterator for &'a IggyMessages2 {
-    type Item = &'a IggyMessage2;
-    type IntoIter = std::slice::Iter<'a, IggyMessage2>;
-
-    fn into_iter(self) -> Self::IntoIter {
-        self.messages.iter()
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct SendMessages2Ref<'a> {
-    pub header: SendMessages2Header,
-    blob: &'a [u8],
-}
-
-#[allow(dead_code)]
-impl<'a> SendMessages2Ref<'a> {
-    pub const fn iter(&self) -> SendMessages2Iterator<'a> {
-        SendMessages2Iterator {
-            blob: self.blob,
-            position: 0,
-        }
-    }
-
-    pub const fn iter_with_offsets(&self) -> SendMessages2IteratorWithOffsets<'a> {
-        SendMessages2IteratorWithOffsets {
-            blob: self.blob,
-            position: 0,
-        }
-    }
-
-    pub const fn blob(&self) -> &'a [u8] {
-        self.blob
-    }
-
-    pub const fn message_count(&self) -> u32 {
-        self.header.message_count
-    }
-}
-
-#[allow(dead_code)]
-#[derive(Debug, Clone, Copy)]
-pub struct SendMessages2MessageHeader {
-    pub checksum: u64,
-    pub id: u128,
-    pub offset_delta: u32,
-    /// Microsecond delta from `SendMessages2Header::origin_timestamp`.
-    ///
-    /// This is stored in `u32`, which limits a single batch to roughly
-    /// 71.6 minutes of origin timestamp span.
-    pub timestamp_delta: u32,
-    pub user_headers_length: u32,
-    pub payload_length: u32,
-}
-
-impl SendMessages2MessageHeader {
-    fn decode(bytes: &[u8]) -> Result<Self, IggyError> {
-        if bytes.len() < MESSAGE_HEADER_SIZE {
-            return Err(IggyError::InvalidCommand);
-        }
-
-        let reserved = read_u64(bytes, 40)?;
-        if reserved != 0 {
-            return Err(IggyError::InvalidCommand);
-        }
-
-        Ok(Self {
-            checksum: read_u64(bytes, 0)?,
-            id: read_u128(bytes, 8)?,
-            offset_delta: read_u32(bytes, 24)?,
-            timestamp_delta: read_u32(bytes, 28)?,
-            user_headers_length: read_u32(bytes, 32)?,
-            payload_length: read_u32(bytes, 36)?,
-        })
-    }
-
-    const fn total_size(&self) -> usize {
-        MESSAGE_HEADER_SIZE + self.user_headers_length as usize + self.payload_length as usize
-    }
-}
-
-#[allow(dead_code)]
-#[derive(Debug, Clone, Copy)]
-pub struct SendMessages2MessageView<'a> {
-    pub header: SendMessages2MessageHeader,
-    pub user_headers: &'a [u8],
-    pub payload: &'a [u8],
-}
-
-#[allow(dead_code)]
-pub struct SendMessages2Iterator<'a> {
-    blob: &'a [u8],
-    position: usize,
-}
-
-impl<'a> Iterator for SendMessages2Iterator<'a> {
-    type Item = SendMessages2MessageView<'a>;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        if self.position >= self.blob.len() {
-            return None;
-        }
-
-        let header = SendMessages2MessageHeader::decode(&self.blob[self.position..]).ok()?;
-        let start = self.position + MESSAGE_HEADER_SIZE;
-        let payload_end = start + header.payload_length as usize;
-        let headers_end = payload_end + header.user_headers_length as usize;
-        let payload = self.blob.get(start..payload_end)?;
-        let user_headers = self.blob.get(payload_end..headers_end)?;
-        self.position += header.total_size();
-        Some(SendMessages2MessageView {
-            header,
-            user_headers,
-            payload,
-        })
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct SendMessages2MessageViewWithOffsets<'a> {
-    pub message: SendMessages2MessageView<'a>,
-    pub start: usize,
-    pub end: usize,
-}
-
-pub struct SendMessages2IteratorWithOffsets<'a> {
-    blob: &'a [u8],
-    position: usize,
-}
-
-impl<'a> Iterator for SendMessages2IteratorWithOffsets<'a> {
-    type Item = SendMessages2MessageViewWithOffsets<'a>;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        if self.position >= self.blob.len() {
-            return None;
-        }
-
-        let start = self.position;
-        let header = SendMessages2MessageHeader::decode(&self.blob[self.position..]).ok()?;
-        let message_start = self.position + MESSAGE_HEADER_SIZE;
-        let payload_end = message_start + header.payload_length as usize;
-        let headers_end = payload_end + header.user_headers_length as usize;
-        let payload = self.blob.get(message_start..payload_end)?;
-        let user_headers = self.blob.get(payload_end..headers_end)?;
-        self.position += header.total_size();
-        Some(SendMessages2MessageViewWithOffsets {
-            message: SendMessages2MessageView {
-                header,
-                user_headers,
-                payload,
-            },
-            start,
-            end: self.position,
-        })
-    }
-}
-
-pub(crate) type FrozenBatchHeader = crate::iobuf::Frozen<MESSAGE_ALIGN>;
-
-/// Re-encode a canonical `SendMessages2` request with every message's payload
-/// and user headers encrypted, per-message checksums and lengths recomputed,
-/// and the batch header (length + checksum) restamped.
-///
-/// Runs ONCE, on the primary at ingestion (after [`convert_request_message`]
-/// canonicalized the wire form), so the ciphertext is what replicates: every
-/// replica journals and persists identical bytes, and the poll path decrypts
-/// uniformly regardless of which replica or tier served the fragment.
-///
-/// # Errors
-///
-/// [`IggyError::InvalidCommand`] on an undecodable batch; encryption errors
-/// propagate from the encryptor.
-pub fn encrypt_batch_request(
-    message: Message<RoutedRequestHeader>,
-    encryptor: &EncryptorKind,
-) -> Result<Message<RoutedRequestHeader>, IggyError> {
-    let request_header = *message.header();
-    let total_size = request_header.size as usize;
-    let body = &message.as_slice()[std::mem::size_of::<RoutedRequestHeader>()..total_size];
-    let batch = decode_batch_slice(body)?;
-
-    let mut blob = BytesMut::with_capacity(batch.blob().len() * 2);
-    for view in batch.iter() {
-        let encrypted_payload = encryptor.encrypt(view.payload)?;
-        let encrypted_user_headers = if view.user_headers.is_empty() {
-            None
-        } else {
-            Some(encryptor.encrypt(view.user_headers)?)
-        };
-        let user_headers: &[u8] = encrypted_user_headers.as_deref().unwrap_or_default();
-        let payload_length =
-            u32::try_from(encrypted_payload.len()).map_err(|_| IggyError::InvalidCommand)?;
-        let user_headers_length =
-            u32::try_from(user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
-
-        let mut header = [0u8; MESSAGE_HEADER_SIZE];
-        header[8..24].copy_from_slice(&view.header.id.to_le_bytes());
-        header[24..28].copy_from_slice(&view.header.offset_delta.to_le_bytes());
-        header[28..32].copy_from_slice(&view.header.timestamp_delta.to_le_bytes());
-        header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
-        header[36..40].copy_from_slice(&payload_length.to_le_bytes());
-        let msg_start = blob.len();
-        blob.extend_from_slice(&header);
-        blob.extend_from_slice(&encrypted_payload);
-        blob.extend_from_slice(user_headers);
-        let checksum = XxHash3_64::oneshot(&blob[msg_start + 8..]);
-        blob[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
-    }
-
-    let blob = blob.freeze();
-    let mut header = batch.header;
-    header.batch_length =
-        u64::try_from(COMMAND_HEADER_SIZE + blob.len()).map_err(|_| IggyError::InvalidCommand)?;
-    header.batch_checksum = calculate_batch_checksum(&header, &blob);
-
-    SendMessages2Owned { header, blob }.encode_request(request_header)
-}
-
-/// Whether the legacy transcode stamps a batch checksum onto its output.
-///
-/// The recompute is an `XxHash3` batch-checksum pass, needed only when a reader
-/// validates the transcoded batch before [`stamp_prepare_for_persistence`]
-/// recomputes it: the encrypt ingest path re-decodes the canonicalized batch
-/// (`encrypt_batch_request`'s validating decode, then the second `convert` its
-/// output re-enters as the canonical-vs-legacy discriminator). The partition
-/// ingest path has no such reader, so it skips the pass and the checksum stays
-/// zero until stamp.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ChecksumMode {
-    /// Compute the batch checksum for the transcoded batch.
-    Compute,
-    /// Leave the batch checksum zero; `stamp_prepare_for_persistence` fills it.
-    Skip,
-}
-
-pub fn convert_request_message(
-    namespace: IggyNamespace,
-    message: Message<RoutedRequestHeader>,
-    checksum: ChecksumMode,
-) -> Result<Message<RoutedRequestHeader>, IggyError> {
-    let request_header = *message.header();
-    let total_size = request_header.size as usize;
-    let body = &message.as_slice()[std::mem::size_of::<RoutedRequestHeader>()..total_size];
-    // A canonical body enters the pipeline verbatim, so it must end exactly at
-    // `batch_length`: `size` and `batch_length` are independent client-supplied
-    // fields and `decode_batch_slice` only lower-bounds the frame. A suffix past
-    // `batch_length` is covered by no checksum, still rides the buffer to disk,
-    // and desyncs the segment walk that advances by `batch_length`.
-    match decode_batch_slice(body) {
-        Ok(batch) if batch.message_count() == 0 => Err(IggyError::InvalidCommand),
-        Ok(batch) if body.len() == batch.header.total_size() => Ok(message),
-        Ok(_) => Err(IggyError::InvalidCommand),
-        Err(_) => transcode_legacy_request(namespace, body, request_header, checksum),
-    }
-}
-
-/// Transcode a legacy `SendMessages` request body directly into the canonical
-/// `[RoutedRequestHeader][256B SendMessages2Header][blob]` form, writing each message
-/// record straight into the final aligned buffer.
-///
-/// Fused replacement for the `from_legacy_request(..).encode_request(..)`
-/// two-step: a size walk over the legacy input sizes the single output
-/// allocation, then a write walk lays down each canonical record in place. This
-/// drops the intermediate blob allocation and the full-blob copy the two-step
-/// paid. Output bytes are identical to that path.
-///
-/// `checksum` selects whether the output carries a batch checksum (see
-/// [`ChecksumMode`]); [`ChecksumMode::Skip`] leaves it zero for the partition
-/// ingest path, where stamp recomputes it.
-fn transcode_legacy_request(
-    namespace: IggyNamespace,
-    body: &[u8],
-    mut request_header: RoutedRequestHeader,
-    checksum: ChecksumMode,
-) -> Result<Message<RoutedRequestHeader>, IggyError> {
-    let (message_count, messages) = legacy_messages_slice(body)?;
-    if message_count == 0 {
-        return Err(IggyError::InvalidCommand);
-    }
-    let mut parsed = Vec::with_capacity(message_count as usize);
-    let mut origin_timestamp = u64::MAX;
-    let mut cursor = 0usize;
-    let mut blob_len = 0usize;
-
-    while cursor < messages.len() && parsed.len() < message_count as usize {
-        let legacy = LegacyMessageRef::decode(&messages[cursor..])?;
-        origin_timestamp = origin_timestamp.min(legacy.origin_timestamp);
-        cursor += legacy.total_size;
-        blob_len = blob_len
-            .checked_add(MESSAGE_HEADER_SIZE + legacy.payload.len() + legacy.user_headers.len())
-            .ok_or(IggyError::InvalidCommand)?;
-        parsed.push(legacy);
-    }
-
-    if parsed.len() != message_count as usize || cursor != messages.len() {
-        return Err(IggyError::InvalidCommand);
-    }
-
-    if origin_timestamp == u64::MAX {
-        origin_timestamp = 0;
-    }
-
-    let header_size = std::mem::size_of::<RoutedRequestHeader>();
-    let batch_length = COMMAND_HEADER_SIZE
-        .checked_add(blob_len)
-        .ok_or(IggyError::InvalidCommand)?;
-    let total_size = header_size
-        .checked_add(batch_length)
-        .ok_or(IggyError::InvalidCommand)?;
-    request_header.size = u32::try_from(total_size).map_err(|_| IggyError::InvalidCommand)?;
-
-    let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total_size);
-    let bytes = buffer.as_mut_slice();
-    bytes[0..header_size].copy_from_slice(bytemuck::bytes_of(&request_header));
-
-    let mut write = PREPARE_SPLIT_POINT;
-    for (index, legacy) in parsed.iter().enumerate() {
-        let id = if legacy.id == 0 {
-            random_id::get_uuid()
-        } else {
-            legacy.id
-        };
-        let offset_delta = u32::try_from(index).map_err(|_| IggyError::InvalidCommand)?;
-        let timestamp_delta = legacy
-            .origin_timestamp
-            .checked_sub(origin_timestamp)
-            .ok_or(IggyError::InvalidCommand)?;
-        if timestamp_delta > MAX_TIMESTAMP_DELTA_MICROS {
-            return Err(IggyError::InvalidMessageTimestampDelta(timestamp_delta));
-        }
-        let timestamp_delta =
-            u32::try_from(timestamp_delta).map_err(|_| IggyError::InvalidCommand)?;
-        let user_headers_length =
-            u32::try_from(legacy.user_headers.len()).map_err(|_| IggyError::InvalidCommand)?;
-        let payload_length =
-            u32::try_from(legacy.payload.len()).map_err(|_| IggyError::InvalidCommand)?;
-
-        let mut header = [0u8; MESSAGE_HEADER_SIZE];
-        header[8..24].copy_from_slice(&id.to_le_bytes());
-        header[24..28].copy_from_slice(&offset_delta.to_le_bytes());
-        header[28..32].copy_from_slice(&timestamp_delta.to_le_bytes());
-        header[32..36].copy_from_slice(&user_headers_length.to_le_bytes());
-        header[36..40].copy_from_slice(&payload_length.to_le_bytes());
-        let msg_start = write;
-        bytes[write..write + MESSAGE_HEADER_SIZE].copy_from_slice(&header);
-        write += MESSAGE_HEADER_SIZE;
-        bytes[write..write + legacy.payload.len()].copy_from_slice(legacy.payload);
-        write += legacy.payload.len();
-        bytes[write..write + legacy.user_headers.len()].copy_from_slice(legacy.user_headers);
-        write += legacy.user_headers.len();
-        // The cover is [msg_start + 8 .. write], including the 8 reserved zero
-        // header bytes. This relies on the stack header being zero-initialized,
-        // not on the output buffer being pre-zeroed.
-        let checksum = XxHash3_64::oneshot(&bytes[msg_start + 8..write]);
-        bytes[msg_start..msg_start + 8].copy_from_slice(&checksum.to_le_bytes());
-    }
-
-    let mut command = SendMessages2Header::new(
-        namespace.partition_id() as u64,
-        origin_timestamp,
-        batch_length as u64,
-        message_count,
-    );
-    if checksum == ChecksumMode::Compute {
-        command.batch_checksum = calculate_batch_checksum(
-            &command,
-            &bytes[PREPARE_SPLIT_POINT..PREPARE_SPLIT_POINT + blob_len],
-        );
-    }
-    command.encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
-
-    Message::try_from(buffer).map_err(|_| IggyError::InvalidCommand)
-}
-
-/// Decode one batch slice (`[256B command header][blob]`), validating the batch
-/// checksum and every per-message checksum. The persisted segment-file record
-/// and the request slice share this layout, so both decode through here.
-///
-/// `body` may extend past the batch: the poll disk walk hands in the rest of the
-/// chunk and steps by `batch_length`. Callers whose buffer is meant to BE the
-/// batch must reject the surplus themselves - see [`convert_request_message`].
-pub fn decode_batch_slice(body: &[u8]) -> Result<SendMessages2Ref<'_>, IggyError> {
-    decode_batch_slice_with(body, BatchIntegrity::Verify)
-}
-
-/// How much of a batch record [`decode_batch_slice_with`] proves before returning it.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum BatchIntegrity {
-    /// Re-hash the batch and reject it unless it matches its own `batch_checksum`.
-    Verify,
-    /// Check the framing only, and hand back whatever it describes. The caller is
-    /// accepting bytes that may not be the ones written.
-    LayoutOnly,
-}
-
-/// [`decode_batch_slice`] with the integrity level chosen by the caller.
-///
-/// An enum, not a bool: the one caller that passes anything but
-/// [`BatchIntegrity::Verify`] is the disk poll under its operator knob, and
-/// `..., false)` there reads like a detail rather than opting a read out of
-/// corruption detection.
-///
-/// Layout checks are not optional either way: a short or self-inconsistent record is
-/// rejected regardless, because the caller would otherwise index past it.
-///
-/// # Errors
-/// [`IggyError::InvalidCommand`] for a short or inconsistent record, and
-/// [`IggyError::InvalidBatchChecksum`] under [`BatchIntegrity::Verify`] when the batch
-/// does not match. Callers that must tell corruption from a partial tail need both.
-pub fn decode_batch_slice_with(
-    body: &[u8],
-    integrity: BatchIntegrity,
-) -> Result<SendMessages2Ref<'_>, IggyError> {
-    if body.len() < COMMAND_HEADER_SIZE {
-        return Err(IggyError::InvalidCommand);
-    }
-
-    let header = SendMessages2Header::decode(&body[..COMMAND_HEADER_SIZE])?;
-    let blob_len = header.blob_len()?;
-    if body.len() < header.total_size() {
-        return Err(IggyError::InvalidCommand);
-    }
-
-    let blob = &body[COMMAND_HEADER_SIZE..COMMAND_HEADER_SIZE + blob_len];
-    let batch = SendMessages2Ref { header, blob };
-    match integrity {
-        BatchIntegrity::Verify => {
-            let expected_checksum = verify_and_recompute_batch_checksum(&batch)?;
-            if header.batch_checksum != expected_checksum {
-                return Err(IggyError::InvalidBatchChecksum(
-                    header.batch_checksum,
-                    expected_checksum,
-                    header.base_offset,
-                ));
-            }
-        }
-        BatchIntegrity::LayoutOnly => validate_batch_layout(&batch)?,
-    }
-
-    Ok(batch)
-}
-
-/// Decode a `Prepare` message from a slice of bytes, validating the batch
-/// checksum and every per-message checksum.
-///
-/// `bytes` must be 16-byte aligned (`PrepareHeader` has `u128` fields). Source
-/// from `Frozen<MESSAGE_ALIGN>` / `Owned<MESSAGE_ALIGN>` / `Message<H>`.
-/// Misalignment: `debug_assert!` in debug; `InvalidCommand` in release.
-///
-/// # Errors
-///
-/// `IggyError::InvalidCommand` on a short buffer, bad bit pattern, `size`
-/// outside `[header_size, bytes.len()]`, a `size` that does not describe the
-/// batch exactly, or frames that do not tile the batch;
-/// `InvalidBatchChecksum` / `InvalidMessageChecksum` on an integrity mismatch.
-pub fn decode_prepare_slice(bytes: &[u8]) -> Result<SendMessages2Ref<'_>, IggyError> {
-    decode_prepare_slice_inner(bytes, true)
-}
-
-/// Like [`decode_prepare_slice`] but skips the per-message checksum
-/// verification and batch-checksum recompute, extracting only the header meta.
-/// Every cheap structural check (length, 16-byte alignment, `size` bounds, and
-/// `size` describing the batch exactly) is still enforced.
-///
-/// INVARIANT: `bytes` MUST be node-local self-stamped -
-/// [`stamp_prepare_for_persistence`] recomputed the batch checksum over the
-/// exact blob on the local node - or already integrity-checked at network
-/// ingress. There is no consensus-layer blob validation: the `PrepareHeader`
-/// integrity fields are inert zeros. Replicated and repaired prepares are
-/// validated via [`decode_prepare_slice`] before the bytes reach any trusted
-/// decode. Calling this on unvalidated network bytes would let a corrupted blob
-/// pass undetected. The full-body per-message checksum pass dominates
-/// produce-path CPU, so trusted call sites that only read header meta skip it.
-///
-/// # Errors
-///
-/// Same structural errors as [`decode_prepare_slice`], minus
-/// `InvalidBatchChecksum` and `InvalidMessageChecksum`.
-pub fn decode_prepare_slice_trusted(bytes: &[u8]) -> Result<SendMessages2Ref<'_>, IggyError> {
-    decode_prepare_slice_inner(bytes, false)
-}
-
-fn decode_prepare_slice_inner(
-    bytes: &[u8],
-    validate_checksum: bool,
-) -> Result<SendMessages2Ref<'_>, IggyError> {
-    let header_size = std::mem::size_of::<PrepareHeader>();
-    if bytes.len() < header_size {
-        return Err(IggyError::InvalidCommand);
-    }
-
-    // Bytemuck enforces alignment in release (maps to InvalidCommand below);
-    // debug_assert surfaces the contract violation early in dev.
-    debug_assert_eq!(
-        bytes
-            .as_ptr()
-            .align_offset(std::mem::align_of::<PrepareHeader>()),
-        0,
-        "decode_prepare_slice: bytes must be at least 16-byte aligned",
-    );
-
-    let prepare = bytemuck::checked::try_from_bytes::<PrepareHeader>(&bytes[..header_size])
-        .map_err(|_| IggyError::InvalidCommand)?;
-    let total_size = prepare.size as usize;
-    // Wire-controllable `size`: reject < header_size to avoid slice OOB below.
-    if total_size < header_size || bytes.len() < total_size {
-        return Err(IggyError::InvalidCommand);
-    }
-
-    let body = &bytes[header_size..total_size];
-    if body.len() < COMMAND_HEADER_SIZE {
-        return Err(IggyError::InvalidCommand);
-    }
-
-    let header = SendMessages2Header::decode(&body[..COMMAND_HEADER_SIZE])?;
-    let blob_len = header.blob_len()?;
-    // Exact, not a lower bound: a prepare frame IS one batch, so bytes past
-    // `batch_length` belong to nobody - no checksum covers them, yet the flush
-    // writes them, desyncing the segment walk. Readers walking a multi-batch
-    // chunk use `decode_batch_slice`, which bounds the blob by design.
-    if body.len() != header.total_size() {
-        return Err(IggyError::InvalidCommand);
-    }
-
-    let blob = &body[COMMAND_HEADER_SIZE..COMMAND_HEADER_SIZE + blob_len];
-    let batch = SendMessages2Ref { header, blob };
-    if validate_checksum {
-        let expected_checksum = verify_and_recompute_batch_checksum(&batch)?;
-        if header.batch_checksum != expected_checksum {
-            return Err(IggyError::InvalidBatchChecksum(
-                header.batch_checksum,
-                expected_checksum,
-                header.base_offset,
-            ));
-        }
-    }
-
-    Ok(batch)
-}
-
-pub fn stamp_prepare_for_persistence(
-    mut message: Message<PrepareHeader>,
-    base_offset: u64,
-    base_timestamp: u64,
-) -> Result<(Message<PrepareHeader>, SendMessages2Header, u32), IggyError> {
-    let total_size = message.header().size as usize;
-    let bytes = message.as_mut_slice();
-    if bytes.len() < PREPARE_SPLIT_POINT || total_size < PREPARE_SPLIT_POINT {
-        return Err(IggyError::InvalidCommand);
-    }
-
-    let header_offset = std::mem::size_of::<PrepareHeader>();
-    let mut command =
-        SendMessages2Header::decode(&bytes[header_offset..header_offset + COMMAND_HEADER_SIZE])?;
-    command.base_offset = base_offset;
-    command.base_timestamp = base_timestamp;
-    let blob = &bytes[PREPARE_SPLIT_POINT..total_size];
-    command.batch_checksum = calculate_batch_checksum(&command, blob);
-    command.encode_into(&mut bytes[header_offset..header_offset + COMMAND_HEADER_SIZE]);
-    Ok((message, command, command.message_count))
-}
-
-fn legacy_messages_slice(body: &[u8]) -> Result<(u32, &[u8]), IggyError> {
-    if body.len() < 4 {
-        return Err(IggyError::InvalidCommand);
-    }
-
-    let metadata_length = read_u32(body, 0)? as usize;
-    let metadata_end = 4usize
-        .checked_add(metadata_length)
-        .ok_or(IggyError::InvalidCommand)?;
-    if metadata_end < 4 || body.len() < metadata_end {
-        return Err(IggyError::InvalidCommand);
-    }
-
-    let message_count = read_u32(body, metadata_end - 4)?;
-    let indexes_len = usize::try_from(message_count)
-        .ok()
-        .and_then(|count| count.checked_mul(INDEX_SIZE))
-        .ok_or(IggyError::InvalidCommand)?;
-    let messages_start = metadata_end
-        .checked_add(indexes_len)
-        .ok_or(IggyError::InvalidCommand)?;
-    if body.len() < messages_start {
-        return Err(IggyError::InvalidCommand);
-    }
-
-    Ok((message_count, &body[messages_start..]))
-}
-
-#[derive(Clone, Copy)]
-struct LegacyMessageRef<'a> {
-    id: u128,
-    origin_timestamp: u64,
-    user_headers: &'a [u8],
-    payload: &'a [u8],
-    total_size: usize,
-}
-
-impl<'a> LegacyMessageRef<'a> {
-    fn decode(bytes: &'a [u8]) -> Result<Self, IggyError> {
-        if bytes.len() < LEGACY_MESSAGE_HEADER_SIZE {
-            return Err(IggyError::InvalidCommand);
-        }
-
-        let user_headers_length = read_u32(bytes, 48)? as usize;
-        let payload_length = read_u32(bytes, 52)? as usize;
-        let total_size = LEGACY_MESSAGE_HEADER_SIZE
-            .checked_add(payload_length)
-            .and_then(|size| size.checked_add(user_headers_length))
-            .ok_or(IggyError::InvalidCommand)?;
-        if bytes.len() < total_size {
-            return Err(IggyError::InvalidCommand);
-        }
-
-        let payload_start = LEGACY_MESSAGE_HEADER_SIZE;
-        let payload_end = payload_start + payload_length;
-        let headers_end = payload_end + user_headers_length;
-
-        Ok(Self {
-            id: read_u128(bytes, 8)?,
-            origin_timestamp: read_u64(bytes, 40)?,
-            user_headers: &bytes[payload_end..headers_end],
-            payload: &bytes[payload_start..payload_end],
-            total_size,
-        })
-    }
-}
-
-/// Batch checksum v2: streaming `XxHash3_64` over the six batch header meta
-/// fields followed by each message's stored 8-byte checksum field in message
-/// order - NOT the message bodies.
-///
-/// Bodies are bound only transitively: each per-message checksum already covers
-/// `header[8..48] || payload || user_headers`, so hashing the checksum fields
-/// binds every body byte IFF a reader also re-verifies the per-message
-/// checksums. Stamp (produce) hashes `N * 8` bytes instead of the whole blob;
-/// validating decoders pay the one body pass as the per-message verify in
-/// [`verify_and_recompute_batch_checksum`], which hashes the checksum-field
-/// bytes in the same order so its recompute matches a compute here.
-///
-/// Assumes a well-formed blob whose frames tile exactly; every compute site
-/// builds the blob and satisfies this.
-fn calculate_batch_checksum(header: &SendMessages2Header, blob: &[u8]) -> u64 {
-    let mut hasher = XxHash3_64::new();
-    write_batch_header_fields(&mut hasher, header);
-    let batch = SendMessages2Ref {
-        header: *header,
-        blob,
-    };
-    for framed in batch.iter_with_offsets() {
-        hasher.write(&blob[framed.start..framed.start + 8]);
-    }
-    hasher.finish()
-}
-
-fn write_batch_header_fields(hasher: &mut XxHash3_64, header: &SendMessages2Header) {
-    hasher.write(&header.partition_id.to_le_bytes());
-    hasher.write(&header.base_offset.to_le_bytes());
-    hasher.write(&header.base_timestamp.to_le_bytes());
-    hasher.write(&header.origin_timestamp.to_le_bytes());
-    hasher.write(&header.batch_length.to_le_bytes());
-    hasher.write(&header.message_count.to_le_bytes());
-}
-
-/// Verify every per-message checksum in `batch` and return the recomputed v2
-/// batch checksum (see [`calculate_batch_checksum`]) from a single frame walk.
-///
-/// The per-message pass is the equal-integrity half of v2: the batch value
-/// binds bodies only through the checksum fields, so a validating decode must
-/// re-verify each message here or body corruption that leaves the checksum
-/// field intact would pass. This is the one full-body pass a validating decode
-/// pays; the caller then compares the returned value against the stored
-/// `batch_checksum`.
-///
-/// # Errors
-///
-/// [`IggyError::InvalidMessageChecksum`] on the first per-message mismatch;
-/// [`IggyError::InvalidCommand`] if the frames do not tile `message_count`
-/// exactly.
-fn verify_and_recompute_batch_checksum(batch: &SendMessages2Ref<'_>) -> Result<u64, IggyError> {
-    let blob = batch.blob();
-    let mut hasher = XxHash3_64::new();
-    write_batch_header_fields(&mut hasher, &batch.header);
-    let mut verified = 0u32;
-    let mut covered = 0usize;
-    for framed in batch.iter_with_offsets() {
-        // Cover (`header[8..48] || payload || user_headers`) hashed raw from the
-        // blob, byte-exact with the encoder's, so a flipped body byte fails even
-        // when the stored checksum field is left intact.
-        let stored = framed.message.header.checksum;
-        let expected = XxHash3_64::oneshot(&blob[framed.start + 8..framed.end]);
-        if expected != stored {
-            return Err(IggyError::InvalidMessageChecksum(
-                stored,
-                expected,
-                batch.header.base_offset + u64::from(framed.message.header.offset_delta),
-            ));
-        }
-        hasher.write(&blob[framed.start..framed.start + 8]);
-        verified += 1;
-        covered = framed.end;
-    }
-    if verified != batch.message_count() || covered != blob.len() {
-        return Err(IggyError::InvalidCommand);
-    }
-    Ok(hasher.finish())
-}
-
-/// The layout half of [`verify_and_recompute_batch_checksum`], without the hashing.
-///
-/// A caller that opts out of checksum verification still must not be handed a batch
-/// whose framing disagrees with its header, since it indexes by `batch_length` and
-/// would step into the next record. Walking the frames costs no hashing.
-fn validate_batch_layout(batch: &SendMessages2Ref<'_>) -> Result<(), IggyError> {
-    let mut framed = 0u32;
-    let mut covered = 0usize;
-    for message in batch.iter_with_offsets() {
-        framed += 1;
-        covered = message.end;
-    }
-    if framed != batch.message_count() || covered != batch.blob().len() {
-        return Err(IggyError::InvalidCommand);
-    }
-    Ok(())
-}
-
-fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, IggyError> {
-    bytes
-        .get(offset..offset + 4)
-        .and_then(|slice| slice.try_into().ok())
-        .map(u32::from_le_bytes)
-        .ok_or(IggyError::InvalidNumberEncoding)
-}
-
-fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, IggyError> {
-    bytes
-        .get(offset..offset + 8)
-        .and_then(|slice| slice.try_into().ok())
-        .map(u64::from_le_bytes)
-        .ok_or(IggyError::InvalidNumberEncoding)
-}
-
-fn read_u128(bytes: &[u8], offset: usize) -> Result<u128, IggyError> {
-    bytes
-        .get(offset..offset + 16)
-        .and_then(|slice| slice.try_into().ok())
-        .map(u128::from_le_bytes)
-        .ok_or(IggyError::InvalidNumberEncoding)
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use iggy_binary_protocol::{Command2, Operation};
-    use iggy_common::Aes256GcmEncryptor;
-
-    fn aligned_prepare_bytes(size: u32) -> Owned<MESSAGE_ALIGN> {
-        let mut owned = Owned::<MESSAGE_ALIGN>::zeroed(std::mem::size_of::<PrepareHeader>());
-        let header: &mut PrepareHeader =
-            bytemuck::checked::try_from_bytes_mut(owned.as_mut_slice())
-                .expect("zeroed bytes form a valid PrepareHeader");
-        header.command = Command2::Prepare;
-        header.size = size;
-        owned
-    }
-
-    /// Assemble an already-stamped batch into a `Prepare`:
-    /// `[PrepareHeader][256B batch header][blob]`, copying `owned`'s header and
-    /// blob verbatim. Shared by every real-batch fixture.
-    fn prepare_from_owned(owned: &SendMessages2Owned) -> Owned<MESSAGE_ALIGN> {
-        let header_size = std::mem::size_of::<PrepareHeader>();
-        let total = header_size + owned.header.total_size();
-        let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
-        {
-            let prepare: &mut PrepareHeader =
-                bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
-                    .expect("zeroed bytes form a valid PrepareHeader");
-            prepare.command = Command2::Prepare;
-            prepare.size = u32::try_from(total).expect("prepare size fits u32");
-        }
-        let bytes = buffer.as_mut_slice();
-        owned
-            .header
-            .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
-        bytes[PREPARE_SPLIT_POINT..PREPARE_SPLIT_POINT + owned.blob.len()]
-            .copy_from_slice(&owned.blob);
-        buffer
-    }
-
-    /// A checksum-consistent STAMPED `Prepare` carrying real per-message records,
-    /// stamped at a non-zero `base_offset` / `base_timestamp` with a v2
-    /// `batch_checksum` over the final header fields + per-message checksum fields.
-    fn valid_prepare_bytes() -> Owned<MESSAGE_ALIGN> {
-        let namespace = IggyNamespace::new(1, 1, 7);
-        let mut owned = SendMessages2Owned::from_messages(namespace, &sample_messages())
-            .expect("build send batch");
-        owned.header.base_offset = 10;
-        owned.header.base_timestamp = 20;
-        owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
-        prepare_from_owned(&owned)
-    }
-
-    #[test]
-    fn decode_prepare_slice_trusted_matches_validating_for_valid_batch() {
-        // The trusted variant must surface byte-identical header meta to the
-        // validating decode for a checksum-consistent batch; only the
-        // per-message and batch-checksum passes are skipped.
-        let owned = valid_prepare_bytes();
-
-        let validated = decode_prepare_slice(owned.as_slice()).expect("valid batch decodes");
-        let trusted =
-            decode_prepare_slice_trusted(owned.as_slice()).expect("valid batch decodes trusted");
-
-        assert_eq!(validated.header.base_offset, trusted.header.base_offset);
-        assert_eq!(
-            validated.header.base_timestamp,
-            trusted.header.base_timestamp
-        );
-        assert_eq!(
-            validated.header.origin_timestamp,
-            trusted.header.origin_timestamp
-        );
-        assert_eq!(validated.header.batch_length, trusted.header.batch_length);
-        assert_eq!(validated.message_count(), trusted.message_count());
-        assert_eq!(validated.header.total_size(), trusted.header.total_size());
-        assert_eq!(validated.blob(), trusted.blob());
-    }
-
-    #[test]
-    fn decode_prepare_slice_trusted_skips_batch_checksum() {
-        // A stored batch_checksum mutated after stamping fails the validating
-        // decode but passes the trusted one: exactly why the trusted variant is
-        // confined to locally-produced bytes (see its doc invariant).
-        let mut owned = valid_prepare_bytes();
-        let corrupt_index = std::mem::size_of::<PrepareHeader>() + BATCH_CHECKSUM_OFFSET;
-        owned.as_mut_slice()[corrupt_index] ^= 0xFF;
-
-        assert!(
-            matches!(
-                decode_prepare_slice(owned.as_slice()),
-                Err(IggyError::InvalidBatchChecksum(..))
-            ),
-            "validating decode must reject a mutated batch checksum",
-        );
-        assert!(
-            decode_prepare_slice_trusted(owned.as_slice()).is_ok(),
-            "trusted decode skips the batch-checksum recomputation",
-        );
-    }
-
-    #[test]
-    fn decode_prepare_slice_size_below_header_size_does_not_panic() {
-        // Regression: without the `total_size < header_size` guard,
-        // `&bytes[256..size]` panics for any size < 256.
-        for adversarial_size in [0u32, 255] {
-            let owned = aligned_prepare_bytes(adversarial_size);
-            let result = decode_prepare_slice(owned.as_slice());
-            assert!(
-                matches!(result, Err(IggyError::InvalidCommand)),
-                "size={adversarial_size} must be rejected, got {result:?}",
-            );
-        }
-    }
-
-    #[cfg(debug_assertions)]
-    #[test]
-    #[should_panic(expected = "must be at least 16-byte aligned")]
-    fn decode_prepare_slice_debug_asserts_on_misaligned_input() {
-        // `Vec<u8>` requests align=1; glibc returns a base that is a
-        // multiple of 16, so `&buf[1..]` has offset 1 mod 16, eliably
-        // misaligned.
-        let buf: Vec<u8> = vec![0u8; std::mem::size_of::<PrepareHeader>() + 1];
-        let misaligned = &buf[1..];
-        assert_ne!(
-            misaligned.as_ptr().align_offset(16),
-            0,
-            "test setup: allocator returned non-16k base",
-        );
-        let _ = decode_prepare_slice(misaligned);
-    }
-
-    fn sample_messages() -> IggyMessages2 {
-        let mut messages = IggyMessages2::with_capacity(2);
-        messages.push(IggyMessage2 {
-            header: IggyMessage2Header {
-                id: 7,
-                origin_timestamp: 1_000,
-                ..Default::default()
-            },
-            payload: Bytes::from_static(b"first-payload"),
-            user_headers: None,
-        });
-        messages.push(IggyMessage2 {
-            header: IggyMessage2Header {
-                id: 8,
-                origin_timestamp: 1_050,
-                ..Default::default()
-            },
-            payload: Bytes::from_static(b"second-payload"),
-            user_headers: Some(Bytes::from_static(b"user-header-bytes")),
-        });
-        messages
-    }
-
-    /// `[PrepareHeader][256B batch header][blob]` carrying real per-message
-    /// records and checksums from the production encoder, with the initial zero
-    /// base offset and timestamp.
-    fn prepare_with_messages(messages: &IggyMessages2) -> Owned<MESSAGE_ALIGN> {
-        let namespace = IggyNamespace::new(1, 1, 7);
-        let owned =
-            SendMessages2Owned::from_messages(namespace, messages).expect("build send batch");
-        prepare_from_owned(&owned)
-    }
-
-    #[test]
-    fn checksum_oneshot_matches_streaming_reference() {
-        // Formula pin: the per-message checksum is XxHash3-64 (default seed)
-        // over `header[8..48] || payload || user_headers` as one byte stream.
-        // The encoders hash the concatenation in a single oneshot pass; this
-        // streaming reference feeds the same parts separately. Both must agree
-        // for every shape, or checksums at rest stop verifying.
-        fn streaming_reference(header_tail: &[u8], payload: &[u8], user_headers: &[u8]) -> u64 {
-            let mut hasher = XxHash3_64::new();
-            hasher.write(header_tail);
-            hasher.write(payload);
-            hasher.write(user_headers);
-            hasher.finish()
-        }
-
-        let header_tail: Vec<u8> = (0u8..40).collect();
-        let kilobyte: Vec<u8> = (0..1024u32).map(|index| (index % 251) as u8).collect();
-        let cases: &[(&[u8], &[u8])] = &[
-            (&[], &[]),
-            (b"payload-bytes", &[]),
-            (b"payload-bytes", b"user-header-bytes"),
-            (&kilobyte, &[]),
-            (&kilobyte, &kilobyte[..7]),
-            (&kilobyte[..1023], &kilobyte[..7]),
-        ];
-        for (payload, user_headers) in cases {
-            let mut concatenated =
-                Vec::with_capacity(header_tail.len() + payload.len() + user_headers.len());
-            concatenated.extend_from_slice(&header_tail);
-            concatenated.extend_from_slice(payload);
-            concatenated.extend_from_slice(user_headers);
-            assert_eq!(
-                XxHash3_64::oneshot(&concatenated),
-                streaming_reference(&header_tail, payload, user_headers),
-                "oneshot must match the streaming reference for payload {} B, user headers {} B",
-                payload.len(),
-                user_headers.len(),
-            );
-        }
-    }
-
-    #[test]
-    fn batch_checksum_v2_pins_header_fields_then_message_checksum_fields() {
-        // Formula pin for batch checksum v2: XxHash3-64 (default seed) streaming
-        // over the six batch header meta fields (LE, in field order) then each
-        // message's stored 8-byte checksum field in message order - never the
-        // bodies. This reference walks the blob by the KNOWN input message sizes,
-        // independent of the production frame decoder, and must equal what the
-        // encoder stamped, or a stamp will not verify against a read-back
-        // recompute.
-        let namespace = IggyNamespace::new(1, 1, 7);
-        let messages = sample_messages();
-        let mut owned =
-            SendMessages2Owned::from_messages(namespace, &messages).expect("build batch");
-        owned.header.base_offset = 100;
-        owned.header.base_timestamp = 200;
-        owned.header.batch_checksum = owned.header.checksum_for_blob(&owned.blob);
-
-        let mut hasher = XxHash3_64::new();
-        hasher.write(&owned.header.partition_id.to_le_bytes());
-        hasher.write(&owned.header.base_offset.to_le_bytes());
-        hasher.write(&owned.header.base_timestamp.to_le_bytes());
-        hasher.write(&owned.header.origin_timestamp.to_le_bytes());
-        hasher.write(&owned.header.batch_length.to_le_bytes());
-        hasher.write(&owned.header.message_count.to_le_bytes());
-        let mut frame_start = 0usize;
-        for message in messages.iter() {
-            hasher.write(&owned.blob[frame_start..frame_start + 8]);
-            let user_headers = message.user_headers.as_deref().unwrap_or_default();
-            frame_start += MESSAGE_HEADER_SIZE + message.payload.len() + user_headers.len();
-        }
-        let reference = hasher.finish();
-
-        assert_eq!(
-            frame_start,
-            owned.blob.len(),
-            "reference walk must consume the whole blob",
-        );
-        assert_eq!(
-            owned.header.batch_checksum, reference,
-            "v2 batch checksum must equal hash(6 header fields || per-message checksum fields)",
-        );
-    }
-
-    #[test]
-    fn decode_batch_slice_rejects_body_corruption_with_intact_checksum_field() {
-        // Equal-integrity: v2 binds bodies only through the per-message checksum
-        // fields, so a flipped body byte that leaves the 8-byte checksum field
-        // intact keeps the batch value matching. The validating decode must still
-        // reject it via the per-message verify - the sole at-rest read-back check
-        // (the poll disk walk) decodes through here.
-        let namespace = IggyNamespace::new(1, 1, 7);
-        let owned =
-            SendMessages2Owned::from_messages(namespace, &sample_messages()).expect("build batch");
-        let mut body = vec![0u8; COMMAND_HEADER_SIZE + owned.blob.len()];
-        owned.header.encode_into(&mut body[..COMMAND_HEADER_SIZE]);
-        body[COMMAND_HEADER_SIZE..].copy_from_slice(&owned.blob);
-
-        decode_batch_slice(&body).expect("the clean batch decodes");
-
-        // First payload byte sits right after the command header and the first
-        // message's 48B frame header, leaving that frame's checksum field intact.
-        let payload_index = COMMAND_HEADER_SIZE + MESSAGE_HEADER_SIZE;
-        body[payload_index] ^= 0xFF;
-        assert!(
-            matches!(
-                decode_batch_slice(&body),
-                Err(IggyError::InvalidMessageChecksum(..))
-            ),
-            "body corruption with an intact checksum field must fail the per-message verify",
-        );
-    }
-
-    #[test]
-    fn decode_prepare_slice_rejects_body_corruption_with_intact_checksum_field() {
-        // The same equal-integrity guarantee at the resident/repair validating
-        // decode, plus proof that the batch value alone is blind to it.
-        let mut owned = prepare_with_messages(&sample_messages());
-        decode_prepare_slice(owned.as_slice()).expect("the clean prepare decodes");
-
-        let payload_index = PREPARE_SPLIT_POINT + MESSAGE_HEADER_SIZE;
-        owned.as_mut_slice()[payload_index] ^= 0xFF;
-        assert!(
-            matches!(
-                decode_prepare_slice(owned.as_slice()),
-                Err(IggyError::InvalidMessageChecksum(..))
-            ),
-            "body corruption with an intact checksum field must fail the per-message verify",
-        );
-        assert!(
-            decode_prepare_slice_trusted(owned.as_slice()).is_ok(),
-            "the intact checksum field leaves the batch value matching, so trusted still passes",
-        );
-    }
-
-    /// Legacy `SendMessages` request body: `[metadata_len=4][message_count]`
-    /// then `count` skipped index slots, then the 64B-header legacy records.
-    fn legacy_send_messages_body(messages: &IggyMessages2) -> Vec<u8> {
-        let count = messages.count();
-        let mut body = Vec::new();
-        body.extend_from_slice(&4u32.to_le_bytes());
-        body.extend_from_slice(&count.to_le_bytes());
-        body.extend_from_slice(&vec![0u8; count as usize * INDEX_SIZE]);
-        for message in messages.iter() {
-            let user_headers = message.user_headers.as_deref().unwrap_or_default();
-            let mut header = [0u8; LEGACY_MESSAGE_HEADER_SIZE];
-            header[8..24].copy_from_slice(&message.header.id.to_le_bytes());
-            header[40..48].copy_from_slice(&message.header.origin_timestamp.to_le_bytes());
-            header[48..52].copy_from_slice(&(user_headers.len() as u32).to_le_bytes());
-            header[52..56].copy_from_slice(&(message.payload.len() as u32).to_le_bytes());
-            body.extend_from_slice(&header);
-            body.extend_from_slice(&message.payload);
-            body.extend_from_slice(user_headers);
-        }
-        body
-    }
-
-    fn legacy_request_message(body: &[u8]) -> Message<RoutedRequestHeader> {
-        let header_size = std::mem::size_of::<RoutedRequestHeader>();
-        let total = header_size + body.len();
-        let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
-        {
-            let header: &mut RoutedRequestHeader =
-                bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
-                    .expect("zeroed bytes form a valid RoutedRequestHeader");
-            header.command = Command2::Request;
-            header.operation = Operation::SendMessages;
-            header.client = 1;
-            header.session = 1;
-            header.request = 1;
-            header.size = u32::try_from(total).expect("size fits u32");
-        }
-        buffer.as_mut_slice()[header_size..].copy_from_slice(body);
-        Message::try_from(buffer).expect("legacy request message is valid")
-    }
-
-    #[test]
-    fn convert_request_message_rejects_empty_canonical_and_legacy_batches() {
-        let namespace = IggyNamespace::new(1, 1, 3);
-        let messages = IggyMessages2::with_capacity(0);
-        let canonical = SendMessages2Owned::from_messages(namespace, &messages)
-            .expect("build empty canonical batch");
-        let mut canonical_body = vec![0; canonical.header.total_size()];
-        canonical
-            .header
-            .encode_into(&mut canonical_body[..COMMAND_HEADER_SIZE]);
-        let legacy_body = legacy_send_messages_body(&messages);
-
-        for mode in [ChecksumMode::Compute, ChecksumMode::Skip] {
-            let canonical_result =
-                convert_request_message(namespace, legacy_request_message(&canonical_body), mode);
-            assert!(matches!(canonical_result, Err(IggyError::InvalidCommand)));
-
-            let legacy_result =
-                convert_request_message(namespace, legacy_request_message(&legacy_body), mode);
-            assert!(matches!(legacy_result, Err(IggyError::InvalidCommand)));
-        }
-    }
-
-    #[test]
-    fn convert_request_message_transcodes_legacy_to_canonical_bytes() {
-        // Golden: the fused legacy transcode must emit the exact canonical batch
-        // the native builder (`from_messages`) produces for the same messages -
-        // command header + blob, byte for byte. Explicit non-zero ids keep it
-        // deterministic (no `random_id` substitution).
-        let namespace = IggyNamespace::new(1, 1, 3);
-        let messages = sample_messages();
-
-        let owned =
-            SendMessages2Owned::from_messages(namespace, &messages).expect("build canonical batch");
-        let mut expected_body = vec![0u8; COMMAND_HEADER_SIZE + owned.blob.len()];
-        owned
-            .header
-            .encode_into(&mut expected_body[..COMMAND_HEADER_SIZE]);
-        expected_body[COMMAND_HEADER_SIZE..].copy_from_slice(&owned.blob);
-
-        let legacy = legacy_request_message(&legacy_send_messages_body(&messages));
-        let converted = convert_request_message(namespace, legacy, ChecksumMode::Compute)
-            .expect("legacy body transcodes");
-        let header_size = std::mem::size_of::<RoutedRequestHeader>();
-        let actual_body = &converted.as_slice()[header_size..converted.header().size as usize];
-
-        assert_eq!(
-            actual_body, expected_body,
-            "legacy transcode must be byte-identical to the canonical native batch",
-        );
-
-        // And the emitted batch is self-consistent: it validates through the
-        // batch-checksum decode and yields the original messages.
-        let decoded = decode_batch_slice(actual_body).expect("transcoded batch checksum is valid");
-        assert_eq!(decoded.message_count(), messages.count());
-        let payloads: Vec<&[u8]> = decoded.iter().map(|view| view.payload).collect();
-        assert_eq!(
-            payloads,
-            vec![&b"first-payload"[..], &b"second-payload"[..]]
-        );
-    }
-
-    #[test]
-    fn convert_request_message_skip_leaves_batch_checksum_zero_until_stamp() {
-        // The partition ingest path passes Skip: the transcoded batch must carry
-        // a zero checksum (stamp fills it) and be otherwise byte-identical to the
-        // Compute output - the flag toggles nothing but that one hash.
-        let namespace = IggyNamespace::new(1, 1, 3);
-        let messages = sample_messages();
-        let body = legacy_send_messages_body(&messages);
-        let header_size = std::mem::size_of::<RoutedRequestHeader>();
-
-        let computed = convert_request_message(
-            namespace,
-            legacy_request_message(&body),
-            ChecksumMode::Compute,
-        )
-        .expect("compute transcode");
-        let skipped =
-            convert_request_message(namespace, legacy_request_message(&body), ChecksumMode::Skip)
-                .expect("skip transcode");
-
-        let computed_body = &computed.as_slice()[header_size..computed.header().size as usize];
-        let skipped_body = &skipped.as_slice()[header_size..skipped.header().size as usize];
-
-        let skipped_header = SendMessages2Header::decode(&skipped_body[..COMMAND_HEADER_SIZE])
-            .expect("decode skipped header");
-        assert_eq!(
-            skipped_header.batch_checksum, 0,
-            "skip leaves the batch checksum zero until stamp",
-        );
-
-        // Patch only the 8-byte batch_checksum field into the skipped body; it
-        // must then equal the computed body, proving nothing else diverges.
-        let mut patched = skipped_body.to_vec();
-        patched[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]
-            .copy_from_slice(&computed_body[BATCH_CHECKSUM_OFFSET..BATCH_CHECKSUM_OFFSET + 8]);
-        assert_eq!(
-            patched.as_slice(),
-            computed_body,
-            "skip and compute differ only in the batch_checksum field",
-        );
-    }
-
-    #[test]
-    fn encrypt_ingest_path_stays_canonical_through_flag_split() {
-        // Mirror the plane encrypt ingest sequence: convert(Compute) -> the
-        // validating decode encrypt performs on its input -> encrypt -> the
-        // validating decode the second convert performs as its discriminator ->
-        // convert(Skip) (the partition convert), which sees an already-canonical
-        // batch and returns it unchanged. Every decode must succeed.
-        let namespace = IggyNamespace::new(1, 1, 3);
-        let messages = sample_messages();
-        let header_size = std::mem::size_of::<RoutedRequestHeader>();
-
-        let legacy = legacy_request_message(&legacy_send_messages_body(&messages));
-        let canonical = convert_request_message(namespace, legacy, ChecksumMode::Compute)
-            .expect("pre-encrypt transcode");
-        let canonical_body = &canonical.as_slice()[header_size..canonical.header().size as usize];
-        decode_batch_slice(canonical_body).expect("encrypt input decode validates the checksum");
-
-        let encryptor =
-            EncryptorKind::Aes256Gcm(Aes256GcmEncryptor::new(&[7u8; 32]).expect("valid 32B key"));
-        let encrypted = encrypt_batch_request(canonical, &encryptor).expect("encrypt batch");
-        let encrypted_body: Vec<u8> =
-            encrypted.as_slice()[header_size..encrypted.header().size as usize].to_vec();
-        decode_batch_slice(&encrypted_body)
-            .expect("encrypt output drives the 2nd-convert discriminator");
-
-        let repassed = convert_request_message(namespace, encrypted, ChecksumMode::Skip)
-            .expect("second convert passes the canonical batch");
-        let repassed_body = &repassed.as_slice()[header_size..repassed.header().size as usize];
-        assert_eq!(
-            repassed_body,
-            encrypted_body.as_slice(),
-            "an already-canonical encrypted batch passes the partition convert untouched",
-        );
-    }
-
-    /// Junk suffixes that must be refused at both ingest boundaries: one below a
-    /// frame header (the frame walk stops on a short read) and one frame-sized
-    /// but undecodable (`reserved != 0`). Neither is covered by any checksum, so
-    /// a walk that stops at the last decodable frame cannot see them.
-    const TRAILING_JUNK_CASES: [&[u8]; 2] = [&[0xAA], &[0xFF; 64]];
-
-    /// Canonical `SendMessages` request carrying `junk` past `batch_length`, with
-    /// `RoutedRequestHeader.size` inflated to cover it. `size` and `batch_length` are
-    /// independent wire fields, so a non-conforming client can emit this.
-    fn canonical_request_with_trailing_bytes(junk: &[u8]) -> Message<RoutedRequestHeader> {
-        let namespace = IggyNamespace::new(1, 1, 3);
-        let owned =
-            SendMessages2Owned::from_messages(namespace, &sample_messages()).expect("build batch");
-        let header_size = std::mem::size_of::<RoutedRequestHeader>();
-        let total = header_size + owned.header.total_size() + junk.len();
-        let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
-        {
-            let header: &mut RoutedRequestHeader =
-                bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
-                    .expect("zeroed bytes form a valid RoutedRequestHeader");
-            header.command = Command2::Request;
-            header.operation = Operation::SendMessages;
-            header.client = 1;
-            header.session = 1;
-            header.request = 1;
-            header.size = u32::try_from(total).expect("size fits u32");
-        }
-        let bytes = buffer.as_mut_slice();
-        owned
-            .header
-            .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
-        let blob_end = PREPARE_SPLIT_POINT + owned.blob.len();
-        bytes[PREPARE_SPLIT_POINT..blob_end].copy_from_slice(&owned.blob);
-        bytes[blob_end..].copy_from_slice(junk);
-        Message::try_from(buffer).expect("request message is valid")
-    }
-
-    /// A `Prepare` whose `size` covers `junk` past `batch_length`.
-    fn prepare_with_trailing_bytes(junk: &[u8]) -> Owned<MESSAGE_ALIGN> {
-        let namespace = IggyNamespace::new(1, 1, 7);
-        let owned =
-            SendMessages2Owned::from_messages(namespace, &sample_messages()).expect("build batch");
-        let header_size = std::mem::size_of::<PrepareHeader>();
-        let total = header_size + owned.header.total_size() + junk.len();
-        let mut buffer = Owned::<MESSAGE_ALIGN>::zeroed(total);
-        {
-            let prepare: &mut PrepareHeader =
-                bytemuck::checked::try_from_bytes_mut(&mut buffer.as_mut_slice()[..header_size])
-                    .expect("zeroed bytes form a valid PrepareHeader");
-            prepare.command = Command2::Prepare;
-            prepare.size = u32::try_from(total).expect("prepare size fits u32");
-        }
-        let bytes = buffer.as_mut_slice();
-        owned
-            .header
-            .encode_into(&mut bytes[header_size..header_size + COMMAND_HEADER_SIZE]);
-        let blob_end = PREPARE_SPLIT_POINT + owned.blob.len();
-        bytes[PREPARE_SPLIT_POINT..blob_end].copy_from_slice(&owned.blob);
-        bytes[blob_end..].copy_from_slice(junk);
-        buffer
-    }
-
-    #[test]
-    fn convert_request_message_rejects_canonical_batch_with_trailing_bytes() {
-        // Client ingest boundary. Accepting the request would carry the suffix
-        // into the journal and onto disk: the flush writes the whole frame while
-        // every reader advances by `batch_length`, so the segment walk lands
-        // inside the junk and every later batch becomes unreadable.
-        let namespace = IggyNamespace::new(1, 1, 3);
-        for junk in TRAILING_JUNK_CASES {
-            for mode in [ChecksumMode::Compute, ChecksumMode::Skip] {
-                let message = canonical_request_with_trailing_bytes(junk);
-                let result = convert_request_message(namespace, message, mode);
-                assert!(
-                    matches!(result, Err(IggyError::InvalidCommand)),
-                    "{} trailing bytes ({mode:?}) must be rejected, got {result:?}",
-                    junk.len(),
-                );
-            }
-        }
-    }
-
-    #[test]
-    fn convert_request_message_accepts_exact_canonical_batch() {
-        // The same builder with no suffix must still pass untouched, so the
-        // rejection above is the suffix and not the fixture.
-        let namespace = IggyNamespace::new(1, 1, 3);
-        let message = canonical_request_with_trailing_bytes(&[]);
-        let expected = message.as_slice().to_vec();
-        let converted = convert_request_message(namespace, message, ChecksumMode::Skip)
-            .expect("an exact canonical batch passes untouched");
-        assert_eq!(converted.as_slice(), expected.as_slice());
-    }
-
-    #[test]
-    fn decode_prepare_slice_rejects_trailing_bytes_past_batch_length() {
-        // Replica ingest must reject bytes beyond `batch_length` because no
-        // per-message checksum covers them.
-        for junk in TRAILING_JUNK_CASES {
-            let owned = prepare_with_trailing_bytes(junk);
-            assert!(
-                matches!(
-                    decode_prepare_slice(owned.as_slice()),
-                    Err(IggyError::InvalidCommand)
-                ),
-                "{} trailing bytes must fail the validating decode",
-                junk.len(),
-            );
-        }
-    }
-}
diff --git a/core/shard/src/lib.rs b/core/shard/src/lib.rs
index d11ef93..726bcb0 100644
--- a/core/shard/src/lib.rs
+++ b/core/shard/src/lib.rs
@@ -39,7 +39,7 @@
 use crossfire::TrySendError;
 use futures::FutureExt;
 use iggy_binary_protocol::{
-    CHECKSUM_UNSEALED, Command2, CommitHeader, ConsensusHeader, DoViewChangeHeader,
+    CHECKSUM_UNSEALED, Command, CommitHeader, ConsensusHeader, DoViewChangeHeader,
     ForwardLogoutHeader, ForwardLogoutResultHeader, ForwardRegisterHeader,
     ForwardRegisterResultHeader, GenericHeader, Operation, PrepareHeader, PrepareOkHeader,
     RepairPrepareHeader, RepairRangeReplyHeader, RequestPreparesHeader, RequestStartViewHeader,
@@ -2309,7 +2309,7 @@
 
     /// No client on this node: nothing to answer, nothing recovers it.
     fn is_replicated(&self) -> bool {
-        self.message.header().command != Command2::Request
+        self.message.header().command != Command::Request
     }
 }
 
@@ -3006,7 +3006,7 @@
     /// return is what makes callers bump
     /// `frame_drops_total{variant=partition,reason=park_dropped}`.
     fn deny_parked_client_request(&self, frame: ParkedFrame) -> bool {
-        if frame.message.header().command == Command2::Request
+        if frame.message.header().command == Command::Request
             && let Ok(request) = frame.message.try_into_typed::<RoutedRequestHeader>()
         {
             return self.stage_transient_deny(request.header());
@@ -3105,7 +3105,7 @@
             .streams()
             .created_revision_for_namespace(namespace);
         let frame_cost = parked_footprint(message.as_slice().len());
-        let replicated = message.header().command() != Command2::Request;
+        let replicated = message.header().command() != Command::Request;
         let mut pending = self.pending_partition_frames.borrow_mut();
         let parked_bytes = self.parked_partition_bytes.get();
         // Read the entry without `entry().or_default()`: inserting first would
@@ -4108,7 +4108,7 @@
                     cluster,
                     self_id,
                     target,
-                    Command2::RangeEvicted,
+                    Command::RangeEvicted,
                     header.nonce,
                     from_op,
                     header.group,
@@ -4118,7 +4118,7 @@
                     cluster,
                     self_id,
                     target,
-                    Command2::RepairDone,
+                    Command::RepairDone,
                     header.nonce,
                     header.from_op.saturating_sub(1),
                     header.group,
@@ -4131,7 +4131,7 @@
                     cluster,
                     self_id,
                     target,
-                    Command2::RangeEvicted,
+                    Command::RangeEvicted,
                     header.nonce,
                     from_op,
                     header.group,
@@ -4160,7 +4160,7 @@
                 cluster,
                 self_id,
                 target,
-                Command2::RepairDone,
+                Command::RepairDone,
                 header.nonce,
                 served_through,
                 header.group,
@@ -4239,7 +4239,7 @@
                 cluster,
                 self_id,
                 target,
-                Command2::RangeEvicted,
+                Command::RangeEvicted,
                 header.nonce,
                 retained_from,
                 header.group,
@@ -4262,7 +4262,7 @@
             cluster,
             self_id,
             target,
-            Command2::RepairDone,
+            Command::RepairDone,
             header.nonce,
             served_through,
             header.group,
@@ -4307,7 +4307,7 @@
         // run full prepare validation on it.
         let msg = msg.transmute_header(|old: RepairPrepareHeader, new: &mut PrepareHeader| {
             *new = old.0;
-            new.command = Command2::Prepare;
+            new.command = Command::Prepare;
         });
         let header = *msg.header();
         let planes = self.plane.inner();
@@ -4481,7 +4481,7 @@
                 return;
             }
             match header.command {
-                Command2::RepairDone => {
+                Command::RepairDone => {
                     let before = consensus.commit_min();
                     planes.0.commit_journal().await;
                     // Completion is decided by the LOCAL walk, not the
@@ -4516,7 +4516,7 @@
                         .await;
                     }
                 }
-                Command2::RangeEvicted => {
+                Command::RangeEvicted => {
                     // Journal repair cannot close this gap: the serving peer
                     // compacted past it, so the ops this replica is missing no
                     // longer exist as WAL entries anywhere. This is the one
@@ -4617,12 +4617,12 @@
             return;
         }
         match header.command {
-            Command2::RangeEvicted => {
+            Command::RangeEvicted => {
                 if let Some(repair) = partition.repair.as_mut() {
                     repair.floor = Some(header.op.saturating_sub(1));
                 }
             }
-            Command2::RepairDone => {
+            Command::RepairDone => {
                 // `complete_repair` walks the window and clears the session
                 // only when the LOCAL commit frontier reached the requested
                 // op (the peer's served-through claim proves nothing about
@@ -4733,7 +4733,7 @@
     {
         let msg = Message::<RequestPreparesHeader>::new(size_of::<RequestPreparesHeader>())
             .transmute_header(|_, h: &mut RequestPreparesHeader| {
-                h.command = Command2::RequestPrepares;
+                h.command = Command::RequestPrepares;
                 h.cluster = cluster;
                 h.replica = self_id;
                 h.nonce = nonce;
@@ -4779,7 +4779,7 @@
         const COMMAND_OFFSET: usize = std::mem::offset_of!(GenericHeader, command);
         let mut owned =
             server_common::iobuf::Owned::<MESSAGE_ALIGN>::copy_from_slice(entry.as_slice());
-        owned.as_mut_slice()[COMMAND_OFFSET] = Command2::RepairPrepare as u8;
+        owned.as_mut_slice()[COMMAND_OFFSET] = Command::RepairPrepare as u8;
         let Ok(message) = Message::<GenericHeader>::try_from(owned) else {
             tracing::warn!(
                 shard = self.id,
@@ -4800,7 +4800,7 @@
         cluster: u128,
         self_id: u8,
         target: u8,
-        command: Command2,
+        command: Command,
         nonce: u128,
         op: u64,
         namespace: u64,
@@ -5297,7 +5297,7 @@
         let msg =
             Message::<RequestStateTransferHeader>::new(size_of::<RequestStateTransferHeader>())
                 .transmute_header(|_, h: &mut RequestStateTransferHeader| {
-                    h.command = Command2::RequestStateTransfer;
+                    h.command = Command::RequestStateTransfer;
                     h.cluster = consensus.cluster();
                     h.replica = consensus.replica();
                     h.nonce = nonce;
@@ -5341,7 +5341,7 @@
             msg.as_mut_slice()[size_of::<StateTransferTargetHeader>()..].copy_from_slice(manifest);
         }
         let msg = msg.transmute_header(|_, h: &mut StateTransferTargetHeader| {
-            h.command = Command2::StateTransferTarget;
+            h.command = Command::StateTransferTarget;
             h.cluster = cluster;
             h.replica = self_id;
             h.nonce = nonce;
@@ -5385,7 +5385,7 @@
     {
         let msg = Message::<RequestStateChunkHeader>::new(size_of::<RequestStateChunkHeader>())
             .transmute_header(|_, h: &mut RequestStateChunkHeader| {
-                h.command = Command2::RequestStateChunk;
+                h.command = Command::RequestStateChunk;
                 h.cluster = cluster;
                 h.replica = self_id;
                 h.nonce = nonce;
@@ -5933,7 +5933,7 @@
                     chunk.as_mut_slice()[size_of::<StateChunkHeader>()..].copy_from_slice(payload);
                     Some(ChunkReply::Chunk(chunk.transmute_header(
                         |_, h: &mut StateChunkHeader| {
-                            h.command = Command2::StateChunk;
+                            h.command = Command::StateChunk;
                             h.cluster = cluster;
                             h.replica = self_id;
                             h.nonce = header.nonce;
@@ -6926,7 +6926,7 @@
                 chunk.as_mut_slice()[size_of::<StateChunkHeader>()..].copy_from_slice(payload);
                 ChunkAttempt::Reply(Some(ChunkReply::Chunk(chunk.transmute_header(
                     |_, h: &mut StateChunkHeader| {
-                        h.command = Command2::StateChunk;
+                        h.command = Command::StateChunk;
                         h.cluster = cluster;
                         h.replica = self_id;
                         h.nonce = header.nonce;
@@ -8988,7 +8988,7 @@
             VsrAction::SendStartViewChange { view, group } => {
                 let msg = Message::<StartViewChangeHeader>::new(size_of::<StartViewChangeHeader>())
                     .transmute_header(|_, h: &mut StartViewChangeHeader| {
-                        h.command = Command2::StartViewChange;
+                        h.command = Command::StartViewChange;
                         h.cluster = cluster;
                         h.replica = self_id;
                         h.view = *view;
@@ -9017,7 +9017,7 @@
                 let nack_bitset = suffix.nack_bitset();
                 let present_bitset = suffix.present_bitset();
                 let msg = msg.transmute_header(|_, h: &mut DoViewChangeHeader| {
-                    h.command = Command2::DoViewChange;
+                    h.command = Command::DoViewChange;
                     h.cluster = cluster;
                     h.replica = self_id;
                     h.view = *view;
@@ -9045,7 +9045,7 @@
                 let msg =
                     Message::<RequestStartViewHeader>::new(size_of::<RequestStartViewHeader>())
                         .transmute_header(|_, h: &mut RequestStartViewHeader| {
-                            h.command = Command2::RequestStartView;
+                            h.command = Command::RequestStartView;
                             h.cluster = cluster;
                             h.replica = self_id;
                             h.view = *view;
@@ -9073,7 +9073,7 @@
                 let body_checksum = control_body_checksum(&msg.as_slice()[header_size..total_size]);
                 let msg = msg.transmute_header(|_, h: &mut StartViewHeader| {
                     h.checksum_body = body_checksum;
-                    h.command = Command2::StartView;
+                    h.command = Command::StartView;
                     h.cluster = cluster;
                     h.replica = self_id;
                     h.view = *view;
@@ -9111,7 +9111,7 @@
                     let prepare_header = *prepare_header;
                     let msg = Message::<PrepareOkHeader>::new(size_of::<PrepareOkHeader>())
                         .transmute_header(|_, h: &mut PrepareOkHeader| {
-                            h.command = Command2::PrepareOk;
+                            h.command = Command::PrepareOk;
                             h.cluster = cluster;
                             h.replica = self_id;
                             h.view = *view;
@@ -9171,7 +9171,7 @@
             } => {
                 let msg = Message::<CommitHeader>::new(size_of::<CommitHeader>()).transmute_header(
                     |_, h: &mut CommitHeader| {
-                        h.command = Command2::Commit;
+                        h.command = Command::Commit;
                         h.cluster = cluster;
                         h.replica = self_id;
                         h.view = *view;
@@ -9245,7 +9245,7 @@
                     };
                     let msg = Message::<PrepareOkHeader>::new(size_of::<PrepareOkHeader>())
                         .transmute_header(|_, h: &mut PrepareOkHeader| {
-                            h.command = Command2::PrepareOk;
+                            h.command = Command::PrepareOk;
                             h.cluster = cluster;
                             h.replica = self_id;
                             h.view = *view;
@@ -9362,11 +9362,11 @@
     //! Who parked the log decides what it means.
 
     use super::{MergedLog, repair_op_in_scope, repair_serve_ceiling};
-    use iggy_binary_protocol::{Command2, PrepareHeader};
+    use iggy_binary_protocol::{Command, PrepareHeader};
 
     fn header(op: u64) -> PrepareHeader {
         PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             op,
             ..Default::default()
         }
@@ -9440,11 +9440,11 @@
     //! Holding an op is not holding the view's op.
 
     use super::{MergedLog, first_op_not_covered};
-    use iggy_binary_protocol::{Command2, Operation, PrepareHeader};
+    use iggy_binary_protocol::{Command, Operation, PrepareHeader};
 
     fn sealed(op: u64, request: u64) -> PrepareHeader {
         let mut header = PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             operation: Operation::CreateStream,
             op,
             request,
@@ -9511,13 +9511,13 @@
     //! that premise, so it cannot be quietly reintroduced.
 
     use super::{DVC_HEADERS_MAX, build_dvc_suffix};
-    use iggy_binary_protocol::{Command2, Operation, PrepareHeader};
+    use iggy_binary_protocol::{Command, Operation, PrepareHeader};
 
     /// A real prepare at `op`. The operation must not be `Reserved`: that is
     /// exactly `dvc_blank`, and `dvc_header_kind` classifies by equality with it.
     fn held(op: u64) -> PrepareHeader {
         PrepareHeader {
-            command: Command2::Prepare,
+            command: Command::Prepare,
             operation: Operation::CreateStream,
             op,
             ..Default::default()
@@ -9695,7 +9695,7 @@
     //! off. Keying on `checksum_body` looking sealed is bypassable by zeroing it.
 
     use super::{control_body_checksum, control_suffix_body_verified};
-    use iggy_binary_protocol::{Command2, DoViewChangeHeader, PrepareHeader};
+    use iggy_binary_protocol::{Command, DoViewChangeHeader, PrepareHeader};
     use server_common::Message;
     use std::mem::size_of;
 
@@ -9711,7 +9711,7 @@
             *byte = u8::try_from(index % 251).expect("modulus fits u8");
         }
         msg.transmute_header(|_, header: &mut DoViewChangeHeader| {
-            header.command = Command2::DoViewChange;
+            header.command = Command::DoViewChange;
             header.checksum_body = checksum_body;
             header.size = u32::try_from(total).expect("frame fits u32");
         })
diff --git a/core/simulator/src/client.rs b/core/simulator/src/client.rs
index db73cfb..3e7742e 100644
--- a/core/simulator/src/client.rs
+++ b/core/simulator/src/client.rs
@@ -22,8 +22,7 @@
     CreateConsumerGroupRequest, DeleteConsumerGroupRequest,
 };
 use iggy_binary_protocol::requests::consumer_offsets::{
-    DeleteConsumerOffset2Request, DeleteConsumerOffsetRequest, StoreConsumerOffset2Request,
-    StoreConsumerOffsetRequest,
+    DeleteConsumerOffsetRequest, StoreConsumerOffsetRequest,
 };
 use iggy_binary_protocol::requests::messages::{
     PollMessagesRequest, RawMessage, SendMessagesEncoder,
@@ -71,12 +70,11 @@
     /// [`SimClient::request_id_for`].
     partition_counter: Cell<u64>,
     /// Deterministic per-message id source for produced messages. The real SDK
-    /// sends `id: 0` and lets the server mint a random UUID
-    /// (`transcode_legacy_request` -> `random_id::get_uuid`); that
-    /// mint is unseeded, so under the deterministic executor a produce's
-    /// replicated body bytes (and their checksums) would differ run to run,
-    /// silently breaking seeded replay. Stamping a deterministic id here keeps
-    /// the body a pure function of the seed. See [`SimClient::next_message_id`].
+    /// mints a random UUID for a zero message id before encoding; that mint is
+    /// unseeded, so under the deterministic executor a produce's replicated
+    /// body bytes (and their checksums) would differ run to run, silently
+    /// breaking seeded replay. Stamping a deterministic id here keeps the body
+    /// a pure function of the seed. See [`SimClient::next_message_id`].
     message_counter: Cell<u64>,
     session: Cell<u64>,
 }
@@ -165,7 +163,7 @@
     pub fn register(&self) -> Message<RoutedRequestHeader> {
         let header_size = std::mem::size_of::<RoutedRequestHeader>();
         let header = RoutedRequestHeader {
-            command: iggy_binary_protocol::Command2::Request,
+            command: iggy_binary_protocol::Command::Request,
             operation: Operation::Register,
             size: header_size as u32,
             client: self.client_id,
@@ -214,7 +212,7 @@
         let header_size = std::mem::size_of::<RoutedRequestHeader>();
         let total_size = header_size + body.len();
         let header = RoutedRequestHeader {
-            command: iggy_binary_protocol::Command2::Request,
+            command: iggy_binary_protocol::Command::Request,
             operation: Operation::Register,
             size: total_size as u32,
             client: self.client_id,
@@ -513,12 +511,12 @@
         self.build_request(Operation::DeletePersonalAccessToken, &wire.to_bytes())
     }
 
-    /// Build a `SendMessages` request in the legacy `SendMessagesEncoder` wire
-    /// shape, byte-compatible with what the real SDK sends (`common` binary
-    /// client). VSR clients resolve to an explicit partition before sending, so
-    /// the sim always emits `WirePartitioning::PartitionId`: that is the shape
-    /// the shell's `resolve_partition_request_namespace` decodes, and the raw
-    /// path converts it to `SendMessages2` via `transcode_legacy_request`.
+    /// Build a `SendMessages` request in the `SendMessagesEncoder` wire shape,
+    /// byte-compatible with what the real SDK sends (`common` binary client).
+    /// VSR clients resolve to an explicit partition before sending, so the sim
+    /// always emits `WirePartitioning::PartitionId`: that is the shape the
+    /// shell's `resolve_partition_request_namespace` decodes before admission
+    /// strips the metadata and stamps the batch.
     ///
     /// # Panics
     /// Panics if a group id exceeds `u32` or the request buffer is invalid.
@@ -549,7 +547,8 @@
 
         let size = SendMessagesEncoder::encoded_size(&stream_id, &topic_id, &partitioning, &raw);
         let mut buf = BytesMut::with_capacity(size);
-        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &raw);
+        SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id, &partitioning, &raw)
+            .expect("simulator send batch encodes");
 
         self.build_request_with_namespace(Operation::SendMessages, &buf, group)
     }
@@ -584,7 +583,7 @@
         let mut reserved = [0u8; 52];
         reserved[..4].copy_from_slice(&POLL_MESSAGES_CODE.to_le_bytes());
         let header = RoutedRequestHeader {
-            command: iggy_binary_protocol::Command2::Request,
+            command: iggy_binary_protocol::Command::Request,
             operation: Operation::NonReplicated,
             size: total_size as u32,
             client: self.client_id,
@@ -602,55 +601,13 @@
             .expect("poll request must be valid")
     }
 
-    pub fn store_consumer_offset(
-        &self,
-        group: IggyNamespace,
-        consumer_kind: u8,
-        consumer_id: u32,
-        offset: u64,
-    ) -> Message<RoutedRequestHeader> {
-        let (stream_id, topic_id, partition_id) = namespace_ids(group);
-        let request = StoreConsumerOffsetRequest {
-            consumer: namespace_consumer(consumer_kind, consumer_id),
-            stream_id,
-            topic_id,
-            partition_id,
-            offset,
-        };
-        self.build_request_with_namespace(
-            Operation::StoreConsumerOffset,
-            &request.to_bytes(),
-            group,
-        )
-    }
-
-    pub fn delete_consumer_offset(
-        &self,
-        group: IggyNamespace,
-        consumer_kind: u8,
-        consumer_id: u32,
-    ) -> Message<RoutedRequestHeader> {
-        let (stream_id, topic_id, partition_id) = namespace_ids(group);
-        let request = DeleteConsumerOffsetRequest {
-            consumer: namespace_consumer(consumer_kind, consumer_id),
-            stream_id,
-            topic_id,
-            partition_id,
-        };
-        self.build_request_with_namespace(
-            Operation::DeleteConsumerOffset,
-            &request.to_bytes(),
-            group,
-        )
-    }
-
     /// Store offset with explicit `AckLevel`. `NoAck` takes the primary's
     /// fast path (no replication); `Quorum` goes through VSR.
     ///
     /// # Panics
     /// Panics on payload too large for `Owned::<4096>` or invalid
     /// `Message<RoutedRequestHeader>` parse; both are simulator misconfig.
-    pub fn store_consumer_offset_2(
+    pub fn store_consumer_offset(
         &self,
         group: IggyNamespace,
         consumer_kind: u8,
@@ -659,7 +616,7 @@
         ack: AckLevel,
     ) -> Message<RoutedRequestHeader> {
         let (stream_id, topic_id, partition_id) = namespace_ids(group);
-        let request = StoreConsumerOffset2Request {
+        let request = StoreConsumerOffsetRequest {
             consumer: namespace_consumer(consumer_kind, consumer_id),
             stream_id,
             topic_id,
@@ -668,7 +625,7 @@
             ack,
         };
         self.build_request_with_namespace(
-            Operation::StoreConsumerOffset2,
+            Operation::StoreConsumerOffset,
             &request.to_bytes(),
             group,
         )
@@ -679,7 +636,7 @@
     /// # Panics
     /// Panics on payload too large for `Owned::<4096>` or invalid
     /// `Message<RoutedRequestHeader>` parse; both are simulator misconfig.
-    pub fn delete_consumer_offset_2(
+    pub fn delete_consumer_offset(
         &self,
         group: IggyNamespace,
         consumer_kind: u8,
@@ -687,7 +644,7 @@
         ack: AckLevel,
     ) -> Message<RoutedRequestHeader> {
         let (stream_id, topic_id, partition_id) = namespace_ids(group);
-        let request = DeleteConsumerOffset2Request {
+        let request = DeleteConsumerOffsetRequest {
             consumer: namespace_consumer(consumer_kind, consumer_id),
             stream_id,
             topic_id,
@@ -695,7 +652,7 @@
             ack,
         };
         self.build_request_with_namespace(
-            Operation::DeleteConsumerOffset2,
+            Operation::DeleteConsumerOffset,
             &request.to_bytes(),
             group,
         )
@@ -742,7 +699,7 @@
     #[allow(clippy::cast_possible_truncation)]
     fn header(&self, operation: Operation, group: u64, total_size: usize) -> RoutedRequestHeader {
         RoutedRequestHeader {
-            command: iggy_binary_protocol::Command2::Request,
+            command: iggy_binary_protocol::Command::Request,
             operation,
             size: total_size as u32,
             cluster: 0, // TODO: Get from config
diff --git a/core/simulator/src/lib.rs b/core/simulator/src/lib.rs
index 70f24b7..fa5b3e3 100644
--- a/core/simulator/src/lib.rs
+++ b/core/simulator/src/lib.rs
@@ -1674,14 +1674,16 @@
         // correctly because `hash % 1 == 0` at one shard per replica.
         // Re-locked again when replies stopped echoing a group id (the
         // client wire lost its namespace field): the reply-hash tuple
-        // dropped that component.
+        // dropped that component. Re-locked when the v1 consumer-offset ops
+        // were removed and the v2 pair became the only store/delete actions:
+        // `Action` lost two variants, shifting discriminants and draw order.
         assert_eq!(
-            h1, 0xCF1F_BC79_B44A_65F7,
+            h1, 0x14BF_3C3F_41D4_F69D,
             "workload reply hash drifted from locked baseline"
         );
     }
 
-    /// Drive workload with uniform weights across all 25 `Action` variants.
+    /// Drive workload with near-uniform weights across all 23 `Action` variants.
     /// Assert it runs without panic and observes at least one reply.
     /// Per-op coverage not asserted: some ops can starve the in-flight slot
     /// at single-client / 1-slot pipeline limits.
@@ -1720,9 +1722,12 @@
         sim.init_partition(ns_b);
         sim.register_client_with_primary(&client);
 
-        // 25 variants × 4 = 100.
-        assert_eq!(Action::COUNT, 25, "Action::COUNT changed; adjust weights");
-        let entries: Vec<(Action, u8)> = Action::iter().map(|a| (a, 4)).collect();
+        // 23 variants: 8 x 5 + 15 x 4 = 100 (weights must sum to 100).
+        assert_eq!(Action::COUNT, 23, "Action::COUNT changed; adjust weights");
+        let entries: Vec<(Action, u8)> = Action::iter()
+            .enumerate()
+            .map(|(index, action)| (action, if index < 8 { 5 } else { 4 }))
+            .collect();
         let weights = ActionWeights::new(&entries);
 
         let mut options = WorkloadOptions::new(0xC0FF_EE00, replica_count, vec![ns_a, ns_b]);
@@ -1787,8 +1792,11 @@
         sim.init_partition(ns_b);
         sim.register_client_with_primary(&client);
 
-        assert_eq!(Action::COUNT, 25, "Action::COUNT changed; adjust weights");
-        let entries: Vec<(Action, u8)> = Action::iter().map(|a| (a, 4)).collect();
+        assert_eq!(Action::COUNT, 23, "Action::COUNT changed; adjust weights");
+        let entries: Vec<(Action, u8)> = Action::iter()
+            .enumerate()
+            .map(|(index, action)| (action, if index < 8 { 5 } else { 4 }))
+            .collect();
         let mut options = WorkloadOptions::new(0xC0FF_EE00, replica_count, vec![ns_a, ns_b]);
         options.weights = ActionWeights::new(&entries);
         let mut wl = Workload::new(options);
@@ -2148,7 +2156,7 @@
         options.weights = ActionWeights::new(&[
             (Action::CreateStream, 5),
             (Action::SendMessages, 70),
-            (Action::StoreConsumerOffset2, 25),
+            (Action::StoreConsumerOffset, 25),
         ]);
         let mut wl = Workload::new(options);
 
@@ -2305,7 +2313,7 @@
         options.weights = ActionWeights::new(&[
             (Action::CreateStream, 5),
             (Action::SendMessages, 70),
-            (Action::StoreConsumerOffset2, 25),
+            (Action::StoreConsumerOffset, 25),
         ]);
 
         let mut wl = Workload::new(options);
@@ -3042,7 +3050,7 @@
     #[test]
     fn per_partition_consensus_independence() {
         use consensus::PIPELINE_PREPARE_QUEUE_MAX;
-        use iggy_binary_protocol::{Command2, PrepareOkHeader};
+        use iggy_binary_protocol::{Command, PrepareOkHeader};
         use packet::Packet;
         use std::sync::atomic::{AtomicU64, Ordering};
 
@@ -3052,7 +3060,7 @@
         static BLOCKED_NS: AtomicU64 = AtomicU64::new(0);
 
         fn drop_blocked_prepare_ok(packet: &Packet) -> bool {
-            if packet.message.header().command != Command2::PrepareOk {
+            if packet.message.header().command != Command::PrepareOk {
                 return false;
             }
             let header: &PrepareOkHeader = bytemuck::checked::from_bytes(
diff --git a/core/simulator/src/packet.rs b/core/simulator/src/packet.rs
index 093615a..921b035 100644
--- a/core/simulator/src/packet.rs
+++ b/core/simulator/src/packet.rs
@@ -23,7 +23,7 @@
 //! - Automatic network partitioning with configurable lifecycle
 //! - Automatic path clogging with exponential duration
 //! - Link capacity limits with random eviction
-//! - Per-command link filtering via `LinkFilter` (`EnumSet<Command2>`)
+//! - Per-command link filtering via `LinkFilter` (`EnumSet<Command>`)
 //!
 //! Partitions are implemented via per-link `LinkFilter`s. When a link's
 //! filter is empty, all packets are silently dropped. When specific commands
@@ -40,18 +40,18 @@
 
 use crate::ready_queue::{Ready, ReadyQueue};
 use enumset::EnumSet;
-use iggy_binary_protocol::{Command2, GenericHeader};
+use iggy_binary_protocol::{Command, GenericHeader};
 use rand::RngExt;
 use rand_xoshiro::Xoshiro256Plus;
 use rand_xoshiro::rand_core::SeedableRng;
 use server_common::Message;
 use std::collections::HashMap;
 
-/// Per-link command filter. An `EnumSet<Command2>` where:
+/// Per-link command filter. An `EnumSet<Command>` where:
 /// - [`ALLOW_ALL`] = all commands pass (link fully enabled)
 /// - [`BLOCK_ALL`] = all commands blocked (link fully disabled/partitioned)
 /// - Custom sets = only matching commands pass through
-pub type LinkFilter = EnumSet<Command2>;
+pub type LinkFilter = EnumSet<Command>;
 
 /// Link filter that allows all commands through (link fully enabled).
 pub const ALLOW_ALL: LinkFilter = EnumSet::all();
@@ -432,7 +432,7 @@
     }
 
     /// Returns a mutable reference to the link's filter.
-    /// This is the per-link command filter — `EnumSet<Command2>`.
+    /// This is the per-link command filter — `EnumSet<Command>`.
     /// Set to [`BLOCK_ALL`] to block all packets (partition).
     /// Set to [`ALLOW_ALL`] to allow all packets (default).
     /// Remove specific commands to selectively filter.
@@ -787,7 +787,7 @@
     }
 
     #[allow(clippy::cast_possible_truncation)]
-    fn create_test_message_with_command(command: Command2) -> Message<GenericHeader> {
+    fn create_test_message_with_command(command: Command) -> Message<GenericHeader> {
         let size = std::mem::size_of::<GenericHeader>();
         let mut buf = vec![0u8; size];
         let header: &mut GenericHeader =
@@ -1030,11 +1030,11 @@
 
         // Set filter to only allow Ping on link 0->1
         let filter = sim.link_filter(ProcessId::Replica(0), ProcessId::Replica(1));
-        *filter = EnumSet::only(Command2::Ping);
+        *filter = EnumSet::only(Command::Ping);
 
         // Submit a Ping message and a Prepare message
-        let ping_msg = create_test_message_with_command(Command2::Ping);
-        let prepare_msg = create_test_message_with_command(Command2::Prepare);
+        let ping_msg = create_test_message_with_command(Command::Ping);
+        let prepare_msg = create_test_message_with_command(Command::Prepare);
 
         sim.submit(ProcessId::Replica(0), ProcessId::Replica(1), ping_msg);
         sim.submit(ProcessId::Replica(0), ProcessId::Replica(1), prepare_msg);
@@ -1048,7 +1048,7 @@
 
         // Only the Ping should be delivered
         assert_eq!(delivered.len(), 1);
-        assert_eq!(delivered[0].message.header().command, Command2::Ping);
+        assert_eq!(delivered[0].message.header().command, Command::Ping);
 
         // Nothing left in flight
         assert_eq!(sim.packets_in_flight(), 0);
diff --git a/core/simulator/src/workload/actions.rs b/core/simulator/src/workload/actions.rs
index e0592da..f830bbe 100644
--- a/core/simulator/src/workload/actions.rs
+++ b/core/simulator/src/workload/actions.rs
@@ -27,7 +27,7 @@
     // DO NOT REORDER (hash baseline depends on these indices).
     CreateStream,
     SendMessages,
-    StoreConsumerOffset2,
+    StoreConsumerOffset,
     DeleteStream,
     UpdateStream,
     PurgeStream,
@@ -47,9 +47,7 @@
     UpdatePermissions,
     CreatePersonalAccessToken,
     DeletePersonalAccessToken,
-    StoreConsumerOffset,
     DeleteConsumerOffset,
-    DeleteConsumerOffset2,
 }
 
 // Lock the discriminants of the first three variants. Inserting a new variant before
@@ -58,5 +56,5 @@
 const _: () = {
     assert!(Action::CreateStream as u8 == 0);
     assert!(Action::SendMessages as u8 == 1);
-    assert!(Action::StoreConsumerOffset2 as u8 == 2);
+    assert!(Action::StoreConsumerOffset as u8 == 2);
 };
diff --git a/core/simulator/src/workload/ops/delete_consumer_offset.rs b/core/simulator/src/workload/ops/delete_consumer_offset.rs
index c05c796..45e61ae 100644
--- a/core/simulator/src/workload/ops/delete_consumer_offset.rs
+++ b/core/simulator/src/workload/ops/delete_consumer_offset.rs
@@ -15,9 +15,9 @@
 // specific language governing permissions and limitations
 // under the License.
 
-//! `DeleteConsumerOffset` op. Live namespace via shadow.
+//! `DeleteConsumerOffset` op. Namespace-routed with `AckLevel`.
 
-use iggy_binary_protocol::RoutedRequestHeader;
+use iggy_binary_protocol::{AckLevel, RoutedRequestHeader};
 use rand::RngExt;
 use rand_xoshiro::Xoshiro256Plus;
 use server_common::Message;
@@ -33,6 +33,7 @@
     pub ns: IggyNamespace,
     pub consumer_kind: u8,
     pub consumer_id: u32,
+    pub ack: AckLevel,
 }
 
 #[derive(Copy, Clone, Eq, PartialEq, Debug)]
@@ -53,10 +54,17 @@
             let ns = shadow.pick_namespace(prng)?;
             let consumer_kind: u8 = u8::from(prng.random::<bool>());
             let consumer_id: u32 = prng.random_range(0..options.consumer_pool_size.max(1));
+            let f: f32 = prng.random();
+            let ack = if f < options.ack_quorum_ratio {
+                AckLevel::Quorum
+            } else {
+                AckLevel::NoAck
+            };
             Some(Input {
                 ns,
                 consumer_kind,
                 consumer_id,
+                ack,
             })
         }
     }
@@ -64,7 +72,7 @@
 
 #[must_use]
 pub fn build_message(client: &SimClient, input: &Input) -> Message<RoutedRequestHeader> {
-    client.delete_consumer_offset(input.ns, input.consumer_kind, input.consumer_id)
+    client.delete_consumer_offset(input.ns, input.consumer_kind, input.consumer_id, input.ack)
 }
 
 #[must_use]
diff --git a/core/simulator/src/workload/ops/delete_consumer_offset_2.rs b/core/simulator/src/workload/ops/delete_consumer_offset_2.rs
deleted file mode 100644
index ad2a620..0000000
--- a/core/simulator/src/workload/ops/delete_consumer_offset_2.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-//! `DeleteConsumerOffset2` op. Namespace-routed with `AckLevel`.
-
-use iggy_binary_protocol::{AckLevel, RoutedRequestHeader};
-use rand::RngExt;
-use rand_xoshiro::Xoshiro256Plus;
-use server_common::Message;
-use server_common::sharding::IggyNamespace;
-
-use crate::client::SimClient;
-use crate::workload::effect::Effect;
-use crate::workload::options::WorkloadOptions;
-use crate::workload::shadow::Shadow;
-
-#[derive(Debug, Clone)]
-pub struct Input {
-    pub ns: IggyNamespace,
-    pub consumer_kind: u8,
-    pub consumer_id: u32,
-    pub ack: AckLevel,
-}
-
-#[derive(Copy, Clone, Eq, PartialEq, Debug)]
-pub enum Outcome {
-    Success,
-}
-
-pub const OUTCOMES: &[Outcome] = &[Outcome::Success];
-
-pub fn sample(
-    shadow: &mut Shadow,
-    outcome: Outcome,
-    prng: &mut Xoshiro256Plus,
-    options: &WorkloadOptions,
-) -> Option<Input> {
-    match outcome {
-        Outcome::Success => {
-            let ns = shadow.pick_namespace(prng)?;
-            let consumer_kind: u8 = u8::from(prng.random::<bool>());
-            let consumer_id: u32 = prng.random_range(0..options.consumer_pool_size.max(1));
-            let f: f32 = prng.random();
-            let ack = if f < options.ack_quorum_ratio {
-                AckLevel::Quorum
-            } else {
-                AckLevel::NoAck
-            };
-            Some(Input {
-                ns,
-                consumer_kind,
-                consumer_id,
-                ack,
-            })
-        }
-    }
-}
-
-#[must_use]
-pub fn build_message(client: &SimClient, input: &Input) -> Message<RoutedRequestHeader> {
-    client.delete_consumer_offset_2(input.ns, input.consumer_kind, input.consumer_id, input.ack)
-}
-
-#[must_use]
-pub const fn classify_reply(_code: u32) -> Outcome {
-    Outcome::Success
-}
-
-#[must_use]
-pub const fn predicted_effect(input: &Input, outcome: Outcome) -> Effect {
-    match outcome {
-        Outcome::Success => Effect::OffsetDeleted {
-            key: (input.ns, input.consumer_kind, input.consumer_id),
-        },
-    }
-}
diff --git a/core/simulator/src/workload/ops/mod.rs b/core/simulator/src/workload/ops/mod.rs
index df6ffed..604e110 100644
--- a/core/simulator/src/workload/ops/mod.rs
+++ b/core/simulator/src/workload/ops/mod.rs
@@ -38,7 +38,6 @@
 pub mod create_user;
 pub mod delete_consumer_group;
 pub mod delete_consumer_offset;
-pub mod delete_consumer_offset_2;
 pub mod delete_partitions;
 pub mod delete_personal_access_token;
 pub mod delete_segments;
@@ -49,7 +48,6 @@
 pub mod purge_topic;
 pub mod send_messages;
 pub mod store_consumer_offset;
-pub mod store_consumer_offset_2;
 pub mod update_permissions;
 pub mod update_stream;
 pub mod update_topic;
@@ -172,7 +170,7 @@
     // First three positions lock the hash baseline (do not reorder).
     CreateStream              => create_stream,
     SendMessages              => send_messages,
-    StoreConsumerOffset2      => store_consumer_offset_2,
+    StoreConsumerOffset       => store_consumer_offset,
     // Append-only; mirrors actions::Action declaration order.
     DeleteStream              => delete_stream,
     UpdateStream              => update_stream,
@@ -193,7 +191,5 @@
     UpdatePermissions         => update_permissions,
     CreatePersonalAccessToken => create_personal_access_token,
     DeletePersonalAccessToken => delete_personal_access_token,
-    StoreConsumerOffset       => store_consumer_offset,
     DeleteConsumerOffset      => delete_consumer_offset,
-    DeleteConsumerOffset2     => delete_consumer_offset_2,
 }
diff --git a/core/simulator/src/workload/ops/store_consumer_offset.rs b/core/simulator/src/workload/ops/store_consumer_offset.rs
index edefbfc..1f2821b 100644
--- a/core/simulator/src/workload/ops/store_consumer_offset.rs
+++ b/core/simulator/src/workload/ops/store_consumer_offset.rs
@@ -15,10 +15,15 @@
 // specific language governing permissions and limitations
 // under the License.
 
-//! `StoreConsumerOffset` op. Pre-`AckLevel` manual encoding. Live
-//! namespace via shadow, fabricated consumer kind/id. Samples Success.
+//! `StoreConsumerOffset` op. Samples only `Success`. PRNG draw order:
+//!
+//! 1. namespace pick
+//! 2. `consumer_kind` boolean draw
+//! 3. `consumer_id` range draw
+//! 4. `offset` range draw
+//! 5. `ack` ratio draw
 
-use iggy_binary_protocol::RoutedRequestHeader;
+use iggy_binary_protocol::{AckLevel, RoutedRequestHeader};
 use rand::RngExt;
 use rand_xoshiro::Xoshiro256Plus;
 use server_common::Message;
@@ -35,6 +40,7 @@
     pub consumer_kind: u8,
     pub consumer_id: u32,
     pub offset: u64,
+    pub ack: AckLevel,
 }
 
 #[derive(Copy, Clone, Eq, PartialEq, Debug)]
@@ -62,11 +68,18 @@
             let raw: u64 = prng.random_range(0..options.max_offset.max(1));
             let high = shadow.sends_committed(ns).max(1);
             let offset = raw % high;
+            let f: f32 = prng.random();
+            let ack = if f < options.ack_quorum_ratio {
+                AckLevel::Quorum
+            } else {
+                AckLevel::NoAck
+            };
             Some(Input {
                 ns,
                 consumer_kind,
                 consumer_id,
                 offset,
+                ack,
             })
         }
     }
@@ -79,6 +92,7 @@
         input.consumer_kind,
         input.consumer_id,
         input.offset,
+        input.ack,
     )
 }
 
diff --git a/core/simulator/src/workload/ops/store_consumer_offset_2.rs b/core/simulator/src/workload/ops/store_consumer_offset_2.rs
deleted file mode 100644
index baac692..0000000
--- a/core/simulator/src/workload/ops/store_consumer_offset_2.rs
+++ /dev/null
@@ -1,112 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-//! `StoreConsumerOffset2` op. Samples only `Success`. PRNG draw order:
-//!
-//! 1. namespace pick
-//! 2. `consumer_kind` boolean draw
-//! 3. `consumer_id` range draw
-//! 4. `offset` range draw
-//! 5. `ack` ratio draw
-
-use iggy_binary_protocol::{AckLevel, RoutedRequestHeader};
-use rand::RngExt;
-use rand_xoshiro::Xoshiro256Plus;
-use server_common::Message;
-use server_common::sharding::IggyNamespace;
-
-use crate::client::SimClient;
-use crate::workload::effect::Effect;
-use crate::workload::options::WorkloadOptions;
-use crate::workload::shadow::Shadow;
-
-#[derive(Debug, Clone)]
-pub struct Input {
-    pub ns: IggyNamespace,
-    pub consumer_kind: u8,
-    pub consumer_id: u32,
-    pub offset: u64,
-    pub ack: AckLevel,
-}
-
-#[derive(Copy, Clone, Eq, PartialEq, Debug)]
-pub enum Outcome {
-    Success,
-}
-
-pub const OUTCOMES: &[Outcome] = &[Outcome::Success];
-
-pub fn sample(
-    shadow: &mut Shadow,
-    outcome: Outcome,
-    prng: &mut Xoshiro256Plus,
-    options: &WorkloadOptions,
-) -> Option<Input> {
-    match outcome {
-        Outcome::Success => {
-            let ns = shadow.pick_namespace(prng)?;
-            let consumer_kind: u8 = u8::from(prng.random::<bool>());
-            let consumer_id: u32 = prng.random_range(0..options.consumer_pool_size.max(1));
-            // Draw against the configured ceiling, then clamp to committed
-            // reality so the offset is reachable. Clamping post-draw keeps
-            // the PRNG draw order (and determinism hash baseline) intact
-            // while staying valid once the server validates offsets.
-            let raw: u64 = prng.random_range(0..options.max_offset.max(1));
-            let high = shadow.sends_committed(ns).max(1);
-            let offset = raw % high;
-            let f: f32 = prng.random();
-            let ack = if f < options.ack_quorum_ratio {
-                AckLevel::Quorum
-            } else {
-                AckLevel::NoAck
-            };
-            Some(Input {
-                ns,
-                consumer_kind,
-                consumer_id,
-                offset,
-                ack,
-            })
-        }
-    }
-}
-
-#[must_use]
-pub fn build_message(client: &SimClient, input: &Input) -> Message<RoutedRequestHeader> {
-    client.store_consumer_offset_2(
-        input.ns,
-        input.consumer_kind,
-        input.consumer_id,
-        input.offset,
-        input.ack,
-    )
-}
-
-#[must_use]
-pub const fn classify_reply(_code: u32) -> Outcome {
-    Outcome::Success
-}
-
-#[must_use]
-pub const fn predicted_effect(input: &Input, outcome: Outcome) -> Effect {
-    match outcome {
-        Outcome::Success => Effect::OffsetStored {
-            key: (input.ns, input.consumer_kind, input.consumer_id),
-            value: input.offset,
-        },
-    }
-}
diff --git a/core/simulator/src/workload/options.rs b/core/simulator/src/workload/options.rs
index a6c0bbe..31ab544 100644
--- a/core/simulator/src/workload/options.rs
+++ b/core/simulator/src/workload/options.rs
@@ -58,7 +58,7 @@
         Self::new(&[
             (Action::CreateStream, 5),
             (Action::SendMessages, 70),
-            (Action::StoreConsumerOffset2, 25),
+            (Action::StoreConsumerOffset, 25),
         ])
     }
 }
@@ -80,7 +80,7 @@
     /// Send-batch size = `batch_size_min + prng.range(batch_size_span)`.
     pub batch_size_min: u32,
     pub batch_size_span: u32,
-    /// Probability a `StoreConsumerOffset2` request uses `Quorum` vs `NoAck`.
+    /// Probability a `StoreConsumerOffset` request uses `Quorum` vs `NoAck`.
     pub ack_quorum_ratio: f32,
     /// Probability a request targets a non-primary replica (exercises the
     /// redirect / forward path).
@@ -88,9 +88,9 @@
     /// Probability that a request is intentionally constructed to fail
     /// validation. Currently unused; reserved.
     pub invalid_request_ratio: f32,
-    /// Distinct consumer ids round-robined in `StoreConsumerOffset2`.
+    /// Distinct consumer ids round-robined in `StoreConsumerOffset`.
     pub consumer_pool_size: u32,
-    /// Upper bound on offset carried by `StoreConsumerOffset2`.
+    /// Upper bound on offset carried by `StoreConsumerOffset`.
     pub max_offset: u64,
     /// Probability per tick that the driver crashes one live non-primary
     /// replica (crash-only, no restart). `0.0` disables injection: the fault
diff --git a/examples/go/go.mod b/examples/go/go.mod
index c41e580..a3db555 100644
--- a/examples/go/go.mod
+++ b/examples/go/go.mod
@@ -10,4 +10,7 @@
 	github.com/avast/retry-go/v5 v5.0.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/klauspost/compress v1.19.2 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+	github.com/zeebo/xxh3 v1.1.0 // indirect
+	golang.org/x/sys v0.30.0 // indirect
 )
diff --git a/examples/go/go.sum b/examples/go/go.sum
index 54cad5a..438f23b 100644
--- a/examples/go/go.sum
+++ b/examples/go/go.sum
@@ -8,9 +8,17 @@
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
 github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
+github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/examples/python/uv.lock b/examples/python/uv.lock
index 137bd9a..ec009c7 100644
--- a/examples/python/uv.lock
+++ b/examples/python/uv.lock
@@ -8,7 +8,7 @@
 
 [[package]]
 name = "apache-iggy"
-version = "0.9.0.dev3"
+version = "0.9.0.dev4"
 source = { directory = "../../foreign/python" }
 
 [package.metadata]
diff --git a/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs b/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs
index 20486f2..b367cc2 100644
--- a/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs
+++ b/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs
@@ -17,6 +17,7 @@
 
 using System.Buffers;
 using System.Buffers.Binary;
+using System.IO.Hashing;
 using System.Runtime.CompilerServices;
 using System.Text;
 using Apache.Iggy.Contracts.Auth;
@@ -37,6 +38,9 @@
     /// <summary>Frames wider than this are built on the heap instead of the stack.</summary>
     private const int MaxStackAllocBytes = 1024;
 
+    /// <summary>Offset mutations always request quorum acknowledgement.</summary>
+    private const byte AckQuorum = 1;
+
     internal static byte[] LoginWithPersonalAccessToken(string token)
     {
         var tokenLength = Encoding.UTF8.GetByteCount(token);
@@ -400,9 +404,19 @@
         bytes[position + 18] = autoCommit ? (byte)1 : (byte)0;
     }
 
+    /// <summary>
+    ///     Encodes a SendMessages body: routing metadata followed by one canonical batch record
+    ///     (a 256-byte batch header plus per-message frames). The server stamps
+    ///     <c>partition_id</c>, <c>base_offset</c>, and <c>base_timestamp</c>; they stay zero here.
+    /// </summary>
     internal static int CreateMessage(Span<byte> bytes, Identifier streamId, Identifier topicId,
         Partitioning partitioning, ReadOnlySpan<Message> messages, IMessageEncryptor? encryptor = null)
     {
+        if (messages.IsEmpty)
+        {
+            throw new ArgumentException("Batch must contain at least one message.", nameof(messages));
+        }
+
         var metadataLength = 2 + streamId.Length + 2 + topicId.Length + 2 + partitioning.Length + 4;
         BinaryPrimitives.WriteInt32LittleEndian(bytes[..4], metadataLength);
         bytes.WriteBytesFromStreamAndTopicIdentifiers(streamId, topicId, 4);
@@ -412,10 +426,23 @@
         BinaryPrimitives.WriteInt32LittleEndian(bytes[position..(position + 4)], messages.Length);
         position += 4;
 
-        var indexPosition = position;
-        position += 16 * messages.Length;
+        // The producer owns message ids: a zero id is minted before the frame checksum covers it.
+        var originTimestamp = ulong.MaxValue;
+        foreach (var message in messages)
+        {
+            if (message.Header.Id == 0)
+            {
+                message.Header = message.Header with { Id = Guid.NewGuid().ToUInt128() };
+            }
 
-        var msgSize = 0;
+            originTimestamp = Math.Min(originTimestamp, message.Header.OriginTimestamp);
+        }
+
+        var batchStart = position;
+        bytes[batchStart..(batchStart + BatchWireFormat.BATCH_HEADER_SIZE)].Clear();
+        position += BatchWireFormat.BATCH_HEADER_SIZE;
+        var blobStart = position;
+        var offsetDelta = 0u;
 
         // One scratch buffer reused across the batch (grown on demand); holds plaintext headers, so it is
         // returned cleared on every path, including a mid-batch throw from the encryptor or header serialization.
@@ -425,7 +452,16 @@
             foreach (var message in messages)
             {
                 var header = message.Header;
-                var payloadStart = position + 64;
+                var timestampDelta = header.OriginTimestamp - originTimestamp;
+                if (timestampDelta > uint.MaxValue)
+                {
+                    throw new ArgumentException(
+                        $"Message origin timestamp runs {timestampDelta} microseconds ahead of the batch's " +
+                        $"earliest one; the frame field holds at most {uint.MaxValue}.", nameof(messages));
+                }
+
+                var frameStart = position;
+                var payloadStart = frameStart + BatchWireFormat.FRAME_HEADER_SIZE;
 
                 int payloadLength;
                 if (encryptor is null)
@@ -489,26 +525,19 @@
                     }
                 }
 
-                BinaryPrimitives.WriteUInt64LittleEndian(bytes[position..(position + 8)], 0);
-                BinaryPrimitives.WriteUInt128LittleEndian(bytes[(position + 8)..(position + 24)], header.Id);
-                BinaryPrimitives.WriteUInt64LittleEndian(bytes[(position + 24)..(position + 32)], header.Offset);
-                BinaryPrimitives.WriteUInt64LittleEndian(bytes[(position + 32)..(position + 40)],
-                    DateTimeOffsetUtils.ToUnixTimeMicroSeconds(header.Timestamp));
-                BinaryPrimitives.WriteUInt64LittleEndian(bytes[(position + 40)..(position + 48)],
-                    header.OriginTimestamp);
-                BinaryPrimitives.WriteInt32LittleEndian(bytes[(position + 48)..(position + 52)], headersLength);
-                BinaryPrimitives.WriteInt32LittleEndian(bytes[(position + 52)..(position + 56)], payloadLength);
+                BinaryPrimitives.WriteUInt128LittleEndian(bytes[(frameStart + 8)..(frameStart + 24)], header.Id);
+                BinaryPrimitives.WriteUInt32LittleEndian(bytes[(frameStart + 24)..(frameStart + 28)], offsetDelta);
+                BinaryPrimitives.WriteUInt32LittleEndian(bytes[(frameStart + 28)..(frameStart + 32)],
+                    (uint)timestampDelta);
+                BinaryPrimitives.WriteInt32LittleEndian(bytes[(frameStart + 32)..(frameStart + 36)], headersLength);
+                BinaryPrimitives.WriteInt32LittleEndian(bytes[(frameStart + 36)..(frameStart + 40)], payloadLength);
                 // Reserved must be zero on the wire; the server rejects non-zero values.
-                BinaryPrimitives.WriteUInt64LittleEndian(bytes[(position + 56)..(position + 64)], 0);
+                BinaryPrimitives.WriteUInt64LittleEndian(bytes[(frameStart + 40)..(frameStart + 48)], 0);
 
-                position += 64 + payloadLength + headersLength;
-
-                msgSize += 64 + payloadLength + headersLength;
-
-                BinaryPrimitives.WriteInt32LittleEndian(bytes[indexPosition..(indexPosition + 4)], 0);
-                BinaryPrimitives.WriteInt32LittleEndian(bytes[(indexPosition + 4)..(indexPosition + 8)], msgSize);
-                BinaryPrimitives.WriteInt64LittleEndian(bytes[(indexPosition + 8)..(indexPosition + 16)], 0);
-                indexPosition += 16;
+                position = headersStart + headersLength;
+                var frameChecksum = XxHash3.HashToUInt64(bytes[(frameStart + 8)..position]);
+                BinaryPrimitives.WriteUInt64LittleEndian(bytes[frameStart..(frameStart + 8)], frameChecksum);
+                offsetDelta++;
             }
         }
         finally
@@ -519,9 +548,38 @@
             }
         }
 
+        var batchLength = (ulong)(BatchWireFormat.BATCH_HEADER_SIZE + (position - blobStart));
+        BinaryPrimitives.WriteUInt64LittleEndian(bytes[(batchStart + 24)..(batchStart + 32)], originTimestamp);
+        BinaryPrimitives.WriteUInt64LittleEndian(bytes[(batchStart + 32)..(batchStart + 40)], batchLength);
+        BinaryPrimitives.WriteUInt32LittleEndian(bytes[(batchStart + 48)..(batchStart + 52)], (uint)messages.Length);
+        var batchChecksum = CalculateBatchChecksum(bytes, batchStart, blobStart, position);
+        BinaryPrimitives.WriteUInt64LittleEndian(bytes[(batchStart + 40)..(batchStart + 48)], batchChecksum);
+
         return position;
     }
 
+    /// <summary>
+    ///     Batch checksum: XXH3-64 over the batch header meta fields followed by each frame's stored
+    ///     8-byte checksum field in message order. Bodies are bound transitively through the
+    ///     per-frame checksums. The header fields must already be backpatched into <paramref name="bytes" />.
+    /// </summary>
+    private static ulong CalculateBatchChecksum(ReadOnlySpan<byte> bytes, int batchStart, int blobStart, int blobEnd)
+    {
+        var hasher = new XxHash3();
+        hasher.Append(bytes.Slice(batchStart, 40));
+        hasher.Append(bytes.Slice(batchStart + 48, 4));
+        var cursor = blobStart;
+        while (cursor < blobEnd)
+        {
+            hasher.Append(bytes.Slice(cursor, 8));
+            var headersLength = BinaryPrimitives.ReadInt32LittleEndian(bytes[(cursor + 32)..(cursor + 36)]);
+            var payloadLength = BinaryPrimitives.ReadInt32LittleEndian(bytes[(cursor + 36)..(cursor + 40)]);
+            cursor += BatchWireFormat.FRAME_HEADER_SIZE + payloadLength + headersLength;
+        }
+
+        return hasher.GetCurrentHashAsUInt64();
+    }
+
     internal static int HeadersByteLength(Dictionary<HeaderKey, HeaderValue>? headers)
     {
         if (headers is null)
@@ -810,7 +868,7 @@
         uint? partitionId)
     {
         Span<byte> bytes =
-            stackalloc byte[2 + streamId.Length + 2 + topicId.Length + 13 + 1 + 2 + consumer.ConsumerId.Length];
+            stackalloc byte[2 + streamId.Length + 2 + topicId.Length + 14 + 1 + 2 + consumer.ConsumerId.Length];
         bytes[0] = GetConsumerTypeByte(consumer.Type);
         bytes.WriteBytesFromIdentifier(consumer.ConsumerId, 1);
         var position = 1 + consumer.ConsumerId.Length + 2;
@@ -830,6 +888,7 @@
         }
 
         BinaryPrimitives.WriteUInt64LittleEndian(bytes[(position + 5)..(position + 13)], offset);
+        bytes[position + 13] = AckQuorum;
         return bytes.ToArray();
     }
 
@@ -933,7 +992,7 @@
     internal static byte[] DeleteOffset(Identifier streamId, Identifier topicId, Consumer consumer, uint? partitionId)
     {
         Span<byte> bytes =
-            stackalloc byte[2 + streamId.Length + 2 + topicId.Length + 5 + 1 + 2 + consumer.ConsumerId.Length];
+            stackalloc byte[2 + streamId.Length + 2 + topicId.Length + 6 + 1 + 2 + consumer.ConsumerId.Length];
         bytes[0] = GetConsumerTypeByte(consumer.Type);
         bytes.WriteBytesFromIdentifier(consumer.ConsumerId, 1);
         var position = 1 + consumer.ConsumerId.Length + 2;
@@ -952,6 +1011,7 @@
             BinaryPrimitives.WriteUInt32LittleEndian(bytes[(position + 1)..(position + 5)], 0); // Padding
         }
 
+        bytes[position + 5] = AckQuorum;
         return bytes.ToArray();
     }
 }
diff --git a/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.Vsr.cs b/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.Vsr.cs
index f70a3b2..1885c69 100644
--- a/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.Vsr.cs
+++ b/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.Vsr.cs
@@ -725,7 +725,7 @@
         await ReadExactVsrAsync(stream, _vsrReplyHeaderBuffer, readCancellation.Token, token);
 
         var command = VsrHeader.PeekCommand(_vsrReplyHeaderBuffer);
-        if (command == Command2.Eviction)
+        if (command == Command.Eviction)
         {
             var eviction = VsrHeader.ReadEviction(_vsrReplyHeaderBuffer);
             _logger.LogWarning("Consensus session evicted by the server: {Reason}", eviction.Reason);
@@ -734,7 +734,7 @@
             throw new VsrSessionEvictedException(VsrReplyDecoder.ToException(eviction));
         }
 
-        if (command != Command2.Reply)
+        if (command != Command.Reply)
         {
             // Neither a reply nor an eviction: this frame was never an answer to the outstanding request, so
             // whatever the peer does send for it would be read as the next request's reply and handed to the
diff --git a/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj b/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj
index c0fe627..6a22ab0 100644
--- a/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj
+++ b/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj
@@ -27,7 +27,7 @@
         <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
         <AssemblyName>Apache.Iggy</AssemblyName>
         <RootNamespace>Apache.Iggy</RootNamespace>
-        <Version>0.9.0-edge.3</Version>
+        <Version>0.9.0-edge.4</Version>
         <GenerateDocumentationFile>true</GenerateDocumentationFile>
     </PropertyGroup>
 
diff --git a/foreign/csharp/Iggy_SDK/Mappers/BinaryMapper.cs b/foreign/csharp/Iggy_SDK/Mappers/BinaryMapper.cs
index 5b443fc..51fb8d8 100644
--- a/foreign/csharp/Iggy_SDK/Mappers/BinaryMapper.cs
+++ b/foreign/csharp/Iggy_SDK/Mappers/BinaryMapper.cs
@@ -33,8 +33,6 @@
 
 internal static class BinaryMapper
 {
-    private const int PropertiesSize = 64;
-
     internal static RawPersonalAccessToken MapRawPersonalAccessToken(ReadOnlySpan<byte> payload)
     {
         var tokenLength = payload[0];
@@ -376,87 +374,90 @@
                 }
             }
 
-            var maxMessages = (length - 16) / PropertiesSize;
+            var maxMessages = (length - 16) / BatchWireFormat.FRAME_HEADER_SIZE;
             var capacity = (int)Math.Min(messagesCount, (uint)maxMessages);
             List<RentedMessageResponse> messages = new(capacity);
 
             while (position < length)
             {
-                if (!TryReadFrameLengths(span, length, position, out var headersLength, out var payloadLength))
+                var batchEnd = ReadBatchExtent(span, length, position, out var baseOffset, out var baseTimestamp,
+                    out var batchOriginTimestamp);
+                // Broker append time is stamped once per batch record; the per-frame delta applies to the
+                // origin timestamp only.
+                var timestamp = DateTimeOffsetUtils.FromUnixTimeMicroSeconds(baseTimestamp);
+                var cursor = position + BatchWireFormat.BATCH_HEADER_SIZE;
+                while (cursor < batchEnd)
                 {
-                    break;
-                }
+                    ReadFrameLengths(span, cursor, batchEnd, out var headersLength, out var payloadLength);
 
-                var checksum = BinaryPrimitives.ReadUInt64LittleEndian(span[position..(position + 8)]);
-                var id = BinaryPrimitives.ReadUInt128LittleEndian(span[(position + 8)..(position + 24)]);
-                var offset = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 24)..(position + 32)]);
-                var timestamp = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 32)..(position + 40)]);
-                var originTimestamp = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 40)..(position + 48)]);
-                var reserved = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 56)..(position + 64)]);
+                    var checksum = BinaryPrimitives.ReadUInt64LittleEndian(span[cursor..(cursor + 8)]);
+                    var id = BinaryPrimitives.ReadUInt128LittleEndian(span[(cursor + 8)..(cursor + 24)]);
+                    var offsetDelta = BinaryPrimitives.ReadUInt32LittleEndian(span[(cursor + 24)..(cursor + 28)]);
+                    var timestampDelta = BinaryPrimitives.ReadUInt32LittleEndian(span[(cursor + 28)..(cursor + 32)]);
+                    var offset = baseOffset + offsetDelta;
 
-                var payloadRangeStart = position + 64;
-                var headersRangeStart = payloadRangeStart + payloadLength;
+                    var payloadRangeStart = cursor + BatchWireFormat.FRAME_HEADER_SIZE;
+                    var headersRangeStart = payloadRangeStart + payloadLength;
 
-                ReadOnlyMemory<byte> payloadSlice = payload.Slice(payloadRangeStart, payloadLength);
-                ReadOnlyMemory<byte> rawHeaders = headersLength > 0
-                    ? payload.Slice(headersRangeStart, headersLength)
-                    : ReadOnlyMemory<byte>.Empty;
+                    ReadOnlyMemory<byte> payloadSlice = payload.Slice(payloadRangeStart, payloadLength);
+                    ReadOnlyMemory<byte> rawHeaders = headersLength > 0
+                        ? payload.Slice(headersRangeStart, headersLength)
+                        : ReadOnlyMemory<byte>.Empty;
 
-                // Decrypt into the shared buffer so the message looks like plaintext downstream. Wire lengths
-                // still drive the cursor advance; only the decrypted lengths land on the header.
-                var storedPayloadLength = payloadLength;
-                var storedHeadersLength = headersLength;
-                if (encryptor is not null)
-                {
-                    try
+                    // Decrypt into the shared buffer so the message looks like plaintext downstream. Wire lengths
+                    // still drive the cursor advance; only the decrypted lengths land on the header.
+                    var storedPayloadLength = payloadLength;
+                    var storedHeadersLength = headersLength;
+                    if (encryptor is not null)
                     {
-                        // Bound each destination to this message's reserved slice so an encryptor that overruns
-                        // its contract fails fast here instead of corrupting the next message's region.
-                        Memory<byte> payloadDest =
-                            plaintext.Slice(plainCursor, encryptor.GetMaxDecryptedLength(payloadLength));
-                        var writtenPayload = encryptor.Decrypt(payloadSlice.Span, payloadDest.Span);
-                        payloadSlice = payloadDest.Slice(0, writtenPayload);
-                        storedPayloadLength = writtenPayload;
-                        plainCursor += writtenPayload;
-
-                        if (!rawHeaders.IsEmpty)
+                        try
                         {
-                            Memory<byte> headersDest =
-                                plaintext.Slice(plainCursor, encryptor.GetMaxDecryptedLength(headersLength));
-                            var writtenHeaders = encryptor.Decrypt(rawHeaders.Span, headersDest.Span);
-                            rawHeaders = headersDest.Slice(0, writtenHeaders);
-                            storedHeadersLength = writtenHeaders;
-                            plainCursor += writtenHeaders;
+                            // Bound each destination to this message's reserved slice so an encryptor that overruns
+                            // its contract fails fast here instead of corrupting the next message's region.
+                            Memory<byte> payloadDest =
+                                plaintext.Slice(plainCursor, encryptor.GetMaxDecryptedLength(payloadLength));
+                            var writtenPayload = encryptor.Decrypt(payloadSlice.Span, payloadDest.Span);
+                            payloadSlice = payloadDest.Slice(0, writtenPayload);
+                            storedPayloadLength = writtenPayload;
+                            plainCursor += writtenPayload;
+
+                            if (!rawHeaders.IsEmpty)
+                            {
+                                Memory<byte> headersDest =
+                                    plaintext.Slice(plainCursor, encryptor.GetMaxDecryptedLength(headersLength));
+                                var writtenHeaders = encryptor.Decrypt(rawHeaders.Span, headersDest.Span);
+                                rawHeaders = headersDest.Slice(0, writtenHeaders);
+                                storedHeadersLength = writtenHeaders;
+                                plainCursor += writtenHeaders;
+                            }
+                        }
+                        catch (Exception ex)
+                        {
+                            throw new MessageDecryptionException(offset, (uint)partitionId, ex);
                         }
                     }
-                    catch (Exception ex)
+
+                    messages.Add(new RentedMessageResponse
                     {
-                        throw new MessageDecryptionException(offset, (uint)partitionId, ex);
-                    }
+                        Header = new MessageHeader
+                        {
+                            Checksum = checksum,
+                            Id = id,
+                            Offset = offset,
+                            OriginTimestamp = batchOriginTimestamp + timestampDelta,
+                            PayloadLength = storedPayloadLength,
+                            Timestamp = timestamp,
+                            UserHeadersLength = storedHeadersLength,
+                            Reserved = 0
+                        },
+                        RawUserHeaders = rawHeaders,
+                        Payload = payloadSlice
+                    });
+
+                    cursor = headersRangeStart + headersLength;
                 }
 
-                messages.Add(new RentedMessageResponse
-                {
-                    Header = new MessageHeader
-                    {
-                        Checksum = checksum,
-                        Id = id,
-                        Offset = offset,
-                        OriginTimestamp = originTimestamp,
-                        PayloadLength = storedPayloadLength,
-                        Timestamp = DateTimeOffsetUtils.FromUnixTimeMicroSeconds(timestamp),
-                        UserHeadersLength = storedHeadersLength,
-                        Reserved = reserved
-                    },
-                    RawUserHeaders = rawHeaders,
-                    Payload = payloadSlice
-                });
-
-                position += 64 + payloadLength + headersLength;
-                if (position + PropertiesSize >= length)
-                {
-                    break;
-                }
+                position = batchEnd;
             }
 
             return new PolledMessagesRental(payloadOwner, plaintextOwner)
@@ -474,60 +475,84 @@
     }
 
     // Shared by the decrypt sizing pre-pass and the main map loop so both agree on which frames are included;
-    // drift would mis-size the shared plaintext buffer. Returns false at buffer end or on a frame running past
-    // the buffer; throws on a negative length so a poison frame surfaces instead of being re-polled forever.
-    private static bool TryReadFrameLengths(ReadOnlySpan<byte> span, int length, int position,
-        out int headersLength, out int payloadLength)
+    // drift would mis-size the shared plaintext buffer.
+    private static int ReadBatchExtent(ReadOnlySpan<byte> span, int length, int position, out ulong baseOffset,
+        out ulong baseTimestamp, out ulong originTimestamp)
     {
-        headersLength = 0;
-        payloadLength = 0;
-        if (position + PropertiesSize > length)
+        if (position + BatchWireFormat.BATCH_HEADER_SIZE > length)
         {
-            return false;
+            throw new MalformedResponseException(
+                $"Malformed batch record at byte {position}: {length - position} bytes cannot hold a batch header.");
         }
 
-        headersLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 48)..(position + 52)]);
-        payloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 52)..(position + 56)]);
+        baseOffset = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 8)..(position + 16)]);
+        baseTimestamp = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 16)..(position + 24)]);
+        originTimestamp = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 24)..(position + 32)]);
+        var batchLength = BinaryPrimitives.ReadUInt64LittleEndian(span[(position + 32)..(position + 40)]);
+        if (batchLength < BatchWireFormat.BATCH_HEADER_SIZE || (ulong)position + batchLength > (ulong)length)
+        {
+            throw new MalformedResponseException(
+                $"Malformed batch record at byte {position}: batch length {batchLength} does not fit the response.");
+        }
+
+        return position + (int)batchLength;
+    }
+
+    private static void ReadFrameLengths(ReadOnlySpan<byte> span, int cursor, int batchEnd,
+        out int headersLength, out int payloadLength)
+    {
+        if (cursor + BatchWireFormat.FRAME_HEADER_SIZE > batchEnd)
+        {
+            throw new MalformedResponseException(
+                $"Malformed message frame at byte {cursor}: {batchEnd - cursor} bytes cannot hold a frame header.");
+        }
+
+        headersLength = BinaryPrimitives.ReadInt32LittleEndian(span[(cursor + 32)..(cursor + 36)]);
+        payloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(cursor + 36)..(cursor + 40)]);
         if (headersLength < 0 || payloadLength < 0)
         {
             throw new MalformedResponseException(
-                $"Malformed message frame at byte {position}: negative payload ({payloadLength}) or header " +
+                $"Malformed message frame at byte {cursor}: negative payload ({payloadLength}) or header " +
                 $"({headersLength}) length.");
         }
 
-        // Overflow-safe: server-controlled lengths can approach int.MaxValue, so compute the bound in long.
-        if ((long)position + 64 + payloadLength + headersLength > length)
+        if (BinaryPrimitives.ReadUInt64LittleEndian(span[(cursor + 40)..(cursor + 48)]) != 0)
         {
-            return false;
+            throw new MalformedResponseException(
+                $"Malformed message frame at byte {cursor}: reserved bytes must be zero.");
         }
 
-        return true;
+        // Overflow-safe: server-controlled lengths can approach int.MaxValue, so compute the bound in long.
+        if ((long)cursor + BatchWireFormat.FRAME_HEADER_SIZE + payloadLength + headersLength > batchEnd)
+        {
+            throw new MalformedResponseException(
+                $"Malformed message frame at byte {cursor}: frame runs past its batch record.");
+        }
     }
 
-    // Pre-pass summing upper-bound plaintext length so the shared buffer is rented exactly once. Same
-    // TryReadFrameLengths walk as the main loop, so both agree on which messages are included.
+    // Pre-pass summing upper-bound plaintext length so the shared buffer is rented exactly once. Same batch
+    // and frame walk as the main loop, so both agree on which messages are included.
     private static int SumMaxDecryptedLength(ReadOnlySpan<byte> span, int length, IMessageEncryptor encryptor)
     {
         var position = 16;
         var total = 0;
         while (position < length)
         {
-            if (!TryReadFrameLengths(span, length, position, out var headersLength, out var payloadLength))
+            var batchEnd = ReadBatchExtent(span, length, position, out _, out _, out _);
+            var cursor = position + BatchWireFormat.BATCH_HEADER_SIZE;
+            while (cursor < batchEnd)
             {
-                break;
+                ReadFrameLengths(span, cursor, batchEnd, out var headersLength, out var payloadLength);
+                total += encryptor.GetMaxDecryptedLength(payloadLength);
+                if (headersLength > 0)
+                {
+                    total += encryptor.GetMaxDecryptedLength(headersLength);
+                }
+
+                cursor += BatchWireFormat.FRAME_HEADER_SIZE + payloadLength + headersLength;
             }
 
-            total += encryptor.GetMaxDecryptedLength(payloadLength);
-            if (headersLength > 0)
-            {
-                total += encryptor.GetMaxDecryptedLength(headersLength);
-            }
-
-            position += 64 + payloadLength + headersLength;
-            if (position + PropertiesSize >= length)
-            {
-                break;
-            }
+            position = batchEnd;
         }
 
         return total;
diff --git a/foreign/csharp/Iggy_SDK/Vsr/Command2.cs b/foreign/csharp/Iggy_SDK/Messages/BatchWireFormat.cs
similarity index 67%
copy from foreign/csharp/Iggy_SDK/Vsr/Command2.cs
copy to foreign/csharp/Iggy_SDK/Messages/BatchWireFormat.cs
index c8f7a76..b67c33d 100644
--- a/foreign/csharp/Iggy_SDK/Vsr/Command2.cs
+++ b/foreign/csharp/Iggy_SDK/Messages/BatchWireFormat.cs
@@ -15,16 +15,16 @@
 // specific language governing permissions and limitations
 // under the License.
 
-namespace Apache.Iggy.Vsr;
+namespace Apache.Iggy.Messages;
 
 /// <summary>
-///     VSR frame discriminant, byte 60 of every consensus header. Only the frames a client emits or
-///     receives are named; every other discriminant decodes as <see cref="Reserved" />.
+///     Sizes of the canonical message batch record shared by send requests and poll responses:
+///     a 256-byte batch header followed by per-message frames of
+///     <c>[48-byte frame header][payload][user headers]</c>.
 /// </summary>
-internal enum Command2 : byte
+internal static class BatchWireFormat
 {
-    Reserved = 0,
-    Request = 5,
-    Reply = 8,
-    Eviction = 13
+    internal const int BATCH_HEADER_SIZE = 256;
+
+    internal const int FRAME_HEADER_SIZE = 48;
 }
diff --git a/foreign/csharp/Iggy_SDK/Utils/CommandCodes.cs b/foreign/csharp/Iggy_SDK/Utils/CommandCodes.cs
index 71e945a..067e097 100644
--- a/foreign/csharp/Iggy_SDK/Utils/CommandCodes.cs
+++ b/foreign/csharp/Iggy_SDK/Utils/CommandCodes.cs
@@ -48,8 +48,6 @@
     internal const int GET_CONSUMER_OFFSET_CODE = 120;
     internal const int STORE_CONSUMER_OFFSET_CODE = 121;
     internal const int DELETE_CONSUMER_OFFSET_CODE = 122;
-    internal const int STORE_CONSUMER_OFFSET_2_CODE = 123;
-    internal const int DELETE_CONSUMER_OFFSET_2_CODE = 124;
     internal const int GET_STREAM_CODE = 200;
     internal const int GET_STREAMS_CODE = 201;
     internal const int CREATE_STREAM_CODE = 202;
diff --git a/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs b/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs
index 6f0dc13..49b6ffe 100644
--- a/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs
+++ b/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs
@@ -46,7 +46,7 @@
 
     internal static int CalculateMessageBytesCount(ReadOnlySpan<Message> messages, IMessageEncryptor? encryptor)
     {
-        var bytesCount = 0;
+        var bytesCount = BatchWireFormat.BATCH_HEADER_SIZE;
         foreach (var message in messages)
         {
             var payloadLength = message.Payload.Length;
@@ -63,7 +63,7 @@
                 }
             }
 
-            bytesCount += 16 + 64 + payloadLength + headersLength;
+            bytesCount += BatchWireFormat.FRAME_HEADER_SIZE + payloadLength + headersLength;
         }
 
         return bytesCount;
diff --git a/foreign/csharp/Iggy_SDK/Vsr/Command2.cs b/foreign/csharp/Iggy_SDK/Vsr/Command.cs
similarity index 97%
rename from foreign/csharp/Iggy_SDK/Vsr/Command2.cs
rename to foreign/csharp/Iggy_SDK/Vsr/Command.cs
index c8f7a76..a124cc8 100644
--- a/foreign/csharp/Iggy_SDK/Vsr/Command2.cs
+++ b/foreign/csharp/Iggy_SDK/Vsr/Command.cs
@@ -21,7 +21,7 @@
 ///     VSR frame discriminant, byte 60 of every consensus header. Only the frames a client emits or
 ///     receives are named; every other discriminant decodes as <see cref="Reserved" />.
 /// </summary>
-internal enum Command2 : byte
+internal enum Command : byte
 {
     Reserved = 0,
     Request = 5,
diff --git a/foreign/csharp/Iggy_SDK/Vsr/VsrHeader.cs b/foreign/csharp/Iggy_SDK/Vsr/VsrHeader.cs
index ad58e0c..3de6211 100644
--- a/foreign/csharp/Iggy_SDK/Vsr/VsrHeader.cs
+++ b/foreign/csharp/Iggy_SDK/Vsr/VsrHeader.cs
@@ -81,7 +81,7 @@
         var totalSize = HEADER_SIZE + payload.Length;
 
         BinaryPrimitives.WriteUInt32LittleEndian(header[SIZE_OFFSET..], (uint)totalSize);
-        header[COMMAND_OFFSET] = (byte)Command2.Request;
+        header[COMMAND_OFFSET] = (byte)Command.Request;
         WriteUInt128(header[REQUEST_CLIENT_OFFSET..], frame.ClientId);
         BinaryPrimitives.WriteUInt64LittleEndian(header[REQUEST_TIMESTAMP_OFFSET..], 0);
         BinaryPrimitives.WriteUInt64LittleEndian(header[REQUEST_ID_OFFSET..], frame.RequestId);
@@ -96,13 +96,13 @@
         return totalSize;
     }
 
-    internal static Command2 PeekCommand(ReadOnlySpan<byte> header)
+    internal static Command PeekCommand(ReadOnlySpan<byte> header)
     {
         return header[COMMAND_OFFSET] switch
         {
-            (byte)Command2.Reply => Command2.Reply,
-            (byte)Command2.Eviction => Command2.Eviction,
-            _ => Command2.Reserved
+            (byte)Command.Reply => Command.Reply,
+            (byte)Command.Eviction => Command.Eviction,
+            _ => Command.Reserved
         };
     }
 
diff --git a/foreign/csharp/Iggy_SDK/Vsr/VsrOperation.cs b/foreign/csharp/Iggy_SDK/Vsr/VsrOperation.cs
index 5653a23..f46eeca 100644
--- a/foreign/csharp/Iggy_SDK/Vsr/VsrOperation.cs
+++ b/foreign/csharp/Iggy_SDK/Vsr/VsrOperation.cs
@@ -61,9 +61,7 @@
 
     SendMessages = 160,
     StoreConsumerOffset = 161,
-    DeleteConsumerOffset = 162,
-    StoreConsumerOffset2 = 164,
-    DeleteConsumerOffset2 = 165
+    DeleteConsumerOffset = 162
 }
 
 internal static class VsrOperations
@@ -128,8 +126,6 @@
             CommandCodes.SEND_MESSAGES_CODE => VsrOperation.SendMessages,
             CommandCodes.STORE_CONSUMER_OFFSET_CODE => VsrOperation.StoreConsumerOffset,
             CommandCodes.DELETE_CONSUMER_OFFSET_CODE => VsrOperation.DeleteConsumerOffset,
-            CommandCodes.STORE_CONSUMER_OFFSET_2_CODE => VsrOperation.StoreConsumerOffset2,
-            CommandCodes.DELETE_CONSUMER_OFFSET_2_CODE => VsrOperation.DeleteConsumerOffset2,
             CommandCodes.CREATE_STREAM_CODE => VsrOperation.CreateStream,
             CommandCodes.DELETE_STREAM_CODE => VsrOperation.DeleteStream,
             CommandCodes.UPDATE_STREAM_CODE => VsrOperation.UpdateStream,
@@ -170,8 +166,7 @@
         // overwrite, on a plane that keeps no client table to dedup against, so a replay lands on the same
         // value. Denying the retry here reports an unknown outcome for a blip on an offset commit, which
         // takes down the consume loop over a write that was safe to repeat.
-        if (operation is VsrOperation.StoreConsumerOffset or VsrOperation.StoreConsumerOffset2
-            or VsrOperation.DeleteConsumerOffset or VsrOperation.DeleteConsumerOffset2)
+        if (operation is VsrOperation.StoreConsumerOffset or VsrOperation.DeleteConsumerOffset)
         {
             return true;
         }
@@ -206,8 +201,8 @@
                 true,
             >= VsrOperation.CreateTopicWithAssignments and <= VsrOperation.TruncatePartition => true,
             >= VsrOperation.CreateStream and <= VsrOperation.LeaveConsumerGroup => true,
-            VsrOperation.SendMessages or VsrOperation.StoreConsumerOffset or VsrOperation.DeleteConsumerOffset
-                or VsrOperation.StoreConsumerOffset2 or VsrOperation.DeleteConsumerOffset2 => true,
+            VsrOperation.SendMessages or VsrOperation.StoreConsumerOffset or VsrOperation.DeleteConsumerOffset =>
+                true,
             _ => false
         };
     }
@@ -268,8 +263,6 @@
     internal static bool IsResultFramed(this VsrOperation operation)
     {
         return operation.IsMetadata() || operation is VsrOperation.StoreConsumerOffset
-            or VsrOperation.StoreConsumerOffset2
-            or VsrOperation.DeleteConsumerOffset
-            or VsrOperation.DeleteConsumerOffset2;
+            or VsrOperation.DeleteConsumerOffset;
     }
 }
diff --git a/foreign/csharp/Iggy_SDK/Vsr/VsrReplyDecoder.cs b/foreign/csharp/Iggy_SDK/Vsr/VsrReplyDecoder.cs
index efb2130..72a5e10 100644
--- a/foreign/csharp/Iggy_SDK/Vsr/VsrReplyDecoder.cs
+++ b/foreign/csharp/Iggy_SDK/Vsr/VsrReplyDecoder.cs
@@ -42,9 +42,9 @@
 
         switch (VsrHeader.PeekCommand(header))
         {
-            case Command2.Eviction:
+            case Command.Eviction:
                 throw ToException(VsrHeader.ReadEviction(header));
-            case Command2.Reply:
+            case Command.Reply:
                 break;
             default:
                 throw VsrError.Exception(VsrError.INVALID_COMMAND,
diff --git a/foreign/csharp/Iggy_SDK_Tests/ContractsTests/MessageBatchGoldenVectorTests.cs b/foreign/csharp/Iggy_SDK_Tests/ContractsTests/MessageBatchGoldenVectorTests.cs
new file mode 100644
index 0000000..da54ff9
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK_Tests/ContractsTests/MessageBatchGoldenVectorTests.cs
@@ -0,0 +1,182 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using System.Buffers.Binary;
+using Apache.Iggy.Contracts.Tcp;
+using Apache.Iggy.Extensions;
+using Apache.Iggy.IggyClient.Implementations;
+using Apache.Iggy.Kinds;
+using Apache.Iggy.Messages;
+using Apache.Iggy.Utils;
+
+namespace Apache.Iggy.Tests.ContractsTests;
+
+/// <summary>
+///     Cross-SDK golden vectors for the canonical message batch, produced by the Rust encoder
+///     (core/binary_protocol/src/requests/messages/send_messages.rs). These bytes are the
+///     contract: a change to the batch layout has to break every SDK's copy of them together.
+///
+///     The produce vector is the SendMessages body for stream 1, topic 2, balanced partitioning,
+///     and two messages: {id 7, origin timestamp 1000, payload "first-payload"} and
+///     {id 8, origin timestamp 1050, payload "second-payload", user headers "user-header-bytes"}.
+///     The poll vector serves that batch back stamped partition 3, base offset 100,
+///     base timestamp 5000, current offset 101.
+/// </summary>
+public sealed class MessageBatchGoldenVectorTests
+{
+    private const string ProduceBodyFull =
+        "12000000010401000000010402000000010002000000000000000000000000000000000000000000000000000000e803" +
+        "0000000000008c01000000000000a91f38c86307267c0200000000000000000000000000000000000000000000000000" +
+        "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+        "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+        "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+        "0000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a7596750700" +
+        "00000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c" +
+        "6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e0000000000000000" +
+        "0000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573";
+
+    private const string ProduceBatchOnly =
+        "000000000000000000000000000000000000000000000000e8030000000000008c01000000000000a91f38c86307267c" +
+        "020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+        "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+        "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+        "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+        "00000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000" +
+        "000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c00800000000000000000000" +
+        "00000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d" +
+        "6865616465722d6279746573";
+
+    private const string PollBody =
+        "03000000650000000000000002000000030000000000000064000000000000008813000000000000e803000000000000" +
+        "8c01000000000000c96826b38a8feed20200000000000000000000000000000000000000000000000000000000000000" +
+        "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+        "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+        "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +
+        "0000000000000000000000000000000000000000000000000000000000000000bfd2b9205a7596750700000000000000" +
+        "00000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e" +
+        "1c758eb7c0080000000000000000000000000000000100000032000000110000000e0000000000000000000000736563" +
+        "6f6e642d7061796c6f6164757365722d6865616465722d6279746573";
+
+    [Fact]
+    public void CreateMessage_EncodesTheProduceGoldenVector()
+    {
+        var expected = Convert.FromHexString(ProduceBodyFull);
+        var messages = GoldenMessages();
+        var streamId = Identifier.Numeric(1);
+        var topicId = Identifier.Numeric(2);
+        var partitioning = Partitioning.None();
+
+        var buffer = new byte[TcpMessageStreamHelpers.CalculateMessageBytesCount(messages, null)
+                              + 2 + streamId.Length + 2 + topicId.Length + 2 + partitioning.Length + 4 + 4];
+        var written = TcpContracts.CreateMessage(buffer, streamId, topicId, partitioning, messages);
+
+        Assert.Equal(expected.Length, written);
+        Assert.Equal(expected, buffer.AsSpan(0, written).ToArray());
+
+        var metadataLength = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(0, 4));
+        Assert.Equal(Convert.FromHexString(ProduceBatchOnly),
+            buffer.AsSpan(4 + metadataLength, written - 4 - metadataLength).ToArray());
+    }
+
+    [Fact]
+    public void CreateMessage_MintsAZeroMessageIdBeforeEncoding()
+    {
+        var message = new Message(UInt128.Zero, "payload"u8.ToArray());
+        var streamId = Identifier.Numeric(1);
+        var topicId = Identifier.Numeric(2);
+        var partitioning = Partitioning.None();
+
+        var buffer = new byte[TcpMessageStreamHelpers.CalculateMessageBytesCount([message], null)
+                              + 2 + streamId.Length + 2 + topicId.Length + 2 + partitioning.Length + 4 + 4];
+        var written = TcpContracts.CreateMessage(buffer, streamId, topicId, partitioning, [message]);
+
+        Assert.NotEqual(UInt128.Zero, message.Header.Id);
+        var metadataLength = BinaryPrimitives.ReadInt32LittleEndian(buffer.AsSpan(0, 4));
+        var frameStart = 4 + metadataLength + 256;
+        var wireId = BinaryPrimitives.ReadUInt128LittleEndian(buffer.AsSpan(frameStart + 8, 16));
+        Assert.Equal(message.Header.Id, wireId);
+        Assert.Equal(buffer.Length, written);
+    }
+
+    [Fact]
+    public void CreateMessage_EmptyBatch_Throws()
+    {
+        Assert.Throws<ArgumentException>(() => TcpContracts.CreateMessage(new byte[512], Identifier.Numeric(1),
+            Identifier.Numeric(2), Partitioning.None(), ReadOnlySpan<Message>.Empty));
+    }
+
+    [Fact]
+    public void CreateMessage_TimestampDeltaOverflow_Throws()
+    {
+        var messages = new[]
+        {
+            new Message(new UInt128(0, 1), "a"u8.ToArray()),
+            new Message(new UInt128(0, 2), "b"u8.ToArray())
+        };
+        messages[1].Header = messages[1].Header with { OriginTimestamp = (ulong)uint.MaxValue + 1 };
+
+        Assert.Throws<ArgumentException>(() => TcpContracts.CreateMessage(new byte[1024], Identifier.Numeric(1),
+            Identifier.Numeric(2), Partitioning.None(), messages));
+    }
+
+    [Fact]
+    public void MapRentedMessages_DecodesThePollGoldenVector()
+    {
+        var pollBody = Convert.FromHexString(PollBody);
+
+        using var rental =
+            Mappers.BinaryMapper.MapRentedMessages(pollBody, TcpMessageStream.EmptyMemoryOwner.Instance);
+
+        Assert.Equal(3, rental.PartitionId);
+        Assert.Equal(101ul, rental.CurrentOffset);
+        Assert.Equal(2, rental.Messages.Count);
+
+        var first = rental.Messages[0];
+        Assert.Equal(100ul, first.Header.Offset);
+        Assert.Equal(DateTimeOffsetUtils.FromUnixTimeMicroSeconds(5000), first.Header.Timestamp);
+        Assert.Equal(1000ul, first.Header.OriginTimestamp);
+        Assert.Equal(new UInt128(0, 7), first.Header.Id);
+        Assert.Equal("first-payload"u8.ToArray(), first.Payload.ToArray());
+        Assert.True(first.RawUserHeaders.IsEmpty);
+        Assert.Equal(BinaryPrimitives.ReadUInt64LittleEndian(pollBody.AsSpan(16 + 256, 8)), first.Header.Checksum);
+
+        var second = rental.Messages[1];
+        Assert.Equal(101ul, second.Header.Offset);
+        Assert.Equal(DateTimeOffsetUtils.FromUnixTimeMicroSeconds(5000), second.Header.Timestamp);
+        Assert.Equal(1050ul, second.Header.OriginTimestamp);
+        Assert.Equal(new UInt128(0, 8), second.Header.Id);
+        Assert.Equal("second-payload"u8.ToArray(), second.Payload.ToArray());
+        Assert.Equal("user-header-bytes"u8.ToArray(), second.RawUserHeaders.ToArray());
+        Assert.Equal(
+            BinaryPrimitives.ReadUInt64LittleEndian(pollBody.AsSpan(16 + 256 + 48 + "first-payload"u8.Length, 8)),
+            second.Header.Checksum);
+    }
+
+    private static Message[] GoldenMessages()
+    {
+        var messages = new[]
+        {
+            new Message(new UInt128(0, 7), "first-payload"u8.ToArray()),
+            new Message(new UInt128(0, 8), "second-payload"u8.ToArray())
+        };
+        messages[0].Header = messages[0].Header with { OriginTimestamp = 1000 };
+        messages[1].Header = messages[1].Header with { OriginTimestamp = 1050 };
+        messages[1].RawUserHeaders = "user-header-bytes"u8.ToArray();
+
+        return messages;
+    }
+}
diff --git a/foreign/csharp/Iggy_SDK_Tests/MapperTests/BinaryMapper.cs b/foreign/csharp/Iggy_SDK_Tests/MapperTests/BinaryMapper.cs
index 0324871..d86b018 100644
--- a/foreign/csharp/Iggy_SDK_Tests/MapperTests/BinaryMapper.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/MapperTests/BinaryMapper.cs
@@ -79,17 +79,15 @@
     public void MapMessages_NoHeaders_ReturnsValidMessageResponses()
     {
         // Arrange
-        var (offset, timestamp, guid, headersLength, checkSum, payload) = MessageFactory.CreateMessageResponseFields();
-        var msgOnePayload = BinaryFactory.CreateMessagePayload(offset, timestamp, 0, checkSum,
-            guid, payload);
-        var (offset1, timestamp1, guid1, headersLength2, checkSum2, payload1)
-            = MessageFactory.CreateMessageResponseFields();
-        var msgTwoPayload = BinaryFactory.CreateMessagePayload(offset1, timestamp1, 0, checkSum2,
-            guid1, payload1);
+        var (offset, timestamp, guid, _, checkSum, payload) = MessageFactory.CreateMessageResponseFields();
+        var msgOneFrame = BinaryFactory.CreateMessageFrame(checkSum, guid, 0, 0, [], payload);
+        var (_, _, guid1, _, checkSum2, payload1) = MessageFactory.CreateMessageResponseFields();
+        var msgTwoFrame = BinaryFactory.CreateMessageFrame(checkSum2, guid1, 1, 5, [], payload1);
+        var record = BinaryFactory.CreateBatchRecord(offset, timestamp, timestamp, msgOneFrame, msgTwoFrame);
 
-        var combinedPayload = new byte[16 + msgOnePayload.Length + msgTwoPayload.Length];
-        msgOnePayload.CopyTo(combinedPayload.AsSpan(16));
-        msgTwoPayload.CopyTo(combinedPayload.AsSpan(16 + msgOnePayload.Length));
+        var combinedPayload = new byte[16 + record.Length];
+        BinaryPrimitives.WriteUInt32LittleEndian(combinedPayload.AsSpan(12, 4), 2);
+        record.CopyTo(combinedPayload.AsSpan(16));
 
         // Act
         var responses
@@ -101,9 +99,13 @@
 
         var response1 = responses.Messages.ElementAt(0);
         Assert.Equal(payload, response1.Payload);
+        Assert.Equal(offset, response1.Header.Offset);
+        Assert.Equal(timestamp, response1.Header.OriginTimestamp);
 
         var response2 = responses.Messages.ElementAt(1);
         Assert.Equal(payload1, response2.Payload);
+        Assert.Equal(offset + 1, response2.Header.Offset);
+        Assert.Equal(timestamp + 5, response2.Header.OriginTimestamp);
     }
 
     [Fact]
@@ -435,15 +437,15 @@
         var headers1 = "first-secret-headers"u8.ToArray();
         var payload2 = "second-secret-payload"u8.ToArray();
 
-        var frame1 = BuildEncryptedFrame(encryptor, 100, payload1, headers1);
-        var frame2 = BuildEncryptedFrame(encryptor, 101, payload2, ReadOnlySpan<byte>.Empty);
+        var frame1 = BuildEncryptedFrame(encryptor, 0, payload1, headers1);
+        var frame2 = BuildEncryptedFrame(encryptor, 1, payload2, ReadOnlySpan<byte>.Empty);
+        var record = BinaryFactory.CreateBatchRecord(100, 12345, 12345, frame1, frame2);
 
-        var combined = new byte[16 + frame1.Length + frame2.Length];
+        var combined = new byte[16 + record.Length];
         BinaryPrimitives.WriteInt32LittleEndian(combined.AsSpan(0, 4), 7);
         BinaryPrimitives.WriteUInt64LittleEndian(combined.AsSpan(4, 8), 101);
         BinaryPrimitives.WriteUInt32LittleEndian(combined.AsSpan(12, 4), 2);
-        frame1.CopyTo(combined.AsSpan(16));
-        frame2.CopyTo(combined.AsSpan(16 + frame1.Length));
+        record.CopyTo(combined.AsSpan(16));
 
         using var rental = Mappers.BinaryMapper.MapRentedMessages(combined, TcpMessageStream.EmptyMemoryOwner.Instance,
             encryptor);
@@ -475,33 +477,50 @@
     {
         var encryptor = new AesMessageEncryptor(AesMessageEncryptor.GenerateKey());
 
-        var frame = new byte[64];
-        BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(48, 4), 0); // headersLength
-        BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(52, 4), -64); // payloadLength
+        var frame = new byte[48];
+        BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(32, 4), 0); // headersLength
+        BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(36, 4), -48); // payloadLength
+        var record = BinaryFactory.CreateBatchRecord(1, 12345, 12345, frame);
 
-        var combined = new byte[16 + frame.Length];
+        var combined = new byte[16 + record.Length];
         BinaryPrimitives.WriteInt32LittleEndian(combined.AsSpan(0, 4), 7);
         BinaryPrimitives.WriteUInt64LittleEndian(combined.AsSpan(4, 8), 1);
         BinaryPrimitives.WriteUInt32LittleEndian(combined.AsSpan(12, 4), 1);
-        frame.CopyTo(combined.AsSpan(16));
+        record.CopyTo(combined.AsSpan(16));
 
         Assert.Throws<MalformedResponseException>(() =>
             Mappers.BinaryMapper.MapRentedMessages(combined, TcpMessageStream.EmptyMemoryOwner.Instance, encryptor));
     }
 
     [Fact]
+    public void MapRentedMessages_NonzeroFrameReserved_Throws()
+    {
+        var frame = BinaryFactory.CreateMessageFrame(0, Guid.NewGuid(), 0, 0, [], "payload"u8);
+        BinaryPrimitives.WriteUInt64LittleEndian(frame.AsSpan(40, 8), 1);
+        var record = BinaryFactory.CreateBatchRecord(1, 12345, 12345, frame);
+
+        var combined = new byte[16 + record.Length];
+        BinaryPrimitives.WriteUInt32LittleEndian(combined.AsSpan(12, 4), 1);
+        record.CopyTo(combined.AsSpan(16));
+
+        Assert.Throws<MalformedResponseException>(() =>
+            Mappers.BinaryMapper.MapRentedMessages(combined, TcpMessageStream.EmptyMemoryOwner.Instance));
+    }
+
+    [Fact]
     public void MapRentedMessages_WithEncryptor_TamperedCiphertext_ThrowsMessageDecryptionException()
     {
         var encryptor = new AesMessageEncryptor(AesMessageEncryptor.GenerateKey());
-        var frame = BuildEncryptedFrame(encryptor, 42, "secret-payload"u8, ReadOnlySpan<byte>.Empty);
+        var frame = BuildEncryptedFrame(encryptor, 0, "secret-payload"u8, ReadOnlySpan<byte>.Empty);
 
-        frame[64 + 12] ^= 0xFF;
+        frame[48 + 12] ^= 0xFF;
+        var record = BinaryFactory.CreateBatchRecord(42, 12345, 12345, frame);
 
-        var combined = new byte[16 + frame.Length];
+        var combined = new byte[16 + record.Length];
         BinaryPrimitives.WriteInt32LittleEndian(combined.AsSpan(0, 4), 7);
         BinaryPrimitives.WriteUInt64LittleEndian(combined.AsSpan(4, 8), 42);
         BinaryPrimitives.WriteUInt32LittleEndian(combined.AsSpan(12, 4), 1);
-        frame.CopyTo(combined.AsSpan(16));
+        record.CopyTo(combined.AsSpan(16));
 
         var ex = Assert.Throws<MessageDecryptionException>(() =>
             Mappers.BinaryMapper.MapRentedMessages(combined, TcpMessageStream.EmptyMemoryOwner.Instance, encryptor));
@@ -587,25 +606,12 @@
         Assert.Throws<InvalidResponseException>(() => Mappers.BinaryMapper.MapSendMessages(payload));
     }
 
-    private static byte[] BuildEncryptedFrame(AesMessageEncryptor encryptor, ulong offset,
+    private static byte[] BuildEncryptedFrame(AesMessageEncryptor encryptor, uint offsetDelta,
         ReadOnlySpan<byte> plainPayload, ReadOnlySpan<byte> plainHeaders)
     {
         var cipherPayload = encryptor.EncryptToArray(plainPayload);
         var cipherHeaders = plainHeaders.Length > 0 ? encryptor.EncryptToArray(plainHeaders) : [];
 
-        var frame = new byte[64 + cipherPayload.Length + cipherHeaders.Length];
-        Span<byte> span = frame.AsSpan();
-        BinaryPrimitives.WriteUInt64LittleEndian(span[..8], 0);
-        BinaryPrimitives.WriteUInt128LittleEndian(span[8..24], Guid.NewGuid().ToUInt128());
-        BinaryPrimitives.WriteUInt64LittleEndian(span[24..32], offset);
-        BinaryPrimitives.WriteUInt64LittleEndian(span[32..40], 12345);
-        BinaryPrimitives.WriteUInt64LittleEndian(span[40..48], 12345);
-        BinaryPrimitives.WriteInt32LittleEndian(span[48..52], cipherHeaders.Length);
-        BinaryPrimitives.WriteInt32LittleEndian(span[52..56], cipherPayload.Length);
-        BinaryPrimitives.WriteUInt64LittleEndian(span[56..64], 0);
-        cipherPayload.CopyTo(span[64..]);
-        cipherHeaders.CopyTo(span[(64 + cipherPayload.Length)..]);
-
-        return frame;
+        return BinaryFactory.CreateMessageFrame(0, Guid.NewGuid(), offsetDelta, 0, cipherHeaders, cipherPayload);
     }
 }
diff --git a/foreign/csharp/Iggy_SDK_Tests/PublisherTests/MessageEncryptionTests.cs b/foreign/csharp/Iggy_SDK_Tests/PublisherTests/MessageEncryptionTests.cs
index 7e64a2b..2302817 100644
--- a/foreign/csharp/Iggy_SDK_Tests/PublisherTests/MessageEncryptionTests.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/PublisherTests/MessageEncryptionTests.cs
@@ -151,21 +151,21 @@
 
         Span<byte> span = buffer.AsSpan(0, written);
         var metadataLength = BinaryPrimitives.ReadInt32LittleEndian(span[..4]);
-        var position = 4 + metadataLength + 16 * messages.Length;
+        var position = 4 + metadataLength + 256;
 
-        var firstPayloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 52)..(position + 56)]);
+        var firstPayloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 36)..(position + 40)]);
         Assert.Equal(encryptor.GetMaxEncryptedLength("first-payload"u8.Length), firstPayloadLength);
         Assert.Equal("first-payload"u8.ToArray(),
-            encryptor.DecryptToArray(span.Slice(position + 64, firstPayloadLength)));
+            encryptor.DecryptToArray(span.Slice(position + 48, firstPayloadLength)));
 
-        position += 64 + firstPayloadLength;
-        var secondHeadersLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 48)..(position + 52)]);
-        var secondPayloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 52)..(position + 56)]);
+        position += 48 + firstPayloadLength;
+        var secondHeadersLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 32)..(position + 36)]);
+        var secondPayloadLength = BinaryPrimitives.ReadInt32LittleEndian(span[(position + 36)..(position + 40)]);
         Assert.Equal("second-payload"u8.ToArray(),
-            encryptor.DecryptToArray(span.Slice(position + 64, secondPayloadLength)));
+            encryptor.DecryptToArray(span.Slice(position + 48, secondPayloadLength)));
 
         var plainHeaders = encryptor.DecryptToArray(
-            span.Slice(position + 64 + secondPayloadLength, secondHeadersLength));
+            span.Slice(position + 48 + secondPayloadLength, secondHeadersLength));
         Assert.Single(BinaryMapper.MapHeaders(plainHeaders));
     }
 }
diff --git a/foreign/csharp/Iggy_SDK_Tests/Utils/BinaryFactory.cs b/foreign/csharp/Iggy_SDK_Tests/Utils/BinaryFactory.cs
index 5204326..d0c6078 100644
--- a/foreign/csharp/Iggy_SDK_Tests/Utils/BinaryFactory.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/Utils/BinaryFactory.cs
@@ -42,25 +42,50 @@
         return payload;
     }
 
-    internal static byte[] CreateMessagePayload(ulong offset, ulong timestamp, int headersLength, uint checkSum,
-        Guid guid, ReadOnlySpan<byte> payload)
+    internal static byte[] CreateMessageFrame(ulong checksum, Guid guid, uint offsetDelta, uint timestampDelta,
+        ReadOnlySpan<byte> userHeaders, ReadOnlySpan<byte> payload)
     {
-        var messageLength = payload.Length;
-        var totalSize = 64 + payload.Length;
-        Span<byte> payloadBuffer = new byte[totalSize].AsSpan();
+        Span<byte> frame = new byte[48 + payload.Length + userHeaders.Length].AsSpan();
 
-        BinaryPrimitives.WriteUInt64LittleEndian(payloadBuffer[..8], checkSum);
-        BinaryPrimitives.WriteUInt128LittleEndian(payloadBuffer[8..24], guid.ToUInt128());
-        BinaryPrimitives.WriteUInt64LittleEndian(payloadBuffer[24..32], offset);
-        BinaryPrimitives.WriteUInt64LittleEndian(payloadBuffer[32..+40], timestamp);
-        BinaryPrimitives.WriteUInt64LittleEndian(payloadBuffer[40..48], timestamp);
-        BinaryPrimitives.WriteInt32LittleEndian(payloadBuffer[48..52], headersLength);
-        BinaryPrimitives.WriteInt32LittleEndian(payloadBuffer[52..56], payload.Length);
-        BinaryPrimitives.WriteUInt64LittleEndian(payloadBuffer[56..64], 0); // reserved
+        BinaryPrimitives.WriteUInt64LittleEndian(frame[..8], checksum);
+        BinaryPrimitives.WriteUInt128LittleEndian(frame[8..24], guid.ToUInt128());
+        BinaryPrimitives.WriteUInt32LittleEndian(frame[24..28], offsetDelta);
+        BinaryPrimitives.WriteUInt32LittleEndian(frame[28..32], timestampDelta);
+        BinaryPrimitives.WriteUInt32LittleEndian(frame[32..36], (uint)userHeaders.Length);
+        BinaryPrimitives.WriteUInt32LittleEndian(frame[36..40], (uint)payload.Length);
+        BinaryPrimitives.WriteUInt64LittleEndian(frame[40..48], 0); // reserved
 
-        payload.CopyTo(payloadBuffer[64..(64 + messageLength)]);
+        payload.CopyTo(frame[48..(48 + payload.Length)]);
+        userHeaders.CopyTo(frame[(48 + payload.Length)..]);
 
-        return payloadBuffer.ToArray();
+        return frame.ToArray();
+    }
+
+    /// <summary>
+    ///     One batch record: a 256-byte batch header followed by the given frames. The batch checksum is
+    ///     left zero; the poll decoder does not verify it.
+    /// </summary>
+    internal static byte[] CreateBatchRecord(ulong baseOffset, ulong baseTimestamp, ulong originTimestamp,
+        params byte[][] frames)
+    {
+        var blobLength = frames.Sum(frame => frame.Length);
+        var record = new byte[256 + blobLength];
+        Span<byte> header = record.AsSpan(0, 256);
+
+        BinaryPrimitives.WriteUInt64LittleEndian(header[8..16], baseOffset);
+        BinaryPrimitives.WriteUInt64LittleEndian(header[16..24], baseTimestamp);
+        BinaryPrimitives.WriteUInt64LittleEndian(header[24..32], originTimestamp);
+        BinaryPrimitives.WriteUInt64LittleEndian(header[32..40], (ulong)record.Length);
+        BinaryPrimitives.WriteUInt32LittleEndian(header[48..52], (uint)frames.Length);
+
+        var position = 256;
+        foreach (var frame in frames)
+        {
+            frame.CopyTo(record.AsSpan(position));
+            position += frame.Length;
+        }
+
+        return record;
     }
 
     internal static byte[] CreateStreamPayload(uint id, int topicsCount, string name, ulong sizeBytes,
diff --git a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrHeaderTests.cs b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrHeaderTests.cs
index 73eb1fb..2780370 100644
--- a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrHeaderTests.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrHeaderTests.cs
@@ -81,7 +81,7 @@
 
         Assert.Equal(VsrHeader.HEADER_SIZE + payload.Length, totalSize);
         Assert.Equal((uint)totalSize, ReadUInt32(header, VsrHeader.SIZE_OFFSET));
-        Assert.Equal((byte)Command2.Request, header[VsrHeader.COMMAND_OFFSET]);
+        Assert.Equal((byte)Command.Request, header[VsrHeader.COMMAND_OFFSET]);
         Assert.Equal((byte)VsrOperation.Register, header[VsrHeader.REQUEST_OPERATION_OFFSET]);
         Assert.Equal(0UL, ReadUInt64(header, VsrHeader.REQUEST_ID_OFFSET));
         Assert.Equal(0UL, ReadUInt64(header, VsrHeader.REQUEST_SESSION_OFFSET));
@@ -213,14 +213,14 @@
     {
         var header = new byte[VsrHeader.HEADER_SIZE];
 
-        header[VsrHeader.COMMAND_OFFSET] = (byte)Command2.Reply;
-        Assert.Equal(Command2.Reply, VsrHeader.PeekCommand(header));
+        header[VsrHeader.COMMAND_OFFSET] = (byte)Command.Reply;
+        Assert.Equal(Command.Reply, VsrHeader.PeekCommand(header));
 
-        header[VsrHeader.COMMAND_OFFSET] = (byte)Command2.Eviction;
-        Assert.Equal(Command2.Eviction, VsrHeader.PeekCommand(header));
+        header[VsrHeader.COMMAND_OFFSET] = (byte)Command.Eviction;
+        Assert.Equal(Command.Eviction, VsrHeader.PeekCommand(header));
 
         header[VsrHeader.COMMAND_OFFSET] = 6;
-        Assert.Equal(Command2.Reserved, VsrHeader.PeekCommand(header));
+        Assert.Equal(Command.Reserved, VsrHeader.PeekCommand(header));
     }
 
     [Fact]
diff --git a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrOperationTests.cs b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrOperationTests.cs
index bd5686e..6888db3 100644
--- a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrOperationTests.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrOperationTests.cs
@@ -35,8 +35,6 @@
     [InlineData(CommandCodes.SEND_MESSAGES_CODE, (byte)VsrOperation.SendMessages)]
     [InlineData(CommandCodes.STORE_CONSUMER_OFFSET_CODE, (byte)VsrOperation.StoreConsumerOffset)]
     [InlineData(CommandCodes.DELETE_CONSUMER_OFFSET_CODE, (byte)VsrOperation.DeleteConsumerOffset)]
-    [InlineData(CommandCodes.STORE_CONSUMER_OFFSET_2_CODE, (byte)VsrOperation.StoreConsumerOffset2)]
-    [InlineData(CommandCodes.DELETE_CONSUMER_OFFSET_2_CODE, (byte)VsrOperation.DeleteConsumerOffset2)]
     [InlineData(CommandCodes.CREATE_STREAM_CODE, (byte)VsrOperation.CreateStream)]
     [InlineData(CommandCodes.DELETE_STREAM_CODE, (byte)VsrOperation.DeleteStream)]
     [InlineData(CommandCodes.UPDATE_STREAM_CODE, (byte)VsrOperation.UpdateStream)]
@@ -110,7 +108,7 @@
     {
         Assert.True(VsrOperation.CreateStream.IsResultFramed());
         Assert.True(VsrOperation.StoreConsumerOffset.IsResultFramed());
-        Assert.True(VsrOperation.DeleteConsumerOffset2.IsResultFramed());
+        Assert.True(VsrOperation.DeleteConsumerOffset.IsResultFramed());
         Assert.False(VsrOperation.SendMessages.IsResultFramed());
         Assert.False(VsrOperation.NonReplicated.IsResultFramed());
         Assert.False(VsrOperation.Register.IsResultFramed());
@@ -122,6 +120,8 @@
     {
         Assert.True(VsrOperations.IsKnown((byte)VsrOperation.SendMessages));
         Assert.False(VsrOperations.IsKnown(163));
+        Assert.False(VsrOperations.IsKnown(164));
+        Assert.False(VsrOperations.IsKnown(165));
         Assert.False(VsrOperations.IsKnown(200));
     }
 
@@ -156,7 +156,7 @@
     [Theory]
     [InlineData((byte)VsrOperation.SendMessages)]
     [InlineData((byte)VsrOperation.StoreConsumerOffset)]
-    [InlineData((byte)VsrOperation.DeleteConsumerOffset2)]
+    [InlineData((byte)VsrOperation.DeleteConsumerOffset)]
     [InlineData((byte)VsrOperation.DeleteSegments)]
     [InlineData((byte)VsrOperation.NonReplicated)]
     [InlineData((byte)VsrOperation.Logout)]
diff --git a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrProtocolDriftTests.cs b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrProtocolDriftTests.cs
index afc9a6b..2fa5bcc 100644
--- a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrProtocolDriftTests.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrProtocolDriftTests.cs
@@ -128,11 +128,11 @@
     }
 
     [Fact]
-    public void Command2Discriminants_MatchTheRustEnum()
+    public void CommandDiscriminants_MatchTheRustEnum()
     {
-        IReadOnlyDictionary<string, int> rust = RustEnum.Discriminants(ReadRustSource(CommandPath), "Command2");
+        IReadOnlyDictionary<string, int> rust = RustEnum.Discriminants(ReadRustSource(CommandPath), "Command");
 
-        AssertSubsetMatches<Command2>(rust);
+        AssertSubsetMatches<Command>(rust);
     }
 
     [Fact]
@@ -723,7 +723,7 @@
         ["u64"] = (8, 8),
         ["u128"] = (16, 16),
         // Every enum the headers embed is `#[repr(u8)]`.
-        ["Command2"] = (1, 1),
+        ["Command"] = (1, 1),
         ["Operation"] = (1, 1),
         ["EvictionReason"] = (1, 1)
     };
diff --git a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrReplyDecoderTests.cs b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrReplyDecoderTests.cs
index a6707b8..c778817 100644
--- a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrReplyDecoderTests.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrReplyDecoderTests.cs
@@ -26,7 +26,7 @@
     private static byte[] ReplyHeader(VsrOperation operation, int bodyLength, uint status = 0)
     {
         var header = new byte[VsrHeader.HEADER_SIZE];
-        header[VsrHeader.COMMAND_OFFSET] = (byte)Command2.Reply;
+        header[VsrHeader.COMMAND_OFFSET] = (byte)Command.Reply;
         header[VsrHeader.REPLY_OPERATION_OFFSET] = (byte)operation;
         BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(VsrHeader.SIZE_OFFSET),
             (uint)(VsrHeader.HEADER_SIZE + bodyLength));
@@ -38,7 +38,7 @@
     private static byte[] EvictionHeader(EvictionReason reason, uint version = 0, uint versionMin = 0)
     {
         var header = new byte[VsrHeader.HEADER_SIZE];
-        header[VsrHeader.COMMAND_OFFSET] = (byte)Command2.Eviction;
+        header[VsrHeader.COMMAND_OFFSET] = (byte)Command.Eviction;
         header[VsrHeader.EVICTION_REASON_OFFSET] = (byte)reason;
         BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(VsrHeader.EVICTION_PROTOCOL_VERSION_OFFSET), version);
         BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(VsrHeader.EVICTION_PROTOCOL_VERSION_MIN_OFFSET),
diff --git a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrTestPayloads.cs b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrTestPayloads.cs
index 1e49147..c7c18d9 100644
--- a/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrTestPayloads.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/VsrTests/VsrTestPayloads.cs
@@ -71,7 +71,9 @@
             BinaryPrimitives.WriteUInt32LittleEndian(partition.AsSpan(1), partitionId.Value);
         }
 
-        return Concat([1], NumericIdentifier(1), streamId, topicId, partition);
+        byte[] ack = [1];
+
+        return Concat([1], NumericIdentifier(1), streamId, topicId, partition, ack);
     }
 
     internal static byte[] DeleteSegments(byte[] streamId, byte[] topicId, uint partitionId, uint segmentsCount = 1)
diff --git a/foreign/go/binary_serialization/binary_response_deserializer.go b/foreign/go/binary_serialization/binary_response_deserializer.go
index ef6803f..04abe22 100644
--- a/foreign/go/binary_serialization/binary_response_deserializer.go
+++ b/foreign/go/binary_serialization/binary_response_deserializer.go
@@ -26,6 +26,7 @@
 
 	iggcon "github.com/apache/iggy/foreign/go/contracts"
 	ierror "github.com/apache/iggy/foreign/go/errors"
+	"github.com/apache/iggy/foreign/go/internal/batch"
 	"github.com/klauspost/compress/s2"
 )
 
@@ -161,13 +162,16 @@
 	return options, consumed, nil
 }
 
-// pollBatchHeaderLength covers [partition_id u32][current_offset u64][count u32].
-const pollBatchHeaderLength = 16
+// pollPrefixLength covers [partition_id u32][current_offset u64][count u32].
+const pollPrefixLength = 16
 
-// DeserializeFetchMessagesResponse decodes a poll reply. A truncated body is
-// a decode error rather than a shorter batch: silently dropping the tail
-// would let a consumer that commits CurrentOffset skip messages it never saw.
-// The returned messages alias the reply buffer; a retained message pins it.
+// DeserializeFetchMessagesResponse decodes a poll reply: the 16-byte prefix
+// followed by batch records ([256-byte batch header][frames]) walked by their
+// batch length, with each frame's deltas resolved to absolute values. A
+// truncated body is a decode error rather than a shorter batch: silently
+// dropping the tail would let a consumer that commits CurrentOffset skip
+// messages it never saw. The returned messages alias the reply buffer; a
+// retained message pins it.
 func DeserializeFetchMessagesResponse(payload []byte, compression iggcon.IggyMessageCompression) (*iggcon.PolledMessage, error) {
 	if len(payload) == 0 {
 		return &iggcon.PolledMessage{
@@ -178,63 +182,80 @@
 	}
 
 	length := len(payload)
-	if length < pollBatchHeaderLength {
-		return nil, fmt.Errorf("poll response: %d bytes is short of the batch header", length)
+	if length < pollPrefixLength {
+		return nil, fmt.Errorf("poll response: %d bytes is short of the reply prefix", length)
 	}
 	partitionId := binary.LittleEndian.Uint32(payload[0:4])
 	currentOffset := binary.LittleEndian.Uint64(payload[4:12])
 	messagesCount := binary.LittleEndian.Uint32(payload[12:16])
-	position := pollBatchHeaderLength
+	position := pollPrefixLength
 
 	// The declared count is server-controlled; the allocation hint is capped
 	// by what the body could possibly hold.
-	maxMessages := (length - pollBatchHeaderLength) / iggcon.MessageHeaderSize
+	maxMessages := (length - pollPrefixLength) / batch.MessageHeaderSize
 	if int(messagesCount) < maxMessages {
 		maxMessages = int(messagesCount)
 	}
 	messages := make([]iggcon.IggyMessage, 0, maxMessages)
 	for position < length {
-		if position+iggcon.MessageHeaderSize > length {
-			return nil, fmt.Errorf("poll response: truncated message header at byte %d", position)
-		}
-		header, err := iggcon.MessageHeaderFromBytes(payload[position : position+iggcon.MessageHeaderSize])
+		record, err := batch.DecodeHeader(payload[position:])
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("poll response: %w", err)
 		}
-		position += iggcon.MessageHeaderSize
-		if uint64(header.PayloadLength) > uint64(length-position) {
+		if record.BatchLength > uint64(length-position) {
 			return nil, fmt.Errorf(
-				"poll response: message payload of %d bytes overruns the body", header.PayloadLength)
+				"poll response: batch record of %d bytes overruns the body", record.BatchLength)
 		}
-		payloadSlice := payload[position : position+int(header.PayloadLength)]
-		position += int(header.PayloadLength)
-
-		if uint64(header.UserHeaderLength) > uint64(length-position) {
-			return nil, fmt.Errorf(
-				"poll response: user headers of %d bytes overrun the body", header.UserHeaderLength)
-		}
-		var userHeaders []byte
-		if header.UserHeaderLength > 0 {
-			userHeaders = payload[position : position+int(header.UserHeaderLength)]
-		}
-		position += int(header.UserHeaderLength)
-
-		switch compression {
-		case iggcon.MESSAGE_COMPRESSION_S2, iggcon.MESSAGE_COMPRESSION_S2_BETTER, iggcon.MESSAGE_COMPRESSION_S2_BEST:
-			if length < 32 {
-				break
-			}
-			payloadSlice, err = s2.Decode(nil, payloadSlice)
+		recordEnd := position + int(record.BatchLength)
+		cursor := position + batch.HeaderSize
+		for cursor < recordEnd {
+			frame, err := batch.DecodeMessageHeader(payload[cursor:recordEnd])
 			if err != nil {
-				return nil, fmt.Errorf("failed to decode s2 payload: %w", err)
+				return nil, fmt.Errorf("poll response: %w", err)
 			}
-		}
+			payloadStart := cursor + batch.MessageHeaderSize
+			payloadEnd := payloadStart + int(frame.PayloadLength)
+			userHeadersEnd := payloadEnd + int(frame.UserHeadersLength)
+			if userHeadersEnd > recordEnd {
+				return nil, fmt.Errorf(
+					"poll response: message of %d payload and %d user-header bytes overruns the batch record",
+					frame.PayloadLength, frame.UserHeadersLength)
+			}
+			payloadSlice := payload[payloadStart:payloadEnd]
+			var userHeaders []byte
+			if frame.UserHeadersLength > 0 {
+				userHeaders = payload[payloadEnd:userHeadersEnd]
+			}
+			cursor = userHeadersEnd
 
-		messages = append(messages, iggcon.IggyMessage{
-			Header:      *header,
-			Payload:     payloadSlice,
-			UserHeaders: userHeaders,
-		})
+			switch compression {
+			case iggcon.MESSAGE_COMPRESSION_S2, iggcon.MESSAGE_COMPRESSION_S2_BETTER, iggcon.MESSAGE_COMPRESSION_S2_BEST:
+				payloadSlice, err = s2.Decode(nil, payloadSlice)
+				if err != nil {
+					return nil, fmt.Errorf("failed to decode s2 payload: %w", err)
+				}
+			}
+
+			messages = append(messages, iggcon.IggyMessage{
+				Header: iggcon.MessageHeader{
+					Checksum: frame.Checksum,
+					Id:       iggcon.MessageID(frame.Id),
+					// A record may be a server-sliced view of a larger stored
+					// batch: BaseOffset stays put and the first frame's delta
+					// positions it, so the sum is the absolute offset either way.
+					Offset: record.BaseOffset + uint64(frame.OffsetDelta),
+					// Broker append time is stamped once per batch; the
+					// per-message delta applies to OriginTimestamp only.
+					Timestamp:        record.BaseTimestamp,
+					OriginTimestamp:  record.OriginTimestamp + uint64(frame.TimestampDelta),
+					UserHeaderLength: frame.UserHeadersLength,
+					PayloadLength:    frame.PayloadLength,
+				},
+				Payload:     payloadSlice,
+				UserHeaders: userHeaders,
+			})
+		}
+		position = recordEnd
 	}
 	if uint32(len(messages)) != messagesCount {
 		return nil, fmt.Errorf(
@@ -242,7 +263,6 @@
 			len(messages), messagesCount)
 	}
 
-	// !TODO: Add message offset ordering
 	return &iggcon.PolledMessage{
 		PartitionId:   partitionId,
 		CurrentOffset: currentOffset,
diff --git a/foreign/go/binary_serialization/binary_response_deserializer_test.go b/foreign/go/binary_serialization/binary_response_deserializer_test.go
index fe41b6b..410c8ff 100644
--- a/foreign/go/binary_serialization/binary_response_deserializer_test.go
+++ b/foreign/go/binary_serialization/binary_response_deserializer_test.go
@@ -27,17 +27,10 @@
 )
 
 func buildFetchPayload(payloadBody []byte) []byte {
-	header := iggcon.NewMessageHeader(iggcon.MessageID{}, uint32(len(payloadBody)), 0)
-	headerBytes := header.ToBytes()
-
-	// 4 (partitionId) + 8 (currentOffset) + 4 (messagesCount) + header + body
-	buf := make([]byte, 16+len(headerBytes)+len(payloadBody))
-	binary.LittleEndian.PutUint32(buf[0:4], 1)
-	binary.LittleEndian.PutUint64(buf[4:12], 0)
-	binary.LittleEndian.PutUint32(buf[12:16], 1)
-	copy(buf[16:], headerBytes)
-	copy(buf[16+len(headerBytes):], payloadBody)
-	return buf
+	buf := binary.LittleEndian.AppendUint32(nil, 1)
+	buf = binary.LittleEndian.AppendUint64(buf, 0)
+	buf = binary.LittleEndian.AppendUint32(buf, 1)
+	return appendBatchRecord(buf, 0, 0, 0, batchFrame{payload: payloadBody})
 }
 
 func TestDeserializeFetchMessages_MalformedS2ReturnsError(t *testing.T) {
diff --git a/foreign/go/binary_serialization/vsr_response_deserializer_test.go b/foreign/go/binary_serialization/vsr_response_deserializer_test.go
index 2ef115c..49b8f39 100644
--- a/foreign/go/binary_serialization/vsr_response_deserializer_test.go
+++ b/foreign/go/binary_serialization/vsr_response_deserializer_test.go
@@ -19,10 +19,12 @@
 
 import (
 	"encoding/binary"
+	"encoding/hex"
 	"testing"
 
 	iggcon "github.com/apache/iggy/foreign/go/contracts"
 	ierror "github.com/apache/iggy/foreign/go/errors"
+	"github.com/apache/iggy/foreign/go/internal/batch"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -167,22 +169,53 @@
 	assert.Error(t, err)
 }
 
-// pollPayload builds a poll reply body carrying the given messages.
+// batchFrame is one message frame of a fabricated batch record. Frame
+// checksums stay zero: a poll decode passes them through unverified.
+type batchFrame struct {
+	offsetDelta    uint32
+	timestampDelta uint32
+	payload        []byte
+	userHeaders    []byte
+}
+
+// appendBatchRecord appends one batch record carrying the given frames.
+func appendBatchRecord(body []byte, baseOffset, baseTimestamp, originTimestamp uint64, frames ...batchFrame) []byte {
+	recordStart := len(body)
+	body = append(body, make([]byte, batch.HeaderSize)...)
+	for _, frame := range frames {
+		frameStart := len(body)
+		body = append(body, make([]byte, batch.MessageHeaderSize)...)
+		binary.LittleEndian.PutUint32(body[frameStart+24:], frame.offsetDelta)
+		binary.LittleEndian.PutUint32(body[frameStart+28:], frame.timestampDelta)
+		binary.LittleEndian.PutUint32(body[frameStart+32:], uint32(len(frame.userHeaders)))
+		binary.LittleEndian.PutUint32(body[frameStart+36:], uint32(len(frame.payload)))
+		body = append(body, frame.payload...)
+		body = append(body, frame.userHeaders...)
+	}
+	header := batch.Header{
+		BaseOffset:      baseOffset,
+		BaseTimestamp:   baseTimestamp,
+		OriginTimestamp: originTimestamp,
+		BatchLength:     uint64(len(body) - recordStart),
+		MessageCount:    uint32(len(frames)),
+	}
+	header.EncodeInto(body[recordStart:])
+	return body
+}
+
+// pollPayload builds a poll reply body carrying the given messages in one
+// batch record.
 func pollPayload(t *testing.T, partitionId uint32, payloads ...[]byte) []byte {
 	t.Helper()
 
 	body := binary.LittleEndian.AppendUint32(nil, partitionId)
 	body = binary.LittleEndian.AppendUint64(body, 42)
 	body = binary.LittleEndian.AppendUint32(body, uint32(len(payloads)))
-	for _, payload := range payloads {
-		message, err := iggcon.NewIggyMessage(payload)
-		require.NoError(t, err)
-		headerBytes, err := message.Header.AppendBinary(nil)
-		require.NoError(t, err)
-		body = append(body, headerBytes...)
-		body = append(body, message.Payload...)
+	frames := make([]batchFrame, 0, len(payloads))
+	for index, payload := range payloads {
+		frames = append(frames, batchFrame{offsetDelta: uint32(index), payload: payload})
 	}
-	return body
+	return appendBatchRecord(body, 0, 0, 0, frames...)
 }
 
 func TestDeserializeFetchMessagesResponse_DecodesABatch(t *testing.T) {
@@ -224,20 +257,72 @@
 }
 
 func TestDeserializeFetchMessagesResponse_RejectsOverrunningUserHeaders(t *testing.T) {
-	message, err := iggcon.NewIggyMessage([]byte("payload"))
-	require.NoError(t, err)
-	message.Header.UserHeaderLength = 64
+	body := pollPayload(t, 1, []byte("payload"))
+	// Claim 64 user-header bytes the batch record does not carry.
+	binary.LittleEndian.PutUint32(body[pollPrefixLength+batch.HeaderSize+32:], 64)
 
+	_, err := DeserializeFetchMessagesResponse(body, iggcon.MESSAGE_COMPRESSION_NONE)
+	assert.Error(t, err, "user headers past the batch record must not panic or pass")
+}
+
+func TestDeserializeFetchMessagesResponse_RejectsNonzeroReservedFrameBytes(t *testing.T) {
+	body := pollPayload(t, 1, []byte("payload"))
+	body[pollPrefixLength+batch.HeaderSize+40] = 1
+
+	_, err := DeserializeFetchMessagesResponse(body, iggcon.MESSAGE_COMPRESSION_NONE)
+	assert.Error(t, err, "a frame with nonzero reserved bytes must not decode")
+}
+
+func TestDeserializeFetchMessagesResponse_ResolvesASlicedRecordsLeadingDelta(t *testing.T) {
+	// A server-sliced record keeps the stored base offset; the first frame's
+	// delta positions it inside the original batch.
 	body := binary.LittleEndian.AppendUint32(nil, 1)
-	body = binary.LittleEndian.AppendUint64(body, 0)
+	body = binary.LittleEndian.AppendUint64(body, 53)
 	body = binary.LittleEndian.AppendUint32(body, 1)
-	headerBytes, err := message.Header.AppendBinary(nil)
-	require.NoError(t, err)
-	body = append(body, headerBytes...)
-	body = append(body, message.Payload...)
+	body = appendBatchRecord(body, 50, 9000, 0,
+		batchFrame{offsetDelta: 3, payload: []byte("tail")})
 
-	_, err = DeserializeFetchMessagesResponse(body, iggcon.MESSAGE_COMPRESSION_NONE)
-	assert.Error(t, err, "user headers past the body must not panic or pass")
+	polled, err := DeserializeFetchMessagesResponse(body, iggcon.MESSAGE_COMPRESSION_NONE)
+	require.NoError(t, err)
+	require.Len(t, polled.Messages, 1)
+	assert.Equal(t, uint64(53), polled.Messages[0].Header.Offset)
+	assert.Equal(t, uint64(9000), polled.Messages[0].Header.Timestamp)
+}
+
+// goldenPollBody is a poll reply generated by the Rust encoder: partition 3,
+// current offset 101, and one batch record stamped with base offset 100 and
+// base timestamp 5000 carrying two messages.
+const goldenPollBody = "03000000650000000000000002000000030000000000000064000000000000008813000000000000e8030000000000008c01000000000000c96826b38a8feed202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573"
+
+func TestDeserializeFetchMessagesResponse_DecodesTheGoldenVector(t *testing.T) {
+	body, err := hex.DecodeString(goldenPollBody)
+	require.NoError(t, err)
+
+	polled, err := DeserializeFetchMessagesResponse(body, iggcon.MESSAGE_COMPRESSION_NONE)
+	require.NoError(t, err)
+
+	assert.Equal(t, uint32(3), polled.PartitionId)
+	assert.Equal(t, uint64(101), polled.CurrentOffset)
+	assert.Equal(t, uint32(2), polled.MessageCount)
+	require.Len(t, polled.Messages, 2)
+
+	first := polled.Messages[0]
+	assert.Equal(t, iggcon.MessageID{7}, first.Header.Id)
+	assert.Equal(t, uint64(100), first.Header.Offset)
+	assert.Equal(t, uint64(5000), first.Header.Timestamp)
+	assert.Equal(t, uint64(1000), first.Header.OriginTimestamp)
+	assert.Equal(t, uint64(0x7596755a20b9d2bf), first.Header.Checksum)
+	assert.Equal(t, []byte("first-payload"), first.Payload)
+	assert.Empty(t, first.UserHeaders)
+
+	second := polled.Messages[1]
+	assert.Equal(t, iggcon.MessageID{8}, second.Header.Id)
+	assert.Equal(t, uint64(101), second.Header.Offset)
+	assert.Equal(t, uint64(5000), second.Header.Timestamp)
+	assert.Equal(t, uint64(1050), second.Header.OriginTimestamp)
+	assert.Equal(t, uint64(0xc0b78e751c7e6bd6), second.Header.Checksum)
+	assert.Equal(t, []byte("second-payload"), second.Payload)
+	assert.Equal(t, []byte("user-header-bytes"), second.UserHeaders)
 }
 
 func TestDeserializeToTopic_PinsTheFieldOrderOfTheWireLayout(t *testing.T) {
diff --git a/foreign/go/contracts/message_header.go b/foreign/go/contracts/message_header.go
index 8b61292..70b58a7 100644
--- a/foreign/go/contracts/message_header.go
+++ b/foreign/go/contracts/message_header.go
@@ -18,15 +18,14 @@
 package iggcon
 
 import (
-	"encoding/binary"
-	"errors"
 	"time"
 )
 
-const MessageHeaderSize = 8 + 16 + 8 + 8 + 8 + 4 + 4 + 8
-
 type MessageID [16]byte
 
+// MessageHeader carries a message's metadata. On send, Id and
+// OriginTimestamp are the producer's (a zero Id is minted on encode); on
+// poll, Offset and Timestamp are the absolute values stamped by the server.
 type MessageHeader struct {
 	Checksum         uint64    `json:"checksum"`
 	Id               MessageID `json:"id"`
@@ -46,45 +45,3 @@
 		UserHeaderLength: userHeaderLength,
 	}
 }
-
-func MessageHeaderFromBytes(data []byte) (*MessageHeader, error) {
-
-	if len(data) != MessageHeaderSize {
-		return nil, errors.New("data has incorrect size, must be 64")
-	}
-	checksum := binary.LittleEndian.Uint64(data[0:8])
-	id := data[8:24]
-	offset := binary.LittleEndian.Uint64(data[24:32])
-	timestamp := binary.LittleEndian.Uint64(data[32:40])
-	originTimestamp := binary.LittleEndian.Uint64(data[40:48])
-	userHeaderLength := binary.LittleEndian.Uint32(data[48:52])
-	payloadLength := binary.LittleEndian.Uint32(data[52:56])
-	reserved := binary.LittleEndian.Uint64(data[56:64])
-
-	return &MessageHeader{
-		Checksum:         checksum,
-		Id:               MessageID(id),
-		Offset:           offset,
-		Timestamp:        timestamp,
-		OriginTimestamp:  originTimestamp,
-		UserHeaderLength: userHeaderLength,
-		PayloadLength:    payloadLength,
-		Reserved:         reserved,
-	}, nil
-}
-
-func (mh *MessageHeader) ToBytes() []byte {
-	bytes, _ := mh.AppendBinary(make([]byte, 0, MessageHeaderSize))
-	return bytes
-}
-
-func (mh *MessageHeader) AppendBinary(b []byte) ([]byte, error) {
-	b = binary.LittleEndian.AppendUint64(b, mh.Checksum)
-	b = append(b, mh.Id[:]...)
-	b = binary.LittleEndian.AppendUint64(b, mh.Offset)
-	b = binary.LittleEndian.AppendUint64(b, mh.Timestamp)
-	b = binary.LittleEndian.AppendUint64(b, mh.OriginTimestamp)
-	b = binary.LittleEndian.AppendUint32(b, mh.UserHeaderLength)
-	b = binary.LittleEndian.AppendUint32(b, mh.PayloadLength)
-	return binary.LittleEndian.AppendUint64(b, mh.Reserved), nil
-}
diff --git a/foreign/go/contracts/version.go b/foreign/go/contracts/version.go
index ba3f36b..9f51ca3 100644
--- a/foreign/go/contracts/version.go
+++ b/foreign/go/contracts/version.go
@@ -17,4 +17,4 @@
 
 package iggcon
 
-const Version = "0.9.0-edge.3"
+const Version = "0.9.0-edge.4"
diff --git a/foreign/go/go.mod b/foreign/go/go.mod
index 70e9dfa..cfd8f65 100644
--- a/foreign/go/go.mod
+++ b/foreign/go/go.mod
@@ -8,13 +8,16 @@
 	github.com/google/uuid v1.6.0
 	github.com/klauspost/compress v1.19.2
 	github.com/stretchr/testify v1.11.1
+	github.com/zeebo/xxh3 v1.1.0
 	gopkg.in/yaml.v3 v3.0.1
 )
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
 	github.com/kr/pretty v0.3.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/rogpeppe/go-internal v1.14.1 // indirect
+	golang.org/x/sys v0.30.0 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 )
diff --git a/foreign/go/go.sum b/foreign/go/go.sum
index c9dd51e..36e95e8 100644
--- a/foreign/go/go.sum
+++ b/foreign/go/go.sum
@@ -9,6 +9,8 @@
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
 github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -24,6 +26,12 @@
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
+github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
+github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
+github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/foreign/go/internal/batch/batch.go b/foreign/go/internal/batch/batch.go
new file mode 100644
index 0000000..c3762c3
--- /dev/null
+++ b/foreign/go/internal/batch/batch.go
@@ -0,0 +1,129 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+// Package batch implements the canonical message batch record shared by
+// produce request bodies and poll reply bodies: a 256-byte batch header
+// followed by message frames of [48-byte header][payload][user headers].
+package batch
+
+import (
+	"encoding/binary"
+	"errors"
+	"fmt"
+
+	"github.com/zeebo/xxh3"
+)
+
+// HeaderSize is the size of the batch header. Bytes past the declared
+// fields are reserved and stay zero.
+const HeaderSize = 256
+
+// MessageHeaderSize is the size of a message frame header.
+const MessageHeaderSize = 48
+
+// Header is the batch header. A producer leaves PartitionId, BaseOffset,
+// and BaseTimestamp zero; the server stamps them.
+type Header struct {
+	PartitionId     uint64
+	BaseOffset      uint64
+	BaseTimestamp   uint64
+	OriginTimestamp uint64
+	BatchLength     uint64
+	BatchChecksum   uint64
+	MessageCount    uint32
+}
+
+// DecodeHeader reads a batch header from the front of data.
+func DecodeHeader(data []byte) (Header, error) {
+	if len(data) < HeaderSize {
+		return Header{}, fmt.Errorf("batch header needs %d bytes, got %d", HeaderSize, len(data))
+	}
+	header := Header{
+		PartitionId:     binary.LittleEndian.Uint64(data[0:8]),
+		BaseOffset:      binary.LittleEndian.Uint64(data[8:16]),
+		BaseTimestamp:   binary.LittleEndian.Uint64(data[16:24]),
+		OriginTimestamp: binary.LittleEndian.Uint64(data[24:32]),
+		BatchLength:     binary.LittleEndian.Uint64(data[32:40]),
+		BatchChecksum:   binary.LittleEndian.Uint64(data[40:48]),
+		MessageCount:    binary.LittleEndian.Uint32(data[48:52]),
+	}
+	if header.BatchLength < HeaderSize {
+		return Header{}, fmt.Errorf(
+			"batch length %d does not cover the %d-byte batch header", header.BatchLength, HeaderSize)
+	}
+	return header, nil
+}
+
+// EncodeInto writes the header into the first HeaderSize bytes of b and
+// zeroes the reserved tail.
+func (h Header) EncodeInto(b []byte) {
+	clear(b[:HeaderSize])
+	binary.LittleEndian.PutUint64(b[0:8], h.PartitionId)
+	binary.LittleEndian.PutUint64(b[8:16], h.BaseOffset)
+	binary.LittleEndian.PutUint64(b[16:24], h.BaseTimestamp)
+	binary.LittleEndian.PutUint64(b[24:32], h.OriginTimestamp)
+	binary.LittleEndian.PutUint64(b[32:40], h.BatchLength)
+	binary.LittleEndian.PutUint64(b[40:48], h.BatchChecksum)
+	binary.LittleEndian.PutUint32(b[48:52], h.MessageCount)
+}
+
+// Checksum computes the batch checksum: XXH3-64 over the six header meta
+// fields followed by every frame's stored 8-byte checksum field in message
+// order. Message bodies are bound transitively through the frame checksums.
+func (h Header) Checksum(frameChecksums []byte) uint64 {
+	input := make([]byte, 0, 44+len(frameChecksums))
+	input = binary.LittleEndian.AppendUint64(input, h.PartitionId)
+	input = binary.LittleEndian.AppendUint64(input, h.BaseOffset)
+	input = binary.LittleEndian.AppendUint64(input, h.BaseTimestamp)
+	input = binary.LittleEndian.AppendUint64(input, h.OriginTimestamp)
+	input = binary.LittleEndian.AppendUint64(input, h.BatchLength)
+	input = binary.LittleEndian.AppendUint32(input, h.MessageCount)
+	input = append(input, frameChecksums...)
+	return xxh3.Hash(input)
+}
+
+// MessageHeader is a message frame header. OffsetDelta and TimestampDelta
+// resolve against the batch header's stamped bases; Checksum is XXH3-64
+// over frame[8:48] followed by the payload and the user headers.
+type MessageHeader struct {
+	Checksum          uint64
+	Id                [16]byte
+	OffsetDelta       uint32
+	TimestampDelta    uint32
+	UserHeadersLength uint32
+	PayloadLength     uint32
+}
+
+// DecodeMessageHeader reads a frame header from the front of data,
+// rejecting nonzero reserved bytes.
+func DecodeMessageHeader(data []byte) (MessageHeader, error) {
+	if len(data) < MessageHeaderSize {
+		return MessageHeader{}, fmt.Errorf(
+			"message frame header needs %d bytes, got %d", MessageHeaderSize, len(data))
+	}
+	if binary.LittleEndian.Uint64(data[40:48]) != 0 {
+		return MessageHeader{}, errors.New("message frame reserved bytes must be zero")
+	}
+	return MessageHeader{
+		Checksum:          binary.LittleEndian.Uint64(data[0:8]),
+		Id:                [16]byte(data[8:24]),
+		OffsetDelta:       binary.LittleEndian.Uint32(data[24:28]),
+		TimestampDelta:    binary.LittleEndian.Uint32(data[28:32]),
+		UserHeadersLength: binary.LittleEndian.Uint32(data[32:36]),
+		PayloadLength:     binary.LittleEndian.Uint32(data[36:40]),
+	}, nil
+}
diff --git a/foreign/go/internal/command/code.go b/foreign/go/internal/command/code.go
index 057a829..e62b816 100644
--- a/foreign/go/internal/command/code.go
+++ b/foreign/go/internal/command/code.go
@@ -20,58 +20,56 @@
 type Code int
 
 const (
-	PingCode                  Code = 1
-	GetStatsCode              Code = 10
-	GetSnapshotFileCode       Code = 11
-	GetClusterMetadataCode    Code = 12
-	DescribeOptionsCode       Code = 13
-	GetMeCode                 Code = 20
-	GetClientCode             Code = 21
-	GetClientsCode            Code = 22
-	GetUserCode               Code = 31
-	GetUsersCode              Code = 32
-	CreateUserCode            Code = 33
-	DeleteUserCode            Code = 34
-	UpdateUserCode            Code = 35
-	UpdatePermissionsCode     Code = 36
-	ChangePasswordCode        Code = 37
-	LoginUserCode             Code = 38
-	LogoutUserCode            Code = 39
-	LoginRegisterCode         Code = 40
-	GetAccessTokensCode       Code = 41
-	CreateAccessTokenCode     Code = 42
-	DeleteAccessTokenCode     Code = 43
-	LoginWithAccessTokenCode  Code = 44
-	LoginRegisterWithPATCode  Code = 45
-	PollMessagesCode          Code = 100
-	SendMessagesCode          Code = 101
-	GetOffsetCode             Code = 120
-	StoreOffsetCode           Code = 121
-	DeleteConsumerOffsetCode  Code = 122
-	StoreOffset2Code          Code = 123
-	DeleteConsumerOffset2Code Code = 124
-	GetStreamCode             Code = 200
-	GetStreamsCode            Code = 201
-	CreateStreamCode          Code = 202
-	DeleteStreamCode          Code = 203
-	UpdateStreamCode          Code = 204
-	PurgeStreamCode           Code = 205
-	GetTopicCode              Code = 300
-	GetTopicsCode             Code = 301
-	CreateTopicCode           Code = 302
-	DeleteTopicCode           Code = 303
-	UpdateTopicCode           Code = 304
-	PurgeTopicCode            Code = 305
-	CreatePartitionsCode      Code = 402
-	DeletePartitionsCode      Code = 403
-	DeleteSegmentsCode        Code = 503
-	GetGroupCode              Code = 600
-	GetGroupsCode             Code = 601
-	CreateGroupCode           Code = 602
-	DeleteGroupCode           Code = 603
-	JoinGroupCode             Code = 604
-	LeaveGroupCode            Code = 605
-	SyncGroupCode             Code = 606
+	PingCode                 Code = 1
+	GetStatsCode             Code = 10
+	GetSnapshotFileCode      Code = 11
+	GetClusterMetadataCode   Code = 12
+	DescribeOptionsCode      Code = 13
+	GetMeCode                Code = 20
+	GetClientCode            Code = 21
+	GetClientsCode           Code = 22
+	GetUserCode              Code = 31
+	GetUsersCode             Code = 32
+	CreateUserCode           Code = 33
+	DeleteUserCode           Code = 34
+	UpdateUserCode           Code = 35
+	UpdatePermissionsCode    Code = 36
+	ChangePasswordCode       Code = 37
+	LoginUserCode            Code = 38
+	LogoutUserCode           Code = 39
+	LoginRegisterCode        Code = 40
+	GetAccessTokensCode      Code = 41
+	CreateAccessTokenCode    Code = 42
+	DeleteAccessTokenCode    Code = 43
+	LoginWithAccessTokenCode Code = 44
+	LoginRegisterWithPATCode Code = 45
+	PollMessagesCode         Code = 100
+	SendMessagesCode         Code = 101
+	GetOffsetCode            Code = 120
+	StoreOffsetCode          Code = 121
+	DeleteConsumerOffsetCode Code = 122
+	GetStreamCode            Code = 200
+	GetStreamsCode           Code = 201
+	CreateStreamCode         Code = 202
+	DeleteStreamCode         Code = 203
+	UpdateStreamCode         Code = 204
+	PurgeStreamCode          Code = 205
+	GetTopicCode             Code = 300
+	GetTopicsCode            Code = 301
+	CreateTopicCode          Code = 302
+	DeleteTopicCode          Code = 303
+	UpdateTopicCode          Code = 304
+	PurgeTopicCode           Code = 305
+	CreatePartitionsCode     Code = 402
+	DeletePartitionsCode     Code = 403
+	DeleteSegmentsCode       Code = 503
+	GetGroupCode             Code = 600
+	GetGroupsCode            Code = 601
+	CreateGroupCode          Code = 602
+	DeleteGroupCode          Code = 603
+	JoinGroupCode            Code = 604
+	LeaveGroupCode           Code = 605
+	SyncGroupCode            Code = 606
 )
 
 //    internal const int GET_PERSONAL_ACCESS_TOKENS_CODE = 41;
diff --git a/foreign/go/internal/command/message.go b/foreign/go/internal/command/message.go
index ef9bcc9..19d277a 100644
--- a/foreign/go/internal/command/message.go
+++ b/foreign/go/internal/command/message.go
@@ -19,18 +19,15 @@
 
 import (
 	"encoding/binary"
+	"errors"
+	"fmt"
+	"math"
 
 	"github.com/apache/iggy/foreign/go/contracts"
+	"github.com/apache/iggy/foreign/go/internal/batch"
+	"github.com/google/uuid"
 	"github.com/klauspost/compress/s2"
-)
-
-const (
-	partitionPresenceSize = 1
-	partitionFieldSize    = 4
-	partitionStrategySize = partitionPresenceSize + partitionFieldSize + 1
-	offsetSize            = 12
-	commitFlagSize        = 1
-	indexSize             = 16
+	"github.com/zeebo/xxh3"
 )
 
 type SendMessages struct {
@@ -46,18 +43,24 @@
 	return SendMessagesCode
 }
 
-// zeroIndex is the blank per-message index entry reserved ahead of the
-// message section and filled in as messages are appended.
-var zeroIndex [indexSize]byte
+// zeroBatchHeader is the blank batch header reserved ahead of the message
+// frames and backpatched once every frame checksum is known.
+var zeroBatchHeader [batch.HeaderSize]byte
 
 func (s *SendMessages) MarshalBinary() ([]byte, error) {
 	return s.AppendBinary(nil)
 }
 
 // AppendBinary encodes the batch straight into b: [metadata_length u32]
-// [stream id][topic id][partitioning][messages_count u32], the per-message
-// index section, then each message as header, payload, user headers.
+// [stream id][topic id][partitioning][messages_count u32], then one canonical
+// batch record: a 256-byte batch header followed by one frame per message.
 func (s *SendMessages) AppendBinary(b []byte) ([]byte, error) {
+	// The server rejects an empty batch at admission. Refuse it before the
+	// wire, matching every other SDK encoder.
+	if len(s.Messages) == 0 {
+		return b, errors.New("cannot encode an empty message batch")
+	}
+
 	s.compressPayloads()
 
 	metadataStart := len(b)
@@ -76,30 +79,65 @@
 	metadataLength := len(b) - metadataStart - 4
 	binary.LittleEndian.PutUint32(b[metadataStart:], uint32(metadataLength))
 
-	indexesStart := len(b)
-	for range s.Messages {
-		b = append(b, zeroIndex[:]...)
+	var originTimestamp uint64
+	for i := range s.Messages {
+		if i == 0 || s.Messages[i].Header.OriginTimestamp < originTimestamp {
+			originTimestamp = s.Messages[i].Header.OriginTimestamp
+		}
 	}
 
-	msgSize := uint32(0)
+	headerStart := len(b)
+	b = append(b, zeroBatchHeader[:]...)
+
+	blobStart := len(b)
+	frameChecksums := make([]byte, 0, len(s.Messages)*8)
 	for i := range s.Messages {
 		message := &s.Messages[i]
+		// The id sits under the frame checksum, so it must exist before the
+		// frame is hashed; the server never mints ids.
+		if message.Header.Id == (iggcon.MessageID{}) {
+			id, err := uuid.NewRandom()
+			if err != nil {
+				return b, err
+			}
+			message.Header.Id = iggcon.MessageID(id)
+		}
 		// The header lengths and the appended slices must agree, or every
 		// message boundary after a mismatch mis-frames; deriving both from
 		// the same slice makes the disagreement impossible.
 		message.Header.PayloadLength = uint32(len(message.Payload))
 		message.Header.UserHeaderLength = uint32(len(message.UserHeaders))
-		if b, err = message.Header.AppendBinary(b); err != nil {
-			return b, err
+		timestampDelta := message.Header.OriginTimestamp - originTimestamp
+		if timestampDelta > math.MaxUint32 {
+			return b, fmt.Errorf(
+				"message origin timestamp %d runs more than %d microseconds past the batch's earliest %d",
+				message.Header.OriginTimestamp, uint64(math.MaxUint32), originTimestamp)
 		}
+
+		frameStart := len(b)
+		b = binary.LittleEndian.AppendUint64(b, 0)
+		b = append(b, message.Header.Id[:]...)
+		b = binary.LittleEndian.AppendUint32(b, uint32(i))
+		b = binary.LittleEndian.AppendUint32(b, uint32(timestampDelta))
+		b = binary.LittleEndian.AppendUint32(b, message.Header.UserHeaderLength)
+		b = binary.LittleEndian.AppendUint32(b, message.Header.PayloadLength)
+		b = binary.LittleEndian.AppendUint64(b, 0)
 		b = append(b, message.Payload...)
 		b = append(b, message.UserHeaders...)
 
-		msgSize += iggcon.MessageHeaderSize +
-			message.Header.PayloadLength + message.Header.UserHeaderLength
-		binary.LittleEndian.PutUint32(b[indexesStart+i*indexSize+4:], msgSize)
+		checksum := xxh3.Hash(b[frameStart+8:])
+		binary.LittleEndian.PutUint64(b[frameStart:], checksum)
+		message.Header.Checksum = checksum
+		frameChecksums = binary.LittleEndian.AppendUint64(frameChecksums, checksum)
 	}
 
+	batchHeader := batch.Header{
+		OriginTimestamp: originTimestamp,
+		BatchLength:     uint64(batch.HeaderSize + len(b) - blobStart),
+		MessageCount:    uint32(len(s.Messages)),
+	}
+	batchHeader.BatchChecksum = batchHeader.Checksum(frameChecksums)
+	batchHeader.EncodeInto(b[headerStart:blobStart])
 	return b, nil
 }
 
diff --git a/foreign/go/internal/command/message_test.go b/foreign/go/internal/command/message_test.go
index 1fbe43c..d6944d6 100644
--- a/foreign/go/internal/command/message_test.go
+++ b/foreign/go/internal/command/message_test.go
@@ -19,9 +19,13 @@
 
 import (
 	"bytes"
+	"encoding/binary"
+	"encoding/hex"
+	"math"
 	"testing"
 
 	"github.com/apache/iggy/foreign/go/contracts"
+	"github.com/apache/iggy/foreign/go/internal/batch"
 	"github.com/google/uuid"
 	"github.com/klauspost/compress/s2"
 )
@@ -89,6 +93,54 @@
 	return true
 }
 
+// Golden vectors generated by the Rust encoder for stream 1, topic 2,
+// balanced partitioning, and two messages: {id 7, origin timestamp 1000,
+// payload "first-payload"} and {id 8, origin timestamp 1050, payload
+// "second-payload", user headers "user-header-bytes"}.
+const (
+	goldenProduceBodyFull  = "12000000010401000000010402000000010002000000000000000000000000000000000000000000000000000000e8030000000000008c01000000000000a91f38c86307267c02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573"
+	goldenProduceBatchOnly = "000000000000000000000000000000000000000000000000e8030000000000008c01000000000000a91f38c86307267c02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573"
+)
+
+func TestSerialize_SendMessagesMatchesTheGoldenVector(t *testing.T) {
+	streamId, _ := iggcon.NewIdentifier(uint32(1))
+	topicId, _ := iggcon.NewIdentifier(uint32(2))
+	request := SendMessages{
+		StreamId:     streamId,
+		TopicId:      topicId,
+		Partitioning: iggcon.None(),
+		Messages: []iggcon.IggyMessage{
+			{
+				Header:  iggcon.MessageHeader{Id: iggcon.MessageID{7}, OriginTimestamp: 1000},
+				Payload: []byte("first-payload"),
+			},
+			{
+				Header:      iggcon.MessageHeader{Id: iggcon.MessageID{8}, OriginTimestamp: 1050},
+				Payload:     []byte("second-payload"),
+				UserHeaders: []byte("user-header-bytes"),
+			},
+		},
+		Compression: iggcon.MESSAGE_COMPRESSION_NONE,
+	}
+
+	serialized, err := request.MarshalBinary()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got := hex.EncodeToString(serialized); got != goldenProduceBodyFull {
+		t.Errorf("the full produce body diverges from the golden vector.\nExpected:\t%s\nGot:\t\t%s",
+			goldenProduceBodyFull, got)
+	}
+
+	metadataLength := binary.LittleEndian.Uint32(serialized[0:4])
+	batchOnly := serialized[4+metadataLength:]
+	if got := hex.EncodeToString(batchOnly); got != goldenProduceBatchOnly {
+		t.Errorf("the batch section diverges from the golden vector.\nExpected:\t%s\nGot:\t\t%s",
+			goldenProduceBatchOnly, got)
+	}
+}
+
 func TestSerialize_SendMessagesRequest(t *testing.T) {
 	message1 := generateTestMessage("data1")
 	streamId, _ := iggcon.NewIdentifier("test_stream_id")
@@ -109,8 +161,7 @@
 		t.Error(err)
 	}
 
-	// Expected serialized bytes based on the provided sample request
-	expected := []byte{
+	expectedMetadata := []byte{
 		0x29, 0x0, 0x0, 0x0, // metadataLength
 		0x02,                                                                               // StreamId Kind (StringId)
 		0x0E,                                                                               // StreamId Length (14)
@@ -121,17 +172,119 @@
 		0x74, 0x65, 0x73, 0x74, 0x5F, 0x74, 0x6F, 0x70, 0x69, 0x63, 0x5F, 0x69, 0x64, // TopicId
 		0x02,                   // PartitionIdKind
 		0x04,                   // Partitioning Length
-		0x01, 0x00, 0x00, 0x00, // PartitionId (123)
+		0x01, 0x00, 0x00, 0x00, // PartitionId (1)
 		0x01, 0x0, 0x0, 0x0, // MessageCount
-		0, 0, 0, 0, 120, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Index (16*1) bytes
 	}
-	expected = append(expected, message1.Header.ToBytes()...)
-	expected = append(expected, message1.Payload...)
-	expected = append(expected, message1.UserHeaders...)
+	if !bytes.Equal(serialized[:len(expectedMetadata)], expectedMetadata) {
+		t.Fatalf("the metadata section is incorrect.\nExpected:\t%v\nGot:\t\t%v",
+			expectedMetadata, serialized[:len(expectedMetadata)])
+	}
 
-	// Check if the serialized bytes match the expected bytes
-	if !bytes.Equal(serialized, expected) {
-		t.Errorf("Serialized bytes are incorrect. \nExpected:\t%v\nGot:\t\t%v", expected, serialized)
+	record := serialized[len(expectedMetadata):]
+	header, err := batch.DecodeHeader(record)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if header.PartitionId != 0 || header.BaseOffset != 0 || header.BaseTimestamp != 0 {
+		t.Errorf("the server-stamped fields must be zero, got %+v", header)
+	}
+	if header.OriginTimestamp != message1.Header.OriginTimestamp {
+		t.Errorf("batch origin timestamp = %d, want %d",
+			header.OriginTimestamp, message1.Header.OriginTimestamp)
+	}
+	if header.BatchLength != uint64(len(record)) {
+		t.Errorf("batch length = %d, the record holds %d bytes", header.BatchLength, len(record))
+	}
+	if header.MessageCount != 1 {
+		t.Errorf("message count = %d, want 1", header.MessageCount)
+	}
+	if got := header.Checksum(record[batch.HeaderSize : batch.HeaderSize+8]); got != header.BatchChecksum {
+		t.Errorf("batch checksum = %d, recomputed %d", header.BatchChecksum, got)
+	}
+
+	frame, err := batch.DecodeMessageHeader(record[batch.HeaderSize:])
+	if err != nil {
+		t.Fatal(err)
+	}
+	if frame.Id != [16]byte(message1.Header.Id) {
+		t.Errorf("frame id = %v, want %v", frame.Id, message1.Header.Id)
+	}
+	if frame.OffsetDelta != 0 || frame.TimestampDelta != 0 {
+		t.Errorf("a single-message batch has zero deltas, got %+v", frame)
+	}
+	if int(frame.PayloadLength) != len(message1.Payload) ||
+		int(frame.UserHeadersLength) != len(message1.UserHeaders) {
+		t.Errorf("frame lengths %d/%d do not match the message %d/%d",
+			frame.PayloadLength, frame.UserHeadersLength,
+			len(message1.Payload), len(message1.UserHeaders))
+	}
+
+	body := record[batch.HeaderSize+batch.MessageHeaderSize:]
+	expectedBody := append(append([]byte{}, message1.Payload...), message1.UserHeaders...)
+	if !bytes.Equal(body, expectedBody) {
+		t.Errorf("the frame body is incorrect.\nExpected:\t%v\nGot:\t\t%v", expectedBody, body)
+	}
+}
+
+func TestSerialize_SendMessagesMintsAZeroMessageId(t *testing.T) {
+	message, err := iggcon.NewIggyMessage([]byte("payload"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	streamId, _ := iggcon.NewIdentifier(uint32(1))
+	topicId, _ := iggcon.NewIdentifier(uint32(1))
+	request := SendMessages{
+		StreamId:     streamId,
+		TopicId:      topicId,
+		Partitioning: iggcon.PartitionId(0),
+		Messages:     []iggcon.IggyMessage{message},
+		Compression:  iggcon.MESSAGE_COMPRESSION_NONE,
+	}
+
+	serialized, err := request.MarshalBinary()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	minted := request.Messages[0].Header.Id
+	if minted == (iggcon.MessageID{}) {
+		t.Fatal("a zero id must be minted before encoding")
+	}
+	metadataLength := binary.LittleEndian.Uint32(serialized[0:4])
+	frame, err := batch.DecodeMessageHeader(serialized[4+int(metadataLength)+batch.HeaderSize:])
+	if err != nil {
+		t.Fatal(err)
+	}
+	if frame.Id != [16]byte(minted) {
+		t.Errorf("the wire carries id %v, the message holds %v", frame.Id, minted)
+	}
+}
+
+func TestSerialize_SendMessagesRejectsATimestampDeltaPastU32(t *testing.T) {
+	streamId, _ := iggcon.NewIdentifier(uint32(1))
+	topicId, _ := iggcon.NewIdentifier(uint32(1))
+	request := SendMessages{
+		StreamId:     streamId,
+		TopicId:      topicId,
+		Partitioning: iggcon.PartitionId(0),
+		Messages: []iggcon.IggyMessage{
+			{
+				Header:  iggcon.MessageHeader{Id: iggcon.MessageID{1}, OriginTimestamp: 0},
+				Payload: []byte("early"),
+			},
+			{
+				Header: iggcon.MessageHeader{
+					Id:              iggcon.MessageID{2},
+					OriginTimestamp: uint64(math.MaxUint32) + 1,
+				},
+				Payload: []byte("late"),
+			},
+		},
+		Compression: iggcon.MESSAGE_COMPRESSION_NONE,
+	}
+
+	if _, err := request.MarshalBinary(); err == nil {
+		t.Fatal("a timestamp delta past u32 must fail the encode")
 	}
 }
 
@@ -166,20 +319,19 @@
 		t.Fatalf("the payload did not compress: %d >= %d", len(compressed.Payload), len(payload))
 	}
 
-	// The message section must be framed by the compressed length: header,
-	// then exactly PayloadLength payload bytes, and nothing after.
-	messageStart := len(serialized) - int(compressed.Header.PayloadLength) - iggcon.MessageHeaderSize
-	header, err := iggcon.MessageHeaderFromBytes(
-		serialized[messageStart : messageStart+iggcon.MessageHeaderSize])
+	// The frame must be sized by the compressed length: header, then exactly
+	// PayloadLength payload bytes, and nothing after.
+	frameStart := len(serialized) - int(compressed.Header.PayloadLength) - batch.MessageHeaderSize
+	frame, err := batch.DecodeMessageHeader(serialized[frameStart:])
 	if err != nil {
 		t.Fatal(err)
 	}
-	if header.PayloadLength != compressed.Header.PayloadLength {
-		t.Fatalf("wire header claims %d, in-memory header %d",
-			header.PayloadLength, compressed.Header.PayloadLength)
+	if frame.PayloadLength != compressed.Header.PayloadLength {
+		t.Fatalf("wire frame claims %d, in-memory header %d",
+			frame.PayloadLength, compressed.Header.PayloadLength)
 	}
 
-	decoded, err := s2.Decode(nil, serialized[messageStart+iggcon.MessageHeaderSize:])
+	decoded, err := s2.Decode(nil, serialized[frameStart+batch.MessageHeaderSize:])
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/foreign/go/internal/command/offset.go b/foreign/go/internal/command/offset.go
index ed187f8..e71c78e 100644
--- a/foreign/go/internal/command/offset.go
+++ b/foreign/go/internal/command/offset.go
@@ -23,6 +23,10 @@
 	iggcon "github.com/apache/iggy/foreign/go/contracts"
 )
 
+// ackQuorum is the AckLevel the offset commands send: the server replies only
+// once the write is committed.
+const ackQuorum byte = 1
+
 type StoreConsumerOffsetRequest struct {
 	StreamId    iggcon.Identifier `json:"streamId"`
 	TopicId     iggcon.Identifier `json:"topicId"`
@@ -54,8 +58,8 @@
 	if err != nil {
 		return nil, err
 	}
-	// consumer + stream_id + topic_id + hasPartition(1) + partition(4) + offset(8)
-	bytes := make([]byte, len(consumerBytes)+len(streamIdBytes)+len(topicIdBytes)+13)
+	// consumer + stream_id + topic_id + hasPartition(1) + partition(4) + offset(8) + ack(1)
+	bytes := make([]byte, len(consumerBytes)+len(streamIdBytes)+len(topicIdBytes)+14)
 	position := 0
 	copy(bytes[position:], consumerBytes)
 	position += len(consumerBytes)
@@ -66,6 +70,7 @@
 	bytes[position] = hasPartition
 	binary.LittleEndian.PutUint32(bytes[position+1:position+5], partition)
 	binary.LittleEndian.PutUint64(bytes[position+5:position+13], s.Offset)
+	bytes[position+13] = ackQuorum
 	return bytes, nil
 }
 
@@ -143,8 +148,8 @@
 	if err != nil {
 		return nil, err
 	}
-	// consumer + stream_id + topic_id + hasPartition(1) + partition(4)
-	bytes := make([]byte, len(consumerBytes)+len(streamIdBytes)+len(topicIdBytes)+5)
+	// consumer + stream_id + topic_id + hasPartition(1) + partition(4) + ack(1)
+	bytes := make([]byte, len(consumerBytes)+len(streamIdBytes)+len(topicIdBytes)+6)
 	position := 0
 	copy(bytes[position:], consumerBytes)
 	position += len(consumerBytes)
@@ -154,5 +159,6 @@
 	position += len(topicIdBytes)
 	bytes[position] = hasPartition
 	binary.LittleEndian.PutUint32(bytes[position+1:position+5], partition)
+	bytes[position+5] = ackQuorum
 	return bytes, nil
 }
diff --git a/foreign/go/internal/vsr/operation.go b/foreign/go/internal/vsr/operation.go
index f9c47ef..cc95c57 100644
--- a/foreign/go/internal/vsr/operation.go
+++ b/foreign/go/internal/vsr/operation.go
@@ -60,11 +60,9 @@
 	OperationJoinConsumerGroup         Operation = 148
 	OperationLeaveConsumerGroup        Operation = 149
 
-	OperationSendMessages          Operation = 160
-	OperationStoreConsumerOffset   Operation = 161
-	OperationDeleteConsumerOffset  Operation = 162
-	OperationStoreConsumerOffset2  Operation = 164
-	OperationDeleteConsumerOffset2 Operation = 165
+	OperationSendMessages         Operation = 160
+	OperationStoreConsumerOffset  Operation = 161
+	OperationDeleteConsumerOffset Operation = 162
 )
 
 // Band boundaries. The internal band is never client-sent.
@@ -111,8 +109,6 @@
 	OperationSendMessages,
 	OperationStoreConsumerOffset,
 	OperationDeleteConsumerOffset,
-	OperationStoreConsumerOffset2,
-	OperationDeleteConsumerOffset2,
 }
 
 var knownOperations = newOperationSet(allOperations)
@@ -122,33 +118,31 @@
 // header field, leaving the server authoritative for commands this SDK build
 // does not know.
 var replicatedOperation = map[uint32]Operation{
-	uint32(command.CreateUserCode):            OperationCreateUser,
-	uint32(command.DeleteUserCode):            OperationDeleteUser,
-	uint32(command.UpdateUserCode):            OperationUpdateUser,
-	uint32(command.UpdatePermissionsCode):     OperationUpdatePermissions,
-	uint32(command.ChangePasswordCode):        OperationChangePassword,
-	uint32(command.CreateAccessTokenCode):     OperationCreatePersonalAccessToken,
-	uint32(command.DeleteAccessTokenCode):     OperationDeletePersonalAccessToken,
-	uint32(command.SendMessagesCode):          OperationSendMessages,
-	uint32(command.StoreOffsetCode):           OperationStoreConsumerOffset,
-	uint32(command.DeleteConsumerOffsetCode):  OperationDeleteConsumerOffset,
-	uint32(command.StoreOffset2Code):          OperationStoreConsumerOffset2,
-	uint32(command.DeleteConsumerOffset2Code): OperationDeleteConsumerOffset2,
-	uint32(command.CreateStreamCode):          OperationCreateStream,
-	uint32(command.DeleteStreamCode):          OperationDeleteStream,
-	uint32(command.UpdateStreamCode):          OperationUpdateStream,
-	uint32(command.PurgeStreamCode):           OperationPurgeStream,
-	uint32(command.CreateTopicCode):           OperationCreateTopic,
-	uint32(command.DeleteTopicCode):           OperationDeleteTopic,
-	uint32(command.UpdateTopicCode):           OperationUpdateTopic,
-	uint32(command.PurgeTopicCode):            OperationPurgeTopic,
-	uint32(command.CreatePartitionsCode):      OperationCreatePartitions,
-	uint32(command.DeletePartitionsCode):      OperationDeletePartitions,
-	uint32(command.DeleteSegmentsCode):        OperationDeleteSegments,
-	uint32(command.CreateGroupCode):           OperationCreateConsumerGroup,
-	uint32(command.DeleteGroupCode):           OperationDeleteConsumerGroup,
-	uint32(command.JoinGroupCode):             OperationJoinConsumerGroup,
-	uint32(command.LeaveGroupCode):            OperationLeaveConsumerGroup,
+	uint32(command.CreateUserCode):           OperationCreateUser,
+	uint32(command.DeleteUserCode):           OperationDeleteUser,
+	uint32(command.UpdateUserCode):           OperationUpdateUser,
+	uint32(command.UpdatePermissionsCode):    OperationUpdatePermissions,
+	uint32(command.ChangePasswordCode):       OperationChangePassword,
+	uint32(command.CreateAccessTokenCode):    OperationCreatePersonalAccessToken,
+	uint32(command.DeleteAccessTokenCode):    OperationDeletePersonalAccessToken,
+	uint32(command.SendMessagesCode):         OperationSendMessages,
+	uint32(command.StoreOffsetCode):          OperationStoreConsumerOffset,
+	uint32(command.DeleteConsumerOffsetCode): OperationDeleteConsumerOffset,
+	uint32(command.CreateStreamCode):         OperationCreateStream,
+	uint32(command.DeleteStreamCode):         OperationDeleteStream,
+	uint32(command.UpdateStreamCode):         OperationUpdateStream,
+	uint32(command.PurgeStreamCode):          OperationPurgeStream,
+	uint32(command.CreateTopicCode):          OperationCreateTopic,
+	uint32(command.DeleteTopicCode):          OperationDeleteTopic,
+	uint32(command.UpdateTopicCode):          OperationUpdateTopic,
+	uint32(command.PurgeTopicCode):           OperationPurgeTopic,
+	uint32(command.CreatePartitionsCode):     OperationCreatePartitions,
+	uint32(command.DeletePartitionsCode):     OperationDeletePartitions,
+	uint32(command.DeleteSegmentsCode):       OperationDeleteSegments,
+	uint32(command.CreateGroupCode):          OperationCreateConsumerGroup,
+	uint32(command.DeleteGroupCode):          OperationDeleteConsumerGroup,
+	uint32(command.JoinGroupCode):            OperationJoinConsumerGroup,
+	uint32(command.LeaveGroupCode):           OperationLeaveConsumerGroup,
 }
 
 func newOperationSet(operations []Operation) map[Operation]struct{} {
@@ -227,8 +221,7 @@
 		return true
 	}
 	switch operation {
-	case OperationStoreConsumerOffset, OperationStoreConsumerOffset2,
-		OperationDeleteConsumerOffset, OperationDeleteConsumerOffset2:
+	case OperationStoreConsumerOffset, OperationDeleteConsumerOffset:
 		return true
 	default:
 		return false
diff --git a/foreign/go/internal/vsr/operation_test.go b/foreign/go/internal/vsr/operation_test.go
index ef08726..0b7b478 100644
--- a/foreign/go/internal/vsr/operation_test.go
+++ b/foreign/go/internal/vsr/operation_test.go
@@ -41,8 +41,6 @@
 		{code: command.SendMessagesCode, want: OperationSendMessages},
 		{code: command.StoreOffsetCode, want: OperationStoreConsumerOffset},
 		{code: command.DeleteConsumerOffsetCode, want: OperationDeleteConsumerOffset},
-		{code: command.StoreOffset2Code, want: OperationStoreConsumerOffset2},
-		{code: command.DeleteConsumerOffset2Code, want: OperationDeleteConsumerOffset2},
 		{code: command.CreateStreamCode, want: OperationCreateStream},
 		{code: command.DeleteStreamCode, want: OperationDeleteStream},
 		{code: command.UpdateStreamCode, want: OperationUpdateStream},
@@ -114,7 +112,7 @@
 		assert.True(t, IsKnownOperation(operation), "operation %d", operation)
 	}
 
-	undeclared := []Operation{4, 63, 69, 127, 150, 159, 163, 166, 255}
+	undeclared := []Operation{4, 63, 69, 127, 150, 159, 163, 164, 165, 255}
 	for _, operation := range undeclared {
 		assert.False(t, IsKnownOperation(operation), "operation %d", operation)
 	}
@@ -153,7 +151,7 @@
 		OperationLogout,
 		OperationSendMessages,
 		OperationStoreConsumerOffset,
-		OperationDeleteConsumerOffset2,
+		OperationDeleteConsumerOffset,
 	}
 	for _, operation := range nonMetadata {
 		assert.False(t, IsMetadata(operation), "operation %d", operation)
@@ -163,7 +161,7 @@
 func TestIsPartition_CoversTheDataPlaneBand(t *testing.T) {
 	assert.True(t, IsPartition(OperationSendMessages))
 	assert.True(t, IsPartition(OperationStoreConsumerOffset))
-	assert.True(t, IsPartition(OperationDeleteConsumerOffset2))
+	assert.True(t, IsPartition(OperationDeleteConsumerOffset))
 	assert.False(t, IsPartition(OperationLeaveConsumerGroup))
 	assert.False(t, IsPartition(OperationDeleteSegments))
 }
@@ -177,8 +175,6 @@
 		OperationLeaveConsumerGroup,
 		OperationStoreConsumerOffset,
 		OperationDeleteConsumerOffset,
-		OperationStoreConsumerOffset2,
-		OperationDeleteConsumerOffset2,
 	}
 	for _, operation := range framed {
 		assert.True(t, IsResultFramed(operation), "operation %d", operation)
diff --git a/foreign/go/internal/vsr/protocol_parity_test.go b/foreign/go/internal/vsr/protocol_parity_test.go
index b510c8e..6cddd7f 100644
--- a/foreign/go/internal/vsr/protocol_parity_test.go
+++ b/foreign/go/internal/vsr/protocol_parity_test.go
@@ -87,8 +87,6 @@
 	"SendMessages":                    OperationSendMessages,
 	"StoreConsumerOffset":             OperationStoreConsumerOffset,
 	"DeleteConsumerOffset":            OperationDeleteConsumerOffset,
-	"StoreConsumerOffset2":            OperationStoreConsumerOffset2,
-	"DeleteConsumerOffset2":           OperationDeleteConsumerOffset2,
 }
 
 // goEvictionReasons names every eviction discriminant the codec declares.
@@ -159,7 +157,7 @@
 	"u32":            {4, 4},
 	"u64":            {8, 8},
 	"u128":           {16, 16},
-	"Command2":       {1, 1},
+	"Command":        {1, 1},
 	"Operation":      {1, 1},
 	"EvictionReason": {1, 1},
 }
@@ -346,15 +344,15 @@
 
 func TestProtocolParity_FrameCommands(t *testing.T) {
 	sources := loadRustSources(t)
-	rustValues := rustEnumValues(sources["command"], "Command2")
-	require.NotEmpty(t, rustValues, "the Rust Command2 enum was not found")
+	rustValues := rustEnumValues(sources["command"], "Command")
+	require.NotEmpty(t, rustValues, "the Rust Command enum was not found")
 
 	for name, got := range goFrameCommands {
 		want, ok := rustValues[name]
-		if !assert.True(t, ok, "Rust does not declare Command2::%s", name) {
+		if !assert.True(t, ok, "Rust does not declare Command::%s", name) {
 			continue
 		}
-		assert.Equal(t, want, uint64(got), "Command2::%s", name)
+		assert.Equal(t, want, uint64(got), "Command::%s", name)
 	}
 }
 
diff --git a/foreign/go/tests/e2e_helpers_test.go b/foreign/go/tests/e2e_helpers_test.go
index 8a4549b..b7d7bf6 100644
--- a/foreign/go/tests/e2e_helpers_test.go
+++ b/foreign/go/tests/e2e_helpers_test.go
@@ -52,12 +52,12 @@
 	rootPassword = "iggy"
 
 	// Command codes the raw-request cases drive directly.
-	getSnapshotCode           = 11
-	storeConsumerOffset2Code  = 123
-	deleteConsumerOffset2Code = 124
-	vendorCode                = 60000
+	getSnapshotCode          = 11
+	storeConsumerOffsetCode  = 121
+	deleteConsumerOffsetCode = 122
+	vendorCode               = 60000
 
-	// AckLevel values of the v2 consumer-offset requests.
+	// AckLevel values of the consumer-offset requests.
 	ackNoAck  = 0
 	ackQuorum = 1
 )
@@ -158,9 +158,9 @@
 	return messages
 }
 
-// consumerOffsetV2Payload builds the shared prefix of the v2 consumer-offset
+// consumerOffsetPayload builds the shared prefix of the consumer-offset
 // requests: [consumer][stream][topic][partition flag][partition].
-func consumerOffsetV2Payload(
+func consumerOffsetPayload(
 	t *testing.T,
 	consumer iggcon.Consumer,
 	streamId, topicId iggcon.Identifier,
diff --git a/foreign/go/tests/e2e_test.go b/foreign/go/tests/e2e_test.go
index 48c2cfc..c6ee7d6 100644
--- a/foreign/go/tests/e2e_test.go
+++ b/foreign/go/tests/e2e_test.go
@@ -205,7 +205,7 @@
 	assert.Error(t, err, "a login must go through LoginUser, not the raw path")
 }
 
-func TestE2E_ConsumerOffsetsV2OverTheRawPath(t *testing.T) {
+func TestE2E_ConsumerOffsetAckLevelsOverTheRawPath(t *testing.T) {
 	connected := connect(t)
 	ctx := context.Background()
 	streamId, topicId := scratchTopic(t, connected, 1)
@@ -223,11 +223,11 @@
 			offset = 4
 		}
 
-		store := consumerOffsetV2Payload(t, consumer, streamId, topicId, partitionId)
+		store := consumerOffsetPayload(t, consumer, streamId, topicId, partitionId)
 		store = binary.LittleEndian.AppendUint64(store, offset)
 		store = append(store, ack)
 
-		_, err := connected.SendBinaryRequest(ctx, storeConsumerOffset2Code, store)
+		_, err := connected.SendBinaryRequest(ctx, storeConsumerOffsetCode, store)
 		require.NoError(t, err, "ack level %d", ack)
 
 		stored, err := connected.GetConsumerOffset(ctx, consumer, streamId, topicId, &partitionId)
@@ -236,9 +236,9 @@
 		assert.Equal(t, offset, stored.StoredOffset, "ack level %d", ack)
 	}
 
-	remove := consumerOffsetV2Payload(t, consumer, streamId, topicId, partitionId)
+	remove := consumerOffsetPayload(t, consumer, streamId, topicId, partitionId)
 	remove = append(remove, ackQuorum)
-	_, err = connected.SendBinaryRequest(ctx, deleteConsumerOffset2Code, remove)
+	_, err = connected.SendBinaryRequest(ctx, deleteConsumerOffsetCode, remove)
 	require.NoError(t, err)
 }
 
diff --git a/foreign/java/gradle/libs.versions.toml b/foreign/java/gradle/libs.versions.toml
index 6b5b54d..2cf5ce3 100644
--- a/foreign/java/gradle/libs.versions.toml
+++ b/foreign/java/gradle/libs.versions.toml
@@ -29,6 +29,9 @@
 # Apache Commons
 commons-lang3 = "3.20.0"
 
+# Hashing
+hash4j = "0.22.0"
+
 # HTTP Client
 httpclient5 = "5.6.3"
 
@@ -71,6 +74,9 @@
 # Apache Commons
 commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang3" }
 
+# Hashing
+hash4j = { module = "com.dynatrace.hash4j:hash4j", version.ref = "hash4j" }
+
 # Logging
 slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
 slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
diff --git a/foreign/java/java-sdk/build.gradle.kts b/foreign/java/java-sdk/build.gradle.kts
index 688680e..2fa1af9 100644
--- a/foreign/java/java-sdk/build.gradle.kts
+++ b/foreign/java/java-sdk/build.gradle.kts
@@ -43,6 +43,7 @@
     implementation(libs.httpclient5)
     implementation(libs.jackson.databind)
     implementation(libs.commons.lang3)
+    implementation(libs.hash4j)
     implementation(libs.slf4j.api)
     implementation(libs.spotbugs.annotations)
     implementation(libs.netty.buffer)
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/ConsumerOffsetsTcpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/ConsumerOffsetsTcpClient.java
index cb53a72..698f158 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/ConsumerOffsetsTcpClient.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/ConsumerOffsetsTcpClient.java
@@ -41,6 +41,8 @@
 public class ConsumerOffsetsTcpClient implements ConsumerOffsetsClient {
     private static final Logger log = LoggerFactory.getLogger(ConsumerOffsetsTcpClient.class);
 
+    private static final byte ACK_QUORUM = 1;
+
     private final Supplier<AsyncTcpConnection> connectionSupplier;
 
     public ConsumerOffsetsTcpClient(Supplier<AsyncTcpConnection> connectionSupplier) {
@@ -59,6 +61,7 @@
         payload.writeBytes(BytesSerializer.toBytes(topicId));
         payload.writeBytes(BytesSerializer.toBytes(partitionId));
         payload.writeBytes(BytesSerializer.toBytesAsU64(offset));
+        payload.writeByte(ACK_QUORUM);
 
         log.debug(
                 "Storing consumer offset - Stream: {}, Topic: {}, Partition: {}, Consumer: {}, Offset: {}",
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/MessagesTcpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/MessagesTcpClient.java
index 17b8bc5..bee4cb8 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/MessagesTcpClient.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/MessagesTcpClient.java
@@ -51,6 +51,7 @@
 import java.util.function.Supplier;
 
 import static org.apache.iggy.serde.BytesSerializer.toBytes;
+import static org.apache.iggy.serde.BytesSerializer.toMessagesBatch;
 
 /**
  * Async TCP implementation of MessagesClient using Netty for non-blocking I/O.
@@ -181,35 +182,16 @@
     private CompletableFuture<SendMessagesResponse> sendToPartition(
             StreamId streamId, TopicId topicId, Partitioning partitioning, List<Message> messages) {
 
-        // Build metadata section following the blocking client pattern
         var metadataLength = streamId.getSize() + topicId.getSize() + partitioning.getSize() + 4;
-        var payload = Unpooled.buffer(4 + metadataLength);
+        var batch = toMessagesBatch(messages);
+        var payload = Unpooled.buffer(4 + metadataLength + batch.readableBytes());
 
-        // Write metadata length and components
         payload.writeIntLE(metadataLength);
         payload.writeBytes(toBytes(streamId));
         payload.writeBytes(toBytes(topicId));
         payload.writeBytes(toBytes(partitioning));
         payload.writeIntLE(messages.size());
-
-        // Write message index metadata (required by server)
-        var position = 0;
-        for (var message : messages) {
-            // Calculate position for next message
-            position += message.getSize();
-
-            // offset (4 bytes)
-            payload.writeIntLE(0);
-            // position (4 bytes)
-            payload.writeIntLE(position);
-            // timestamp (8 bytes)
-            payload.writeZero(8);
-        }
-
-        // Write actual message data
-        for (var message : messages) {
-            payload.writeBytes(toBytes(message));
-        }
+        payload.writeBytes(batch);
 
         return connection().send(CommandCode.Messages.SEND.getValue(), payload).thenApply(response -> {
             try {
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrHeaders.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrHeaders.java
index 356c722..35b933e 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrHeaders.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrHeaders.java
@@ -56,7 +56,7 @@
     static final int EVICTION_PROTOCOL_VERSION_MIN_OFFSET = 148;
     static final int EVICTION_REASON_OFFSET = 255;
 
-    // Command2 discriminants
+    // Command discriminants
     static final int COMMAND_REQUEST = 5;
     static final int COMMAND_REPLY = 8;
     static final int COMMAND_EVICTION = 13;
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrOperation.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrOperation.java
index 6bd6349..794dd6f 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrOperation.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/async/tcp/vsr/VsrOperation.java
@@ -66,8 +66,6 @@
     public static final int SEND_MESSAGES = 160;
     public static final int STORE_CONSUMER_OFFSET = 161;
     public static final int DELETE_CONSUMER_OFFSET = 162;
-    public static final int STORE_CONSUMER_OFFSET_2 = 164;
-    public static final int DELETE_CONSUMER_OFFSET_2 = 165;
 
     private static final int INTERNAL_START = 64;
     private static final int METADATA_START = 128;
@@ -90,8 +88,6 @@
             Map.entry(101, SEND_MESSAGES),
             Map.entry(121, STORE_CONSUMER_OFFSET),
             Map.entry(122, DELETE_CONSUMER_OFFSET),
-            Map.entry(123, STORE_CONSUMER_OFFSET_2),
-            Map.entry(124, DELETE_CONSUMER_OFFSET_2),
             Map.entry(202, CREATE_STREAM),
             Map.entry(203, DELETE_STREAM),
             Map.entry(204, UPDATE_STREAM),
@@ -119,8 +115,6 @@
         KNOWN_OPERATIONS.set(SEND_MESSAGES);
         KNOWN_OPERATIONS.set(STORE_CONSUMER_OFFSET);
         KNOWN_OPERATIONS.set(DELETE_CONSUMER_OFFSET);
-        KNOWN_OPERATIONS.set(STORE_CONSUMER_OFFSET_2);
-        KNOWN_OPERATIONS.set(DELETE_CONSUMER_OFFSET_2);
     }
 
     private VsrOperation() {}
@@ -158,11 +152,7 @@
      * section ({@code [count:u32][{index,result} x count]}).
      */
     static boolean isResultFramed(int operation) {
-        return isMetadata(operation)
-                || operation == STORE_CONSUMER_OFFSET
-                || operation == DELETE_CONSUMER_OFFSET
-                || operation == STORE_CONSUMER_OFFSET_2
-                || operation == DELETE_CONSUMER_OFFSET_2;
+        return isMetadata(operation) || operation == STORE_CONSUMER_OFFSET || operation == DELETE_CONSUMER_OFFSET;
     }
 
     /**
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/MessageHeader.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/MessageHeader.java
index 85f5868..62fc128 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/MessageHeader.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/MessageHeader.java
@@ -30,5 +30,9 @@
         Long userHeadersLength,
         Long payloadLength,
         BigInteger reserved) {
-    public static final int SIZE = 8 + 16 + 8 + 8 + 8 + 4 + 4 + 8;
+    /**
+     * Size of a message's frame header inside a batch record. The frame carries offset and
+     * timestamp deltas as u32; this record resolves them to the absolute values.
+     */
+    public static final int SIZE = 8 + 16 + 4 + 4 + 4 + 4 + 8;
 }
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesDeserializer.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesDeserializer.java
index 8a1b3c5..59b6b54 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesDeserializer.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesDeserializer.java
@@ -250,47 +250,111 @@
         var messagesCount = response.readUnsignedIntLE();
         var messages = new ArrayList<Message>();
         while (response.isReadable()) {
-            messages.add(readPolledMessage(response));
+            readBatchRecord(response, messages);
         }
         return new PolledMessages(partitionId, currentOffset, messagesCount, messages);
     }
 
-    public static Message readPolledMessage(ByteBuf response) {
-        var checksum = readU64AsBigInteger(response);
-        var id = readBytesMessageId(response);
-        var offset = readU64AsBigInteger(response);
-        var timestamp = readU64AsBigInteger(response);
-        var originTimestamp = readU64AsBigInteger(response);
-        var userHeadersLength = response.readUnsignedIntLE();
-        var payloadLength = response.readUnsignedIntLE();
-        var reserved = readU64AsBigInteger(response);
-        var header = new MessageHeader(
-                checksum, id, offset, timestamp, originTimestamp, userHeadersLength, payloadLength, reserved);
-        var payload = newByteArray(payloadLength);
-        response.readBytes(payload);
-        Map<HeaderKey, HeaderValue> userHeaders = new HashMap<>();
-        if (userHeadersLength > 0) {
-            ByteBuf userHeadersBuffer = response.readSlice(toInt(userHeadersLength));
-            Map<HeaderKey, HeaderValue> headers = new HashMap<>();
-            while (userHeadersBuffer.isReadable()) {
-                var userHeaderKeyKindCode = userHeadersBuffer.readUnsignedByte();
-                var userHeaderKeyLength = userHeadersBuffer.readUnsignedIntLE();
-                byte[] userHeaderKeyBytes = new byte[toInt(userHeaderKeyLength)];
-                userHeadersBuffer.readBytes(userHeaderKeyBytes);
-                var userHeaderKey = new HeaderKey(HeaderKind.fromCode(userHeaderKeyKindCode), userHeaderKeyBytes);
-
-                var userHeaderValueKindCode = userHeadersBuffer.readUnsignedByte();
-                var userHeaderValueLength = userHeadersBuffer.readUnsignedIntLE();
-                byte[] userHeaderValueBytes = new byte[toInt(userHeaderValueLength)];
-                userHeadersBuffer.readBytes(userHeaderValueBytes);
-                headers.put(
-                        userHeaderKey,
-                        new HeaderValue(HeaderKind.fromCode(userHeaderValueKindCode), userHeaderValueBytes));
-            }
-            userHeaders = headers;
+    /**
+     * Reads one batch record ({@code [256B batch header][frames]}) into messages with absolute
+     * offsets and timestamps. A record may be a server-sliced view of a stored batch, so the
+     * first frame's offset delta is not necessarily zero.
+     */
+    private static void readBatchRecord(ByteBuf response, List<Message> messages) {
+        if (response.readableBytes() < BytesSerializer.BATCH_HEADER_SIZE) {
+            throw new IggyMalformedResponseException(
+                    "Truncated batch header: " + response.readableBytes() + " bytes left");
         }
+        var headerStart = response.readerIndex();
+        response.skipBytes(Long.BYTES); // partition_id, already carried by the poll response header
+        var baseOffset = readU64AsBigInteger(response);
+        var baseTimestamp = readU64AsBigInteger(response);
+        var batchOriginTimestamp = readU64AsBigInteger(response);
+        var batchLength = readU64AsBigInteger(response);
+        response.readerIndex(headerStart + BytesSerializer.BATCH_HEADER_SIZE);
 
-        return new Message(header, payload, userHeaders);
+        var blobLength = batchLength.subtract(BigInteger.valueOf(BytesSerializer.BATCH_HEADER_SIZE));
+        if (blobLength.signum() < 0 || blobLength.compareTo(BigInteger.valueOf(response.readableBytes())) > 0) {
+            throw new IggyMalformedResponseException("Batch length " + batchLength + " exceeds remaining payload of "
+                    + response.readableBytes() + " bytes");
+        }
+        ByteBuf blob = response.readSlice(blobLength.intValueExact());
+        while (blob.isReadable()) {
+            messages.add(readBatchMessage(blob, baseOffset, baseTimestamp, batchOriginTimestamp));
+        }
+    }
+
+    private static Message readBatchMessage(
+            ByteBuf blob, BigInteger baseOffset, BigInteger baseTimestamp, BigInteger batchOriginTimestamp) {
+        if (blob.readableBytes() < MessageHeader.SIZE) {
+            throw new IggyMalformedResponseException(
+                    "Truncated message frame header: " + blob.readableBytes() + " bytes left");
+        }
+        var checksum = readU64AsBigInteger(blob);
+        var id = readBytesMessageId(blob);
+        var offsetDelta = blob.readUnsignedIntLE();
+        var timestampDelta = blob.readUnsignedIntLE();
+        var userHeadersLength = blob.readUnsignedIntLE();
+        var payloadLength = blob.readUnsignedIntLE();
+        var reserved = readU64AsBigInteger(blob);
+        if (reserved.signum() != 0) {
+            throw new IggyMalformedResponseException("Message frame reserved bytes must be zero");
+        }
+        if (payloadLength + userHeadersLength > blob.readableBytes()) {
+            throw new IggyMalformedResponseException("Message frame length " + (payloadLength + userHeadersLength)
+                    + " exceeds remaining batch of " + blob.readableBytes() + " bytes");
+        }
+        var header = new MessageHeader(
+                checksum,
+                id,
+                baseOffset.add(BigInteger.valueOf(offsetDelta)),
+                baseTimestamp,
+                batchOriginTimestamp.add(BigInteger.valueOf(timestampDelta)),
+                userHeadersLength,
+                payloadLength,
+                BigInteger.ZERO);
+        var payload = newByteArray(payloadLength);
+        blob.readBytes(payload);
+        return new Message(header, payload, readUserHeaders(blob, userHeadersLength));
+    }
+
+    /**
+     * User headers ride the frame as opaque bytes, so another SDK may put non-TLV data there;
+     * such bytes decode to an empty map while {@code userHeadersLength} still reports them.
+     */
+    private static Map<HeaderKey, HeaderValue> readUserHeaders(ByteBuf frame, Long userHeadersLength) {
+        Map<HeaderKey, HeaderValue> userHeaders = new HashMap<>();
+        if (userHeadersLength == 0) {
+            return userHeaders;
+        }
+        ByteBuf slice = frame.readSlice(toInt(userHeadersLength));
+        while (slice.isReadable()) {
+            var key = readUserHeaderField(slice);
+            var value = key == null ? null : readUserHeaderField(slice);
+            if (value == null) {
+                return new HashMap<>();
+            }
+            userHeaders.put(new HeaderKey(key.kind(), key.value()), new HeaderValue(value.kind(), value.value()));
+        }
+        return userHeaders;
+    }
+
+    private static UserHeaderField readUserHeaderField(ByteBuf slice) {
+        if (slice.readableBytes() < 1 + Integer.BYTES) {
+            return null;
+        }
+        var kindCode = slice.readUnsignedByte();
+        var length = slice.readUnsignedIntLE();
+        if (length > slice.readableBytes()) {
+            return null;
+        }
+        byte[] value = newByteArray(length);
+        slice.readBytes(value);
+        try {
+            return new UserHeaderField(HeaderKind.fromCode(kindCode), value);
+        } catch (IggyInvalidArgumentException unknownKind) {
+            return null;
+        }
     }
 
     public static Stats readStats(ByteBuf response) {
@@ -695,4 +759,6 @@
     private static byte[] newByteArray(Long size) {
         return new byte[size.intValue()];
     }
+
+    private record UserHeaderField(HeaderKind kind, byte[] value) {}
 }
diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java
index 8448e50..6a67b13 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/BytesSerializer.java
@@ -19,6 +19,7 @@
 
 package org.apache.iggy.serde;
 
+import com.dynatrace.hash4j.hashing.Hashing;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import org.apache.commons.lang3.ArrayUtils;
@@ -29,8 +30,10 @@
 import org.apache.iggy.message.HeaderValue;
 import org.apache.iggy.message.Message;
 import org.apache.iggy.message.MessageHeader;
+import org.apache.iggy.message.MessageId;
 import org.apache.iggy.message.Partitioning;
 import org.apache.iggy.message.PollingStrategy;
+import org.apache.iggy.message.UuidMessageId;
 import org.apache.iggy.user.GlobalPermissions;
 import org.apache.iggy.user.Permissions;
 import org.apache.iggy.user.StreamPermissions;
@@ -38,8 +41,11 @@
 
 import java.math.BigInteger;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.UUID;
 
 /**
  * Unified serializer for both blocking and async clients.
@@ -47,6 +53,9 @@
  */
 public final class BytesSerializer {
 
+    /** Size of the batch header on the wire; bytes past the stamped fields stay zero. */
+    static final int BATCH_HEADER_SIZE = 256;
+
     /**
      * Key and value length bound, in encoded bytes rather than characters. Belongs to the
      * header-field codec that both user headers and resource options ride, so the server refuses a
@@ -54,6 +63,12 @@
      */
     private static final int MAX_HEADER_FIELD_LENGTH = 255;
 
+    /** The timestamp delta is a u32 microsecond offset from the batch origin timestamp. */
+    private static final BigInteger MAX_TIMESTAMP_DELTA_MICROS = BigInteger.valueOf(0xFFFF_FFFFL);
+
+    /** Batch checksum input: five u64 header fields plus the u32 message count. */
+    private static final int BATCH_CHECKSUM_FIXED_INPUT_BYTES = 5 * Long.BYTES + Integer.BYTES;
+
     private BytesSerializer() {}
 
     public static ByteBuf toBytes(Consumer consumer) {
@@ -89,27 +104,6 @@
         return buffer;
     }
 
-    public static ByteBuf toBytes(Message message) {
-        var buffer = Unpooled.buffer(message.getSize());
-        buffer.writeBytes(toBytes(message.header()));
-        buffer.writeBytes(message.payload());
-        buffer.writeBytes(toBytes(message.userHeaders()));
-        return buffer;
-    }
-
-    public static ByteBuf toBytes(MessageHeader header) {
-        var buffer = Unpooled.buffer(MessageHeader.SIZE);
-        buffer.writeBytes(toBytesAsU64(header.checksum()));
-        buffer.writeBytes(header.id().toBytes());
-        buffer.writeBytes(toBytesAsU64(header.offset()));
-        buffer.writeBytes(toBytesAsU64(header.timestamp()));
-        buffer.writeBytes(toBytesAsU64(header.originTimestamp()));
-        buffer.writeIntLE(header.userHeadersLength().intValue());
-        buffer.writeIntLE(header.payloadLength().intValue());
-        buffer.writeBytes(toBytesAsU64(header.reserved()));
-        return buffer;
-    }
-
     public static ByteBuf toBytes(PollingStrategy strategy) {
         var buffer = Unpooled.buffer(9);
         buffer.writeByte(strategy.kind().asCode());
@@ -259,6 +253,114 @@
     }
 
     /**
+     * Encodes messages as one batch record: a batch header followed by per-message frames.
+     * The server stamps {@code partition_id}, {@code base_offset}, and {@code base_timestamp},
+     * so they are encoded as zero here.
+     */
+    public static ByteBuf toMessagesBatch(List<Message> messages) {
+        if (messages.isEmpty()) {
+            throw new IggyInvalidArgumentException("Cannot encode an empty message batch");
+        }
+        List<RawMessage> rawMessages = new ArrayList<>(messages.size());
+        for (Message message : messages) {
+            rawMessages.add(new RawMessage(
+                    encodedMessageId(message.header().id()),
+                    message.header().originTimestamp(),
+                    message.payload(),
+                    readAllBytes(toBytes(message.userHeaders()))));
+        }
+        return encodeBatch(rawMessages);
+    }
+
+    static ByteBuf encodeBatch(List<RawMessage> messages) {
+        var batchOriginTimestamp = messages.stream()
+                .map(RawMessage::originTimestamp)
+                .min(BigInteger::compareTo)
+                .orElseThrow(() -> new IggyInvalidArgumentException("Cannot encode an empty message batch"));
+        var blobLength = 0;
+        for (RawMessage message : messages) {
+            blobLength += MessageHeader.SIZE + message.payload().length + message.userHeaders().length;
+        }
+
+        var batch = Unpooled.buffer(BATCH_HEADER_SIZE + blobLength);
+        batch.writeZero(BATCH_HEADER_SIZE);
+        for (int index = 0; index < messages.size(); index++) {
+            RawMessage message = messages.get(index);
+            var timestampDelta = message.originTimestamp().subtract(batchOriginTimestamp);
+            if (timestampDelta.compareTo(MAX_TIMESTAMP_DELTA_MICROS) > 0) {
+                throw new IggyInvalidArgumentException("Message origin timestamp exceeds the batch origin by "
+                        + timestampDelta + " microseconds, more than the timestamp delta field can hold");
+            }
+            var frameStart = batch.writerIndex();
+            batch.writeLongLE(0); // checksum, backpatched below
+            batch.writeBytes(message.id());
+            batch.writeIntLE(index); // offset_delta
+            batch.writeIntLE(timestampDelta.intValue());
+            batch.writeIntLE(message.userHeaders().length);
+            batch.writeIntLE(message.payload().length);
+            batch.writeLongLE(0); // reserved
+            batch.writeBytes(message.payload());
+            batch.writeBytes(message.userHeaders());
+            batch.setLongLE(
+                    frameStart, xxHash3(batch, frameStart + Long.BYTES, batch.writerIndex() - frameStart - Long.BYTES));
+        }
+
+        long batchLength = BATCH_HEADER_SIZE + blobLength;
+        batch.setBytes(24, toBytesAsU64(batchOriginTimestamp));
+        batch.setLongLE(32, batchLength);
+        batch.setLongLE(40, batchChecksum(batch, batchOriginTimestamp, batchLength, messages));
+        batch.setIntLE(48, messages.size());
+        return batch;
+    }
+
+    /**
+     * The batch checksum covers the header meta fields and each frame's checksum field, not the
+     * message bodies; bodies are bound through the per-frame checksums.
+     */
+    private static long batchChecksum(
+            ByteBuf batch, BigInteger batchOriginTimestamp, long batchLength, List<RawMessage> messages) {
+        var input = Unpooled.buffer(BATCH_CHECKSUM_FIXED_INPUT_BYTES + Long.BYTES * messages.size());
+        input.writeLongLE(0); // partition_id
+        input.writeLongLE(0); // base_offset
+        input.writeLongLE(0); // base_timestamp
+        input.writeBytes(toBytesAsU64(batchOriginTimestamp));
+        input.writeLongLE(batchLength);
+        input.writeIntLE(messages.size());
+        var frameStart = BATCH_HEADER_SIZE;
+        for (RawMessage message : messages) {
+            input.writeLongLE(batch.getLongLE(frameStart));
+            frameStart += MessageHeader.SIZE + message.payload().length + message.userHeaders().length;
+        }
+        return xxHash3(input, 0, input.readableBytes());
+    }
+
+    /**
+     * The frame checksum covers the id, so a zero id is minted client-side before encoding
+     * rather than assigned by the server.
+     */
+    private static byte[] encodedMessageId(MessageId id) {
+        if (id.toBigInteger().signum() == 0) {
+            return readAllBytes(new UuidMessageId(UUID.randomUUID()).toBytes());
+        }
+        return readAllBytes(id.toBytes());
+    }
+
+    private static long xxHash3(ByteBuf buffer, int index, int length) {
+        if (buffer.hasArray()) {
+            return Hashing.xxh3_64().hashBytesToLong(buffer.array(), buffer.arrayOffset() + index, length);
+        }
+        var bytes = new byte[length];
+        buffer.getBytes(index, bytes);
+        return Hashing.xxh3_64().hashBytesToLong(bytes);
+    }
+
+    private static byte[] readAllBytes(ByteBuf buffer) {
+        var bytes = new byte[buffer.readableBytes()];
+        buffer.readBytes(bytes);
+        return bytes;
+    }
+
+    /**
      * Rejects a key or value the TLV codec cannot express.
      *
      * <p>The {@code HeaderKey} / {@code HeaderValue} factories bound what they build, but both are
@@ -272,4 +374,16 @@
                     + " bytes, must be between 1 and " + MAX_HEADER_FIELD_LENGTH);
         }
     }
+
+    /**
+     * One message as it enters the batch encoder: the id already encoded to its 16 wire bytes
+     * and the user headers already encoded to their opaque bytes.
+     */
+    record RawMessage(byte[] id, BigInteger originTimestamp, byte[] payload, byte[] userHeaders) {
+        RawMessage {
+            if (id.length != 16) {
+                throw new IggyInvalidArgumentException("Message id must have 16 bytes");
+            }
+        }
+    }
 }
diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/BaseIntegrationTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/BaseIntegrationTest.java
index fd24f17..7c94728 100644
--- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/BaseIntegrationTest.java
+++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/BaseIntegrationTest.java
@@ -58,11 +58,18 @@
     private static final boolean USE_EXTERNAL_SERVER = System.getenv("USE_EXTERNAL_SERVER") != null;
 
     public static int serverTcpPort() {
-        return USE_EXTERNAL_SERVER ? TCP_PORT : iggyServer.getMappedPort(TCP_PORT);
+        return USE_EXTERNAL_SERVER ? externalPort("EXTERNAL_TCP_PORT", TCP_PORT) : iggyServer.getMappedPort(TCP_PORT);
     }
 
     public static int serverHttpPort() {
-        return USE_EXTERNAL_SERVER ? HTTP_PORT : iggyServer.getMappedPort(HTTP_PORT);
+        return USE_EXTERNAL_SERVER
+                ? externalPort("EXTERNAL_HTTP_PORT", HTTP_PORT)
+                : iggyServer.getMappedPort(HTTP_PORT);
+    }
+
+    private static int externalPort(String variable, int defaultPort) {
+        var configured = System.getenv(variable);
+        return configured != null ? Integer.parseInt(configured) : defaultPort;
     }
 
     public static String serverHost() {
diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/BytesSerializerTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/BytesSerializerTest.java
index 13893f0..091ffc1 100644
--- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/BytesSerializerTest.java
+++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/BytesSerializerTest.java
@@ -25,11 +25,8 @@
 import org.apache.iggy.exception.IggyInvalidArgumentException;
 import org.apache.iggy.identifier.ConsumerId;
 import org.apache.iggy.identifier.StreamId;
-import org.apache.iggy.message.BytesMessageId;
 import org.apache.iggy.message.HeaderKey;
 import org.apache.iggy.message.HeaderValue;
-import org.apache.iggy.message.Message;
-import org.apache.iggy.message.MessageHeader;
 import org.apache.iggy.message.Partitioning;
 import org.apache.iggy.message.PollingStrategy;
 import org.apache.iggy.serde.BytesSerializer;
@@ -492,105 +489,6 @@
     }
 
     @Nested
-    class MessageSerialization {
-
-        @Test
-        void shouldSerializeMessageWithoutUserHeaders() {
-            // given
-            var messageId = new BytesMessageId(new byte[16]);
-            var header = new MessageHeader(
-                    BigInteger.valueOf(123), // checksum
-                    messageId,
-                    BigInteger.valueOf(0), // offset
-                    BigInteger.valueOf(1000), // timestamp
-                    BigInteger.valueOf(1000), // originTimestamp
-                    0L, // userHeadersLength
-                    5L, // payloadLength
-                    BigInteger.ZERO // reserved
-                    );
-            byte[] payload = "hello".getBytes();
-            var message = new Message(header, payload, new HashMap<>());
-
-            // when
-            ByteBuf result = BytesSerializer.toBytes(message);
-
-            // then
-            assertThat(result.readableBytes()).isEqualTo(MessageHeader.SIZE + 5); // header + payload, no user headers
-        }
-
-        @Test
-        void shouldSerializeMessageWithUserHeaders() {
-            // given
-            var messageId = new BytesMessageId(new byte[16]);
-            Map<HeaderKey, HeaderValue> userHeaders = new HashMap<>();
-            userHeaders.put(HeaderKey.fromString("key"), HeaderValue.fromRaw("val".getBytes()));
-
-            // Calculate user headers size
-            ByteBuf headersBuf = BytesSerializer.toBytes(userHeaders);
-            int userHeadersLength = headersBuf.readableBytes();
-
-            var header = new MessageHeader(
-                    BigInteger.ZERO,
-                    messageId,
-                    BigInteger.ZERO,
-                    BigInteger.valueOf(1000),
-                    BigInteger.valueOf(1000),
-                    (long) userHeadersLength,
-                    3L, // "abc".length()
-                    BigInteger.ZERO // reserved
-                    );
-            byte[] payload = "abc".getBytes();
-            var message = new Message(header, payload, userHeaders);
-
-            // when
-            ByteBuf result = BytesSerializer.toBytes(message);
-
-            // then
-            assertThat(result.readableBytes()).isEqualTo(MessageHeader.SIZE + 3 + userHeadersLength);
-        }
-    }
-
-    @Nested
-    class MessageHeaderSerialization {
-
-        @Test
-        void shouldSerializeMessageHeader() {
-            // given
-            var messageId = new BytesMessageId(new byte[16]);
-            var header = new MessageHeader(
-                    BigInteger.valueOf(999), // checksum
-                    messageId,
-                    BigInteger.valueOf(42), // offset
-                    BigInteger.valueOf(2000), // timestamp
-                    BigInteger.valueOf(1999), // originTimestamp
-                    10L, // userHeadersLength
-                    100L, // payloadLength
-                    BigInteger.ZERO // reserved
-                    );
-
-            // when
-            ByteBuf result = BytesSerializer.toBytes(header);
-
-            // then
-            assertThat(result.readableBytes()).isEqualTo(MessageHeader.SIZE);
-            // Read checksum (8 bytes)
-            result.skipBytes(8);
-            // Read message ID (16 bytes)
-            result.skipBytes(16);
-            // Read offset (8 bytes)
-            result.skipBytes(8);
-            // Read timestamp (8 bytes)
-            result.skipBytes(8);
-            // Read origin timestamp (8 bytes)
-            result.skipBytes(8);
-            // Read user headers length (4 bytes)
-            assertThat(result.readIntLE()).isEqualTo(10);
-            // Read payload length (4 bytes)
-            assertThat(result.readIntLE()).isEqualTo(100);
-        }
-    }
-
-    @Nested
     class PermissionsSerialization {
 
         @Test
diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/message/MessageTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/message/MessageTest.java
index ba6b992..48cb5a8 100644
--- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/message/MessageTest.java
+++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/message/MessageTest.java
@@ -145,14 +145,14 @@
     void getSizeReturnsExpectedSizeWhenThereAreNoUserHeaders() {
         var message = Message.of("foo");
 
-        assertThat(message.getSize()).isEqualTo(67);
+        assertThat(message.getSize()).isEqualTo(51);
     }
 
     @Test
     void getSizeReturnsExpectedSizeWhenThereAreUserHeaders() {
         var message = Message.of("foo", Map.of(HeaderKey.fromString("k1"), HeaderValue.fromInt32(123)));
 
-        assertThat(message.getSize()).isEqualTo(83);
+        assertThat(message.getSize()).isEqualTo(67);
     }
 
     private void assertDefaultMessageHeaderValues(MessageHeader header) {
diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
index d36cdb5..56e05eb 100644
--- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
+++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
@@ -53,8 +53,6 @@
 import static org.apache.iggy.serde.BytesDeserializer.readPartition;
 import static org.apache.iggy.serde.BytesDeserializer.readPermissions;
 import static org.apache.iggy.serde.BytesDeserializer.readPersonalAccessTokenInfo;
-import static org.apache.iggy.serde.BytesDeserializer.readPolledMessage;
-import static org.apache.iggy.serde.BytesDeserializer.readPolledMessages;
 import static org.apache.iggy.serde.BytesDeserializer.readRawPersonalAccessToken;
 import static org.apache.iggy.serde.BytesDeserializer.readSendMessagesResponse;
 import static org.apache.iggy.serde.BytesDeserializer.readStats;
@@ -445,96 +443,6 @@
     }
 
     @Nested
-    class MessageDeserialization {
-
-        @Test
-        void shouldDeserializePolledMessageWithoutUserHeaders() {
-            // given
-            ByteBuf buffer = Unpooled.buffer();
-            writeU64(buffer, BigInteger.valueOf(123)); // checksum
-            buffer.writeBytes(new byte[16]); // message ID
-            writeU64(buffer, BigInteger.ZERO); // offset
-            writeU64(buffer, BigInteger.valueOf(1000)); // timestamp
-            writeU64(buffer, BigInteger.valueOf(1000)); // origin timestamp
-            buffer.writeIntLE(0); // user headers length
-            buffer.writeIntLE(5); // payload length
-            writeU64(buffer, BigInteger.ZERO); // reserved
-            buffer.writeBytes("hello".getBytes()); // payload
-
-            // when
-            var message = readPolledMessage(buffer);
-
-            // then
-            assertThat(message.header().checksum()).isEqualTo(BigInteger.valueOf(123));
-            assertThat(message.header().payloadLength()).isEqualTo(5L);
-            assertThat(message.payload()).isEqualTo("hello".getBytes());
-            assertThat(message.userHeaders()).isEmpty();
-        }
-
-        @Test
-        void shouldDeserializePolledMessageWithUserHeaders() {
-            // given
-            ByteBuf buffer = Unpooled.buffer();
-            writeU64(buffer, BigInteger.ZERO);
-            buffer.writeBytes(new byte[16]);
-            writeU64(buffer, BigInteger.ZERO);
-            writeU64(buffer, BigInteger.valueOf(1000));
-            writeU64(buffer, BigInteger.valueOf(1000));
-
-            // Calculate and write user headers
-            ByteBuf headersBuffer = Unpooled.buffer();
-            headersBuffer.writeByte(HeaderKind.String.asCode());
-            headersBuffer.writeIntLE(3);
-            headersBuffer.writeBytes("key".getBytes());
-            headersBuffer.writeByte(HeaderKind.Raw.asCode());
-            headersBuffer.writeIntLE(3);
-            headersBuffer.writeBytes("val".getBytes());
-
-            buffer.writeIntLE(headersBuffer.readableBytes()); // user headers length
-            buffer.writeIntLE(3); // payload length
-            writeU64(buffer, BigInteger.ZERO); // reserved
-            buffer.writeBytes("abc".getBytes()); // payload
-            buffer.writeBytes(headersBuffer); // user headers
-
-            // when
-            var message = readPolledMessage(buffer);
-
-            // then
-            assertThat(message.userHeaders()).hasSize(1);
-            assertThat(message.userHeaders().get(HeaderKey.fromString("key")).asRaw())
-                    .isEqualTo("val".getBytes());
-        }
-
-        @Test
-        void shouldDeserializePolledMessages() {
-            // given
-            ByteBuf buffer = Unpooled.buffer();
-            buffer.writeIntLE(1); // partition ID
-            writeU64(buffer, BigInteger.valueOf(10)); // current offset
-            buffer.writeIntLE(1); // messages count
-            // Write one message
-            writeU64(buffer, BigInteger.ZERO);
-            buffer.writeBytes(new byte[16]);
-            writeU64(buffer, BigInteger.ZERO);
-            writeU64(buffer, BigInteger.valueOf(1000));
-            writeU64(buffer, BigInteger.valueOf(1000));
-            buffer.writeIntLE(0);
-            buffer.writeIntLE(2);
-            writeU64(buffer, BigInteger.ZERO); // reserved
-            buffer.writeBytes("hi".getBytes());
-
-            // when
-            var polledMessages = readPolledMessages(buffer);
-
-            // then
-            assertThat(polledMessages.partitionId()).isEqualTo(1L);
-            assertThat(polledMessages.currentOffset()).isEqualTo(BigInteger.valueOf(10));
-            assertThat(polledMessages.count()).isEqualTo(1L);
-            assertThat(polledMessages.messages()).hasSize(1);
-        }
-    }
-
-    @Nested
     class SendMessagesResponseDeserialization {
 
         private ByteBuf singleConfirmation() {
diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/MessagesBatchWireFormatTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/MessagesBatchWireFormatTest.java
new file mode 100644
index 0000000..056e563
--- /dev/null
+++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/MessagesBatchWireFormatTest.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iggy.serde;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import org.apache.iggy.exception.IggyInvalidArgumentException;
+import org.apache.iggy.exception.IggyMalformedResponseException;
+import org.apache.iggy.message.BigIntegerMessageId;
+import org.apache.iggy.message.HeaderKey;
+import org.apache.iggy.message.HeaderValue;
+import org.apache.iggy.message.Message;
+import org.apache.iggy.message.MessageHeader;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Golden vectors generated by the Rust implementation: the batch encoded for
+ * {@code SendMessages} and the poll body serving it back.
+ */
+class MessagesBatchWireFormatTest {
+
+    private static final String PRODUCE_BATCH_ONLY =
+            "000000000000000000000000000000000000000000000000e8030000000000008c01000000000000a91f38c86307267c02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573";
+
+    private static final String POLL_BODY =
+            "03000000650000000000000002000000030000000000000064000000000000008813000000000000e8030000000000008c01000000000000c96826b38a8feed202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000bfd2b9205a759675070000000000000000000000000000000000000000000000000000000d000000000000000000000066697273742d7061796c6f6164d66b7e1c758eb7c0080000000000000000000000000000000100000032000000110000000e00000000000000000000007365636f6e642d7061796c6f6164757365722d6865616465722d6279746573";
+
+    @Test
+    void shouldEncodeGoldenProduceBatch() {
+        var messages = List.of(
+                new BytesSerializer.RawMessage(
+                        idBytes(7),
+                        BigInteger.valueOf(1000),
+                        "first-payload".getBytes(StandardCharsets.UTF_8),
+                        new byte[0]),
+                new BytesSerializer.RawMessage(
+                        idBytes(8),
+                        BigInteger.valueOf(1050),
+                        "second-payload".getBytes(StandardCharsets.UTF_8),
+                        "user-header-bytes".getBytes(StandardCharsets.UTF_8)));
+
+        var batch = BytesSerializer.encodeBatch(messages);
+
+        assertThat(ByteBufUtil.hexDump(batch)).isEqualTo(PRODUCE_BATCH_ONLY);
+    }
+
+    @Test
+    void shouldDecodeGoldenPollBody() {
+        var polled = BytesDeserializer.readPolledMessages(Unpooled.wrappedBuffer(ByteBufUtil.decodeHexDump(POLL_BODY)));
+
+        assertThat(polled.partitionId()).isEqualTo(3L);
+        assertThat(polled.currentOffset()).isEqualTo(BigInteger.valueOf(101));
+        assertThat(polled.count()).isEqualTo(2L);
+        assertThat(polled.messages()).hasSize(2);
+
+        var first = polled.messages().get(0);
+        assertThat(first.header().offset()).isEqualTo(BigInteger.valueOf(100));
+        assertThat(first.header().timestamp()).isEqualTo(BigInteger.valueOf(5000));
+        assertThat(first.header().originTimestamp()).isEqualTo(BigInteger.valueOf(1000));
+        assertThat(first.header().id().toBigInteger()).isEqualTo(BigInteger.valueOf(7));
+        assertThat(first.header().checksum()).isEqualTo(new BigInteger("7596755a20b9d2bf", 16));
+        assertThat(first.payload()).isEqualTo("first-payload".getBytes(StandardCharsets.UTF_8));
+        assertThat(first.userHeaders()).isEmpty();
+
+        var second = polled.messages().get(1);
+        assertThat(second.header().offset()).isEqualTo(BigInteger.valueOf(101));
+        assertThat(second.header().timestamp()).isEqualTo(BigInteger.valueOf(5000));
+        assertThat(second.header().originTimestamp()).isEqualTo(BigInteger.valueOf(1050));
+        assertThat(second.header().id().toBigInteger()).isEqualTo(BigInteger.valueOf(8));
+        assertThat(second.header().checksum()).isEqualTo(new BigInteger("c0b78e751c7e6bd6", 16));
+        assertThat(second.payload()).isEqualTo("second-payload".getBytes(StandardCharsets.UTF_8));
+        // The golden vector carries opaque non-TLV user header bytes: reported
+        // by length, decoded to no entries.
+        assertThat(second.header().userHeadersLength()).isEqualTo(17L);
+        assertThat(second.userHeaders()).isEmpty();
+    }
+
+    @Test
+    void shouldRoundTripBatchThroughPollDecoding() {
+        var userHeaders =
+                Map.of(HeaderKey.fromString("k1"), HeaderValue.fromRaw("v1".getBytes(StandardCharsets.UTF_8)));
+        var batch = BytesSerializer.toMessagesBatch(
+                List.of(message(100, 2000, "first", Map.of()), message(200, 2500, "second", userHeaders)));
+
+        var polled = BytesDeserializer.readPolledMessages(pollBody(9, 1, 2, batch));
+
+        assertThat(polled.messages()).hasSize(2);
+        var first = polled.messages().get(0);
+        assertThat(first.header().id().toBigInteger()).isEqualTo(BigInteger.valueOf(100));
+        assertThat(first.header().offset()).isEqualTo(BigInteger.ZERO);
+        assertThat(first.header().originTimestamp()).isEqualTo(BigInteger.valueOf(2000));
+        assertThat(first.payload()).isEqualTo("first".getBytes(StandardCharsets.UTF_8));
+        assertThat(first.userHeaders()).isEmpty();
+        var second = polled.messages().get(1);
+        assertThat(second.header().id().toBigInteger()).isEqualTo(BigInteger.valueOf(200));
+        assertThat(second.header().offset()).isEqualTo(BigInteger.ONE);
+        assertThat(second.header().originTimestamp()).isEqualTo(BigInteger.valueOf(2500));
+        assertThat(second.payload()).isEqualTo("second".getBytes(StandardCharsets.UTF_8));
+        assertThat(second.userHeaders().get(HeaderKey.fromString("k1")).asRaw())
+                .isEqualTo("v1".getBytes(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    void shouldMintIdsForZeroIdMessages() {
+        var batch = BytesSerializer.toMessagesBatch(List.of(Message.of("a"), Message.of("b")));
+
+        var polled = BytesDeserializer.readPolledMessages(pollBody(1, 0, 2, batch));
+
+        var firstId = polled.messages().get(0).header().id().toBigInteger();
+        var secondId = polled.messages().get(1).header().id().toBigInteger();
+        assertThat(firstId).isNotEqualTo(BigInteger.ZERO);
+        assertThat(secondId).isNotEqualTo(BigInteger.ZERO);
+        assertThat(firstId).isNotEqualTo(secondId);
+    }
+
+    @Test
+    void shouldRejectEmptyBatch() {
+        assertThatThrownBy(() -> BytesSerializer.toMessagesBatch(List.of()))
+                .isInstanceOf(IggyInvalidArgumentException.class);
+    }
+
+    @Test
+    void shouldRejectTimestampDeltaExceedingU32() {
+        var messages = List.of(message(1, 0, "a", Map.of()), message(2, 0x1_0000_0000L, "b", Map.of()));
+
+        assertThatThrownBy(() -> BytesSerializer.toMessagesBatch(messages))
+                .isInstanceOf(IggyInvalidArgumentException.class);
+    }
+
+    @Test
+    void shouldRejectNonZeroFrameReserved() {
+        var body = ByteBufUtil.decodeHexDump(POLL_BODY);
+        // Poll header (16) + batch header (256) + reserved offset inside the first frame (40).
+        body[16 + 256 + 40] = 1;
+
+        assertThatThrownBy(() -> BytesDeserializer.readPolledMessages(Unpooled.wrappedBuffer(body)))
+                .isInstanceOf(IggyMalformedResponseException.class);
+    }
+
+    private static byte[] idBytes(long id) {
+        return ByteBufUtil.getBytes(BytesSerializer.toBytesAsU128(BigInteger.valueOf(id)));
+    }
+
+    private static Message message(
+            long id, long originTimestamp, String payload, Map<HeaderKey, HeaderValue> userHeaders) {
+        var payloadBytes = payload.getBytes(StandardCharsets.UTF_8);
+        var header = new MessageHeader(
+                BigInteger.ZERO,
+                new BigIntegerMessageId(BigInteger.valueOf(id)),
+                BigInteger.ZERO,
+                BigInteger.ZERO,
+                BigInteger.valueOf(originTimestamp),
+                0L,
+                (long) payloadBytes.length,
+                BigInteger.ZERO);
+        return new Message(header, payloadBytes, userHeaders);
+    }
+
+    private static ByteBuf pollBody(long partitionId, long currentOffset, long count, ByteBuf batch) {
+        var body = Unpooled.buffer(16 + batch.readableBytes());
+        body.writeIntLE((int) partitionId);
+        body.writeLongLE(currentOffset);
+        body.writeIntLE((int) count);
+        body.writeBytes(batch);
+        return body;
+    }
+}
diff --git a/foreign/node/package-lock.json b/foreign/node/package-lock.json
index e58a7d5..d5445bd 100644
--- a/foreign/node/package-lock.json
+++ b/foreign/node/package-lock.json
@@ -1,14 +1,15 @@
 {
   "name": "apache-iggy",
-  "version": "0.10.0-edge.3",
+  "version": "0.10.0-edge.4",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "apache-iggy",
-      "version": "0.10.0-edge.3",
+      "version": "0.10.0-edge.4",
       "license": "Apache-2.0",
       "dependencies": {
+        "@node-rs/xxhash": "1.7.6",
         "debug": "4.4.3",
         "generic-pool": "3.9.0",
         "uuidv7": "1.2.1"
@@ -608,7 +609,6 @@
       "version": "1.11.3",
       "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
       "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
@@ -620,7 +620,6 @@
       "version": "1.11.3",
       "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
       "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
@@ -631,7 +630,6 @@
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
       "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
@@ -863,7 +861,6 @@
       "version": "0.2.12",
       "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
       "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
@@ -876,7 +873,6 @@
       "version": "1.7.6",
       "resolved": "https://registry.npmjs.org/@node-rs/xxhash/-/xxhash-1.7.6.tgz",
       "integrity": "sha512-XMisO+aQHsVpxRp/85EszTtOQTOlhPbd149P/Xa9F55wafA6UM3h2UhOgOs7aAzItnHU/Aw1WQ1FVTEg7WB43Q==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">= 12"
@@ -909,7 +905,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -926,7 +921,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -943,7 +937,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -960,7 +953,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -977,7 +969,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -994,7 +985,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1011,7 +1001,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1028,7 +1017,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1045,7 +1033,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1062,7 +1049,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1079,7 +1065,6 @@
       "cpu": [
         "wasm32"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
@@ -1096,7 +1081,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1113,7 +1097,6 @@
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1130,7 +1113,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "os": [
@@ -1822,7 +1804,6 @@
       "version": "0.10.3",
       "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
       "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
@@ -4225,7 +4206,7 @@
       "version": "2.8.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
       "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
-      "dev": true,
+      "devOptional": true,
       "license": "0BSD"
     },
     "node_modules/type-check": {
diff --git a/foreign/node/package.json b/foreign/node/package.json
index c906d5c..e31cee1 100644
--- a/foreign/node/package.json
+++ b/foreign/node/package.json
@@ -1,7 +1,7 @@
 {
   "name": "apache-iggy",
   "type": "module",
-  "version": "0.10.0-edge.3",
+  "version": "0.10.0-edge.4",
   "description": "Official Apache Iggy NodeJS SDK",
   "keywords": [
     "iggy",
@@ -50,6 +50,7 @@
   "author": "github.com/T1B0",
   "license": "Apache-2.0",
   "dependencies": {
+    "@node-rs/xxhash": "1.7.6",
     "debug": "4.4.3",
     "generic-pool": "3.9.0",
     "uuidv7": "1.2.1"
diff --git a/foreign/node/scripts/check-vsr-protocol.mjs b/foreign/node/scripts/check-vsr-protocol.mjs
index ebfcf66..dcbe060 100644
--- a/foreign/node/scripts/check-vsr-protocol.mjs
+++ b/foreign/node/scripts/check-vsr-protocol.mjs
@@ -150,22 +150,22 @@
   'Node EvictionReason differs from the Rust consensus enum'
 );
 
-const rustCommand2Block =
-  rustCommand.match(/pub enum Command2 \{([\s\S]*?)\n\}/)?.[1] ?? '';
-const rustCommand2 = enumValues(
-  rustCommand2Block,
+const rustCommandBlock =
+  rustCommand.match(/pub enum Command \{([\s\S]*?)\n\}/)?.[1] ?? '';
+const rustCommandTable = enumValues(
+  rustCommandBlock,
   /^\s+([A-Za-z0-9]+)\s*=\s*([0-9]+),$/gm
 );
-const nodeCommand2 = enumValues(
-  nodeHeader.match(/export const Command2 = \{([\s\S]*?)\n\}/)?.[1] ?? '',
+const nodeCommandTable = enumValues(
+  nodeHeader.match(/export const Command = \{([\s\S]*?)\n\}/)?.[1] ?? '',
   /^\s+([A-Za-z0-9]+):\s*([0-9]+),?$/gm
 );
-assert.ok(nodeCommand2.size > 0, 'Node Command2 table was not found');
-for (const [name, value] of nodeCommand2)
+assert.ok(nodeCommandTable.size > 0, 'Node Command table was not found');
+for (const [name, value] of nodeCommandTable)
   assert.equal(
-    rustCommand2.get(name),
+    rustCommandTable.get(name),
     value,
-    `Node Command2.${name} differs from Rust`
+    `Node Command.${name} differs from Rust`
   );
 
 const rustHeaderSize = Number(
@@ -185,7 +185,7 @@
   ['u32', [4, 4]],
   ['u64', [8, 8]],
   ['u128', [16, 16]],
-  ['Command2', [1, 1]],
+  ['Command', [1, 1]],
   ['Operation', [1, 1]],
   ['EvictionReason', [1, 1]]
 ]);
diff --git a/foreign/node/src/client/client.connection.test.ts b/foreign/node/src/client/client.connection.test.ts
index 5c11c54..2564cfa 100644
--- a/foreign/node/src/client/client.connection.test.ts
+++ b/foreign/node/src/client/client.connection.test.ts
@@ -28,7 +28,7 @@
 import { ProtocolFrameError } from './client.frame.js';
 import { IggyConnection } from './client.connection.js';
 import type { ClientConfig } from './client.type.js';
-import { Command2, HEADER_SIZE, REPLY_OFFSET } from '../wire/vsr/header.js';
+import { Command, HEADER_SIZE, REPLY_OFFSET } from '../wire/vsr/header.js';
 
 const FRAME_LIMIT = 2 * HEADER_SIZE;
 
@@ -53,7 +53,7 @@
 const replyFrame = (body: Buffer): Buffer => {
   const frame = Buffer.alloc(HEADER_SIZE + body.length);
   frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
-  frame.writeUInt8(Command2.Reply, REPLY_OFFSET.command);
+  frame.writeUInt8(Command.Reply, REPLY_OFFSET.command);
   body.copy(frame, HEADER_SIZE);
   return frame;
 };
diff --git a/foreign/node/src/client/client.connection.ts b/foreign/node/src/client/client.connection.ts
index 99bcda9..10b6863 100644
--- a/foreign/node/src/client/client.connection.ts
+++ b/foreign/node/src/client/client.connection.ts
@@ -27,7 +27,7 @@
   ProtocolFrameError,
   ResponseFrameDecoder
 } from './client.frame.js';
-import { Command2, peekCommand } from '../wire/vsr/header.js';
+import { Command, peekCommand } from '../wire/vsr/header.js';
 import { evictionError } from '../wire/vsr/reply.js';
 
 
@@ -425,7 +425,7 @@
 
     try {
       for (const response of this.responseDecoder.push(data)) {
-        if (peekCommand(response) === Command2.Eviction)
+        if (peekCommand(response) === Command.Eviction)
           this.emit('eviction', evictionError(response));
         else
           this.emit('response', response);
diff --git a/foreign/node/src/client/client.frame.test.ts b/foreign/node/src/client/client.frame.test.ts
index bdeba1e..7164cf0 100644
--- a/foreign/node/src/client/client.frame.test.ts
+++ b/foreign/node/src/client/client.frame.test.ts
@@ -23,7 +23,7 @@
   ResponseFrameDecoder
 } from './client.frame.js';
 import {
-  Command2,
+  Command,
   HEADER_SIZE,
   REPLY_OFFSET
 } from '../wire/vsr/header.js';
@@ -33,7 +33,7 @@
 const vsrFrame = (body: Buffer): Buffer => {
   const frame = Buffer.alloc(HEADER_SIZE + body.length);
   frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
-  frame.writeUInt8(Command2.Reply, REPLY_OFFSET.command);
+  frame.writeUInt8(Command.Reply, REPLY_OFFSET.command);
   body.copy(frame, HEADER_SIZE);
   return frame;
 };
diff --git a/foreign/node/src/client/client.socket.test.ts b/foreign/node/src/client/client.socket.test.ts
index 2f72cb6..0697c5f 100644
--- a/foreign/node/src/client/client.socket.test.ts
+++ b/foreign/node/src/client/client.socket.test.ts
@@ -26,7 +26,7 @@
 import { COMMAND_CODE } from '../wire/command.code.js';
 import { ResponseError } from '../wire/error.utils.js';
 import {
-  Command2,
+  Command,
   EVICTION_OFFSET,
   EvictionReason,
   HEADER_SIZE,
@@ -100,7 +100,7 @@
 ): Buffer => {
   const frame = Buffer.alloc(HEADER_SIZE + body.length);
   frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
-  frame.writeUInt8(Command2.Reply, REPLY_OFFSET.command);
+  frame.writeUInt8(Command.Reply, REPLY_OFFSET.command);
   frame.writeUInt8(operation, REPLY_OFFSET.operation);
   frame.writeUInt32LE(status, REPLY_OFFSET.status);
   body.copy(frame, HEADER_SIZE);
@@ -110,7 +110,7 @@
 const evictionFrame = (reason: number): Buffer => {
   const frame = Buffer.alloc(HEADER_SIZE);
   frame.writeUInt32LE(HEADER_SIZE, REPLY_OFFSET.size);
-  frame.writeUInt8(Command2.Eviction, REPLY_OFFSET.command);
+  frame.writeUInt8(Command.Eviction, REPLY_OFFSET.command);
   frame.writeUInt8(reason, EVICTION_OFFSET.reason);
   return frame;
 };
diff --git a/foreign/node/src/wire/command.code.ts b/foreign/node/src/wire/command.code.ts
index 972f00e..fd27a48 100644
--- a/foreign/node/src/wire/command.code.ts
+++ b/foreign/node/src/wire/command.code.ts
@@ -48,8 +48,6 @@
   GetOffset: 120,
   StoreOffset: 121,
   DeleteConsumerOffset: 122,
-  StoreOffset2: 123,
-  DeleteConsumerOffset2: 124,
   GetStream: 200,
   GetStreams: 201,
   CreateStream: 202,
diff --git a/foreign/node/src/wire/message/iggy-header.utils.ts b/foreign/node/src/wire/message/iggy-header.utils.ts
index fb285b4..cd4c53b 100644
--- a/foreign/node/src/wire/message/iggy-header.utils.ts
+++ b/foreign/node/src/wire/message/iggy-header.utils.ts
@@ -16,11 +16,160 @@
 // under the License.
 //
 
-import { toDate } from "../serialize.utils.js";
+import { xxh3 } from "@node-rs/xxhash";
 import { u128LEBufToBigint } from "../number.utils.js";
 
 /**
- * Iggy message header containing metadata for each message.
+ * Size of the batch header in bytes.
+ * Layout: u64 (partitionId) + u64 (baseOffset) + u64 (baseTimestamp) +
+ * u64 (originTimestamp) + u64 (batchLength) + u64 (batchChecksum) +
+ * u32 (messageCount) + zero padding up to 256 bytes.
+ */
+export const BATCH_HEADER_SIZE = 256;
+
+/**
+ * Size of the per-message frame header in bytes.
+ * Layout: u64 (checksum) + u128 (id) + u32 (offsetDelta) +
+ * u32 (timestampDelta) + u32 (userHeadersLength) + u32 (payloadLength) +
+ * u64 (reserved).
+ */
+export const FRAME_HEADER_SIZE = 48;
+
+/** Size of the frame checksum field prefixing the frame header. */
+const FRAME_CHECKSUM_SIZE = 8;
+
+/**
+ * Batch header describing a run of message frames.
+ */
+export type BatchHeader = {
+  /** Partition the batch belongs to (zero when sent by a client) */
+  partitionId: bigint;
+  /** Offset of the first message in the batch (zero when sent by a client) */
+  baseOffset: bigint;
+  /** Server timestamp of the batch in microseconds (zero when sent by a client) */
+  baseTimestamp: bigint;
+  /** Smallest origin timestamp of the batched messages in microseconds */
+  originTimestamp: bigint;
+  /** Total batch size in bytes, header included */
+  batchLength: bigint;
+  /** XXH3-64 checksum of the batch header fields and frame checksums */
+  batchChecksum: bigint;
+  /** Number of message frames in the batch */
+  messageCount: number;
+};
+
+/**
+ * Per-message frame header.
+ */
+export type FrameHeader = {
+  /** XXH3-64 checksum of the frame past this field, payload and user headers included */
+  checksum: bigint;
+  /** Unique message identifier */
+  id: bigint;
+  /** Index of the message within the batch */
+  offsetDelta: number;
+  /** Message origin timestamp minus batch origin timestamp in microseconds */
+  timestampDelta: number;
+  /** Length of user-defined headers in bytes */
+  userHeadersLength: number;
+  /** Length of message payload in bytes */
+  payloadLength: number;
+  /** Reserved for future use, must be zero */
+  reserved: bigint;
+};
+
+/**
+ * Serializes a batch header to its 256-byte wire format.
+ *
+ * @param header - Batch header to serialize
+ * @returns Serialized batch header buffer
+ */
+export const serializeBatchHeader = (header: BatchHeader): Buffer => {
+  const b = Buffer.alloc(BATCH_HEADER_SIZE);
+  b.writeBigUInt64LE(header.partitionId, 0);
+  b.writeBigUInt64LE(header.baseOffset, 8);
+  b.writeBigUInt64LE(header.baseTimestamp, 16);
+  b.writeBigUInt64LE(header.originTimestamp, 24);
+  b.writeBigUInt64LE(header.batchLength, 32);
+  b.writeBigUInt64LE(header.batchChecksum, 40);
+  b.writeUInt32LE(header.messageCount, 48);
+  return b;
+};
+
+/**
+ * Deserializes a batch header from a buffer.
+ *
+ * @param b - Buffer containing the serialized batch header
+ * @param pos - Starting position in the buffer
+ * @returns Parsed BatchHeader object
+ */
+export const deserializeBatchHeader = (b: Buffer, pos = 0): BatchHeader => ({
+  partitionId: b.readBigUInt64LE(pos),
+  baseOffset: b.readBigUInt64LE(pos + 8),
+  baseTimestamp: b.readBigUInt64LE(pos + 16),
+  originTimestamp: b.readBigUInt64LE(pos + 24),
+  batchLength: b.readBigUInt64LE(pos + 32),
+  batchChecksum: b.readBigUInt64LE(pos + 40),
+  messageCount: b.readUInt32LE(pos + 48),
+});
+
+/**
+ * Deserializes a frame header from a buffer.
+ *
+ * @param b - Buffer containing the serialized frame header
+ * @param pos - Starting position in the buffer
+ * @returns Parsed FrameHeader object
+ */
+export const deserializeFrameHeader = (b: Buffer, pos = 0): FrameHeader => ({
+  checksum: b.readBigUInt64LE(pos),
+  id:
+    b.readBigUInt64LE(pos + 8) | (b.readBigUInt64LE(pos + 16) << 64n),
+  offsetDelta: b.readUInt32LE(pos + 24),
+  timestampDelta: b.readUInt32LE(pos + 28),
+  userHeadersLength: b.readUInt32LE(pos + 32),
+  payloadLength: b.readUInt32LE(pos + 36),
+  reserved: b.readBigUInt64LE(pos + 40),
+});
+
+/**
+ * Computes the XXH3-64 checksum of a complete frame.
+ * Covers everything past the checksum field: the remaining frame header,
+ * the payload, and the user headers.
+ *
+ * @param frame - Complete frame buffer [header][payload][user headers]
+ * @returns Frame checksum
+ */
+export const frameChecksum = (frame: Buffer): bigint =>
+  xxh3.xxh64(frame.subarray(FRAME_CHECKSUM_SIZE));
+
+/**
+ * Computes the XXH3-64 checksum of a batch.
+ * Covers the batch header fields up to the checksum, the message count,
+ * and each frame checksum in message order.
+ *
+ * @param header - Batch header fields, batchChecksum ignored
+ * @param frameChecksums - Frame checksums in message order
+ * @returns Batch checksum
+ */
+export const batchChecksum = (
+  header: Omit<BatchHeader, "batchChecksum" | "messageCount">,
+  frameChecksums: bigint[],
+): bigint => {
+  const b = Buffer.allocUnsafe(44 + frameChecksums.length * 8);
+  b.writeBigUInt64LE(header.partitionId, 0);
+  b.writeBigUInt64LE(header.baseOffset, 8);
+  b.writeBigUInt64LE(header.baseTimestamp, 16);
+  b.writeBigUInt64LE(header.originTimestamp, 24);
+  b.writeBigUInt64LE(header.batchLength, 32);
+  b.writeUInt32LE(frameChecksums.length, 40);
+  frameChecksums.forEach((checksum, index) => {
+    b.writeBigUInt64LE(checksum, 44 + index * 8);
+  });
+  return xxh3.xxh64(b);
+};
+
+/**
+ * Iggy message header containing metadata for each polled message.
  */
 export type IggyMessageHeader = {
   /** Message checksum for integrity verification */
@@ -42,67 +191,9 @@
 };
 
 /**
- * Size of the Iggy message header in bytes.
- * Layout: u64 (checksum) + u128 (id) + u64 (offset) + u64 (timestamp) + u64 (originTimestamp) + u32 (userHeadersLength) + u32 (payloadLength) + u64 (reserved)
- */
-export const IGGY_MESSAGE_HEADER_SIZE = 8 + 16 + 8 + 8 + 8 + 4 + 4 + 8;
-
-/**
- * Serializes an Iggy message header to wire format.
- * Sets checksum, offset, and timestamp to zero (filled by server).
- *
- * @param id - Message ID as 16-byte buffer
- * @param payload - Message payload
- * @param userHeaders - Serialized user headers
- * @returns Serialized header buffer
- */
-export const serializeIggyMessageHeader = (
-  id: Buffer,
-  payload: Buffer,
-  userHeaders: Buffer,
-) => {
-  const b = Buffer.allocUnsafe(IGGY_MESSAGE_HEADER_SIZE);
-  b.writeBigUInt64LE(0n, 0); // checksum u64
-  b.fill(id, 8, 24); // id u128
-  b.writeBigUInt64LE(0n, 24); // offset u64
-  b.writeBigUInt64LE(0n, 32); // timestamp u64
-  b.writeBigUint64LE(BigInt(new Date().getTime()), 40); // originTimestamp u64
-  b.writeUInt32LE(userHeaders.length, 48); // userHeaders len u32
-  b.writeUInt32LE(payload.length, 52); // payload len u32
-  b.writeBigUInt64LE(0n, 56); // reserved u64
-  return b;
-};
-
-/**
  * Deserializes a message ID from a 16-byte buffer to BigInt.
  *
  * @param b - 16-byte buffer containing the message ID
  * @returns Message ID as BigInt
  */
 export const deserialiseMessageId = (b: Buffer) => u128LEBufToBigint(b);
-
-/**
- * Deserializes Iggy message headers from a buffer.
- *
- * @param b - Buffer containing the serialized header
- * @returns Parsed IggyMessageHeader object
- * @throws Error if buffer length doesn't match expected header size
- */
-export const deserializeIggyMessageHeaders = (b: Buffer) => {
-  if (b.length !== IGGY_MESSAGE_HEADER_SIZE)
-    throw new Error(
-      `deserialize message headers error, length = ${b.length} ` +
-        `expected ${IGGY_MESSAGE_HEADER_SIZE}`,
-    );
-  const headers: IggyMessageHeader = {
-    checksum: b.readBigUInt64LE(0),
-    id: deserialiseMessageId(b.subarray(8, 24)),
-    offset: b.readBigUInt64LE(24),
-    timestamp: toDate(b.readBigUInt64LE(32)),
-    originTimestamp: toDate(b.readBigUInt64LE(40)),
-    userHeadersLength: b.readUInt32LE(48),
-    payloadLength: b.readUInt32LE(52),
-    reserved: b.readBigUInt64LE(56),
-  };
-  return headers;
-};
diff --git a/foreign/node/src/wire/message/message-batch.test.ts b/foreign/node/src/wire/message/message-batch.test.ts
new file mode 100644
index 0000000..4cb14bb
--- /dev/null
+++ b/foreign/node/src/wire/message/message-batch.test.ts
@@ -0,0 +1,218 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+import { describe, it } from "node:test";
+import assert from "node:assert/strict";
+import { u128ToBuf } from "../number.utils.js";
+import {
+  BATCH_HEADER_SIZE,
+  FRAME_HEADER_SIZE,
+  serializeBatchHeader,
+  deserializeBatchHeader,
+} from "./iggy-header.utils.js";
+import {
+  encodeMessagesBatch,
+  serializeSendMessages,
+} from "./message.utils.js";
+import {
+  HeaderValue,
+  HeaderKeyFactory,
+  serializeHeaders,
+} from "./header.utils.js";
+import {
+  deserializeBatchMessages,
+  deserializePollMessages,
+} from "./poll.utils.js";
+
+// Golden vectors generated by the Rust implementation for stream id 1,
+// topic id 2, balanced partitioning, and two messages:
+// {id=7, originTimestamp=1000, payload="first-payload"} and
+// {id=8, originTimestamp=1050, payload="second-payload",
+// userHeaders="user-header-bytes"}.
+const PRODUCE_BATCH_ONLY =
+  "000000000000000000000000000000000000000000000000e803000000000000" +
+  "8c01000000000000a91f38c86307267c02000000000000000000000000000000" +
+  "0000000000000000000000000000000000000000000000000000000000000000" +
+  "0000000000000000000000000000000000000000000000000000000000000000" +
+  "0000000000000000000000000000000000000000000000000000000000000000" +
+  "0000000000000000000000000000000000000000000000000000000000000000" +
+  "0000000000000000000000000000000000000000000000000000000000000000" +
+  "0000000000000000000000000000000000000000000000000000000000000000" +
+  "bfd2b9205a759675070000000000000000000000000000000000000000000000" +
+  "000000000d000000000000000000000066697273742d7061796c6f6164d66b7e" +
+  "1c758eb7c0080000000000000000000000000000000100000032000000110000" +
+  "000e00000000000000000000007365636f6e642d7061796c6f6164757365722d" +
+  "6865616465722d6279746573";
+
+const POLL_BODY =
+  "0300000065000000000000000200000003000000000000006400000000000000" +
+  "8813000000000000e8030000000000008c01000000000000c96826b38a8feed2" +
+  "0200000000000000000000000000000000000000000000000000000000000000" +
+  "0000000000000000000000000000000000000000000000000000000000000000" +
+  "0000000000000000000000000000000000000000000000000000000000000000" +
+  "0000000000000000000000000000000000000000000000000000000000000000" +
+  "0000000000000000000000000000000000000000000000000000000000000000" +
+  "0000000000000000000000000000000000000000000000000000000000000000" +
+  "00000000000000000000000000000000bfd2b9205a7596750700000000000000" +
+  "00000000000000000000000000000000000000000d0000000000000000000000" +
+  "66697273742d7061796c6f6164d66b7e1c758eb7c00800000000000000000000" +
+  "00000000000100000032000000110000000e0000000000000000000000736563" +
+  "6f6e642d7061796c6f6164757365722d6865616465722d6279746573";
+
+const goldenMessages = () => [
+  {
+    id: u128ToBuf(7n),
+    payload: Buffer.from("first-payload"),
+    userHeaders: Buffer.alloc(0),
+    originTimestamp: 1000n,
+  },
+  {
+    id: u128ToBuf(8n),
+    payload: Buffer.from("second-payload"),
+    userHeaders: Buffer.from("user-header-bytes"),
+    originTimestamp: 1050n,
+  },
+];
+
+describe("message batch golden vectors", () => {
+  it("encodes the golden produce batch byte-for-byte", () => {
+    const batch = encodeMessagesBatch(goldenMessages());
+    assert.equal(batch.toString("hex"), PRODUCE_BATCH_ONLY);
+  });
+
+  it("prefixes the batch with the golden send metadata", () => {
+    const body = serializeSendMessages(1, 2, [
+      { id: 7n, payload: "first-payload" },
+      { id: 8n, payload: "second-payload" },
+    ]);
+    const metadata = Buffer.from(
+      "120000000104010000000104020000000100" + "02000000",
+      "hex",
+    );
+    assert.deepEqual(body.subarray(0, metadata.length), metadata);
+    assert.equal(
+      body.length,
+      metadata.length + BATCH_HEADER_SIZE + FRAME_HEADER_SIZE * 2 +
+        "first-payload".length + "second-payload".length,
+    );
+  });
+
+  it("decodes the golden poll body", () => {
+    const body = Buffer.from(POLL_BODY, "hex");
+    assert.equal(body.readUInt32LE(0), 3);
+    assert.equal(body.readBigUInt64LE(4), 101n);
+    assert.equal(body.readUInt32LE(12), 2);
+
+    const messages = deserializeBatchMessages(body, 16);
+    assert.equal(messages.length, 2);
+
+    assert.equal(messages[0].id, 7n);
+    assert.equal(messages[0].offset, 100n);
+    assert.equal(messages[0].timestamp, 5000n);
+    assert.equal(messages[0].originTimestamp, 1000n);
+    assert.equal(messages[0].checksum, 0x7596755a20b9d2bfn);
+    assert.equal(messages[0].payload.toString(), "first-payload");
+    assert.equal(messages[0].userHeaders.length, 0);
+
+    assert.equal(messages[1].id, 8n);
+    assert.equal(messages[1].offset, 101n);
+    assert.equal(messages[1].timestamp, 5000n);
+    assert.equal(messages[1].originTimestamp, 1050n);
+    assert.equal(messages[1].checksum, 0xc0b78e751c7e6bd6n);
+    assert.equal(messages[1].payload.toString(), "second-payload");
+    assert.equal(messages[1].userHeaders.toString(), "user-header-bytes");
+  });
+
+  it("rejects a non-zero reserved frame field", () => {
+    const body = Buffer.from(POLL_BODY, "hex");
+    body.writeUInt8(1, 16 + BATCH_HEADER_SIZE + 40);
+    assert.throws(
+      () => deserializeBatchMessages(body, 16),
+      /reserved/,
+    );
+  });
+
+  it("rejects an empty batch", () => {
+    assert.throws(() => encodeMessagesBatch([]));
+    assert.throws(() => serializeSendMessages(1, 2, []));
+  });
+
+  it("rejects a timestamp delta beyond u32", () => {
+    const messages = goldenMessages();
+    messages[1].originTimestamp =
+      messages[0].originTimestamp + 0x1_0000_0000n;
+    assert.throws(() => encodeMessagesBatch(messages), /u32/);
+  });
+
+  it("round-trips messages through the public poll decoder", () => {
+    const userHeaders = [
+      {
+        key: HeaderKeyFactory.String("trace"),
+        value: HeaderValue.String("golden"),
+      },
+    ];
+    const batch = encodeMessagesBatch([
+      {
+        id: u128ToBuf(7n),
+        payload: Buffer.from("first-payload"),
+        userHeaders: Buffer.alloc(0),
+        originTimestamp: 3_000_000n,
+      },
+      {
+        id: u128ToBuf(8n),
+        payload: Buffer.from("second-payload"),
+        userHeaders: serializeHeaders(userHeaders),
+        originTimestamp: 3_000_500n,
+      },
+    ]);
+    const stamped = deserializeBatchHeader(batch);
+    stamped.partitionId = 3n;
+    stamped.baseOffset = 100n;
+    stamped.baseTimestamp = 5_000_000n;
+    serializeBatchHeader(stamped).copy(batch);
+
+    const envelope = Buffer.alloc(16);
+    envelope.writeUInt32LE(3, 0);
+    envelope.writeBigUInt64LE(101n, 4);
+    envelope.writeUInt32LE(2, 12);
+    const response = deserializePollMessages(
+      Buffer.concat([envelope, batch]),
+    );
+
+    assert.equal(response.partitionId, 3);
+    assert.equal(response.currentOffset, 101n);
+    assert.equal(response.count, 2);
+    assert.equal(response.messages.length, 2);
+
+    const [first, second] = response.messages;
+    assert.equal(first.headers.id, 7n);
+    assert.equal(first.headers.offset, 100n);
+    assert.deepEqual(first.headers.timestamp, new Date(5_000));
+    assert.deepEqual(first.headers.originTimestamp, new Date(3_000));
+    assert.equal(first.payload.toString(), "first-payload");
+    assert.deepEqual(first.userHeaders, []);
+
+    assert.equal(second.headers.id, 8n);
+    assert.equal(second.headers.offset, 101n);
+    assert.deepEqual(second.headers.originTimestamp, new Date(3_000));
+    assert.equal(second.payload.toString(), "second-payload");
+    assert.equal(second.userHeaders.length, 1);
+    assert.equal(second.userHeaders[0].key.value, "trace");
+    assert.equal(second.userHeaders[0].value.value, "golden");
+  });
+});
diff --git a/foreign/node/src/wire/message/message.utils.ts b/foreign/node/src/wire/message/message.utils.ts
index ae7e91e..a350f7b 100644
--- a/foreign/node/src/wire/message/message.utils.ts
+++ b/foreign/node/src/wire/message/message.utils.ts
@@ -16,18 +16,25 @@
 // under the License.
 //
 
-import Debug from 'debug';
+import { uuidv4 } from 'uuidv7';
 import { uint32ToBuf, u128ToBuf, uint8ToBuf } from '../number.utils.js';
 import { serializeHeaders, type Headers } from './header.utils.js';
 import { serializeIdentifier, type Id } from '../identifier.utils.js';
 import { serializePartitioning, type Partitioning } from './partitioning.utils.js';
 import { parse as parseUUID } from '../uuid.utils.js';
-import { serializeIggyMessageHeader } from './iggy-header.utils.js';
+import {
+  BATCH_HEADER_SIZE,
+  FRAME_HEADER_SIZE,
+  batchChecksum,
+  frameChecksum,
+  serializeBatchHeader,
+} from './iggy-header.utils.js';
 
-const debug = Debug('iggy:client');
+/** Size of the message ID in bytes (u128) */
+const MESSAGE_ID_SIZE = 16;
 
-/** Size of the message index entry in bytes (16 bytes per message) */
-const INDEX_SIZE = 16;
+/** Largest representable frame timestamp delta (u32, microseconds) */
+const MAX_TIMESTAMP_DELTA = 0xFFFF_FFFFn;
 
 /** Valid types for message ID: numeric, bigint, or UUID string */
 export type MessageIdKind = number | bigint | string;
@@ -41,7 +48,23 @@
   /** Optional user-defined headers */
   headers?: Headers,
   /** Message payload as string or Buffer */
-  payload: string | Buffer
+  payload: string | Buffer,
+  /** Optional origin timestamp in microseconds (defaults to send time) */
+  originTimestamp?: bigint
+};
+
+/**
+ * A message prepared for batch encoding.
+ */
+export type MessageToEncode = {
+  /** Message ID as a 16-byte little-endian buffer */
+  id: Buffer,
+  /** Message payload */
+  payload: Buffer,
+  /** Serialized user headers */
+  userHeaders: Buffer,
+  /** Origin timestamp in microseconds */
+  originTimestamp: bigint
 };
 
 /**
@@ -57,7 +80,7 @@
   'number' === typeof x;
 
 /**
- * Serializes a message ID to a 16-byte buffer.
+ * Serializes a message ID to a 16-byte little-endian buffer.
  * Supports undefined (zero), numeric, bigint, and UUID string formats.
  *
  * @param id - Message ID to serialize
@@ -70,7 +93,7 @@
     throw new Error(`invalid message id: '${id}' (use uuid string | number | bigint >= 0)`)
 
   if(id === undefined)
-    return Buffer.alloc(16, 0); // 0u128
+    return Buffer.alloc(MESSAGE_ID_SIZE, 0); // 0u128
 
   if ('bigint' === typeof id || 'number' === typeof id) {
     if (id < 0)
@@ -82,7 +105,7 @@
 
   try {
     const uuid = parseUUID(id);
-    return Buffer.from(uuid.toHex(), 'hex');
+    return u128ToBuf(BigInt(`0x${uuid.toHex()}`));
   } catch (err) {
     throw new Error(
       `invalid message id: '${id}' (use uuid string | number | bigint >= 0)`,
@@ -93,76 +116,112 @@
 }
 
 /**
- * Serializes a single message to wire format.
- * Format: [iggy_header][payload][user_headers]
+ * Serializes a message ID, minting a random UUID when the ID is
+ * absent or zero.
  *
- * @param msg - Message to serialize
- * @returns Serialized message buffer
+ * @param id - Optional message ID
+ * @returns 16-byte little-endian buffer containing a non-zero ID
  */
-export const serializeMessage = (msg: CreateMessage) => {
-  const { id, headers, payload } = msg;
-
+const resolveMessageId = (id?: MessageIdKind): Buffer => {
   const bId = serializeMessageId(id);
-  const bUserHeaders = serializeHeaders(headers);
-  const bPayload = 'string' === typeof payload ? Buffer.from(payload) : payload
-  const bIggyMessageHeader = serializeIggyMessageHeader(bId, bPayload, bUserHeaders);
-
-  const r = Buffer.concat([
-    bIggyMessageHeader,
-    bPayload,
-    bUserHeaders
-  ]);
-
-  debug(
-    'id', bId.length, bId.toString('hex'),
-    'iggyHeaders', bIggyMessageHeader.length, bIggyMessageHeader.toString('hex'),
-    'userHeaders', bUserHeaders.length, bUserHeaders.toString('hex'),
-    'payload', bPayload.length, bPayload.toString('hex'),
-    'full len', r.length //, r.toString('hex')
-  );
-
-  return r;
+  return bId.every((byte) => byte === 0)
+    ? u128ToBuf(BigInt(`0x${uuidv4().replaceAll('-', '')}`))
+    : bId;
 };
 
 /**
- * Serializes multiple messages to an array of buffers.
+ * Serializes a single message frame.
+ * Format: [frame header][payload][user headers]
  *
- * @param messages - Array of messages to serialize
- * @returns Array of serialized message buffers
+ * @param message - Message to serialize
+ * @param index - Index of the message within the batch
+ * @param batchOriginTimestamp - Origin timestamp of the batch in microseconds
+ * @returns Serialized frame buffer
+ * @throws Error if the timestamp delta exceeds u32
  */
-export const serializeMessages = (messages: CreateMessage[]) =>
-  messages.map(c => serializeMessage(c));
+const serializeMessageFrame = (
+  { id, payload, userHeaders, originTimestamp }: MessageToEncode,
+  index: number,
+  batchOriginTimestamp: bigint
+): Buffer => {
+  if (id.length !== MESSAGE_ID_SIZE)
+    throw new Error(
+      `invalid message id length: ${id.length}, expected ${MESSAGE_ID_SIZE}`
+    );
+  const timestampDelta = originTimestamp - batchOriginTimestamp;
+  if (timestampDelta > MAX_TIMESTAMP_DELTA)
+    throw new Error(
+      `message timestamp delta ${timestampDelta} exceeds u32 range`
+    );
+
+  const frame = Buffer.alloc(
+    FRAME_HEADER_SIZE + payload.length + userHeaders.length
+  );
+  id.copy(frame, 8);
+  frame.writeUInt32LE(index, 24);
+  frame.writeUInt32LE(Number(timestampDelta), 28);
+  frame.writeUInt32LE(userHeaders.length, 32);
+  frame.writeUInt32LE(payload.length, 36);
+  payload.copy(frame, FRAME_HEADER_SIZE);
+  userHeaders.copy(frame, FRAME_HEADER_SIZE + payload.length);
+  frame.writeBigUInt64LE(frameChecksum(frame), 0);
+  return frame;
+};
 
 /**
- * Creates an index buffer for a batch of messages.
- * Each index entry is 16 bytes tracking message positions.
+ * Encodes messages into the canonical batch format.
+ * Format: [batch header][frames], one frame per message.
  *
- * @param messages - Array of serialized message buffers
- * @returns Index buffer
+ * @param messages - Messages to encode
+ * @returns Serialized batch buffer
+ * @throws Error if the batch is empty
  */
-export const createMessagesIndex = (messages: Buffer[]) => {
-  const bIndex = Buffer.allocUnsafe(messages.length * INDEX_SIZE);
-  let currentIndex = 0;
-  let msgsSize = 0;
-  messages.forEach(msg => {
-    msgsSize += msg.length;
-    bIndex.writeBigUInt64LE(0n, currentIndex)
-    bIndex.writeUInt32LE(msgsSize, currentIndex + 4)
-    bIndex.writeBigUInt64LE(0n, currentIndex + 8)
-    currentIndex += INDEX_SIZE;
-  });
-  return bIndex;
-}
+export const encodeMessagesBatch = (messages: MessageToEncode[]): Buffer => {
+  if (messages.length === 0)
+    throw new Error('cannot encode an empty message batch');
+
+  const originTimestamp = messages.reduce(
+    (min, message) =>
+      message.originTimestamp < min ? message.originTimestamp : min,
+    messages[0].originTimestamp
+  );
+  const frames = messages.map((message, index) =>
+    serializeMessageFrame(message, index, originTimestamp)
+  );
+  const framesLength = frames.reduce((sum, frame) => sum + frame.length, 0);
+  const batchLength = BigInt(BATCH_HEADER_SIZE + framesLength);
+  const header = {
+    partitionId: 0n,
+    baseOffset: 0n,
+    baseTimestamp: 0n,
+    originTimestamp,
+    batchLength,
+  };
+
+  return Buffer.concat([
+    serializeBatchHeader({
+      ...header,
+      batchChecksum: batchChecksum(
+        header,
+        frames.map((frame) => frame.readBigUInt64LE(0))
+      ),
+      messageCount: messages.length,
+    }),
+    ...frames,
+  ]);
+};
 
 /**
  * Serializes a send messages command payload.
- * Includes stream/topic identifiers, partitioning, and all messages with index.
+ * Format: [metadata length][stream id][topic id][partitioning]
+ * [messages count][batch].
  *
  * @param streamId - Stream identifier
  * @param topicId - Topic identifier
  * @param messages - Array of messages to send
  * @param partitioning - Optional partitioning strategy
  * @returns Serialized command payload
+ * @throws Error if the message array is empty
  */
 export const serializeSendMessages = (
   streamId: Id,
@@ -170,6 +229,9 @@
   messages: CreateMessage[],
   partitioning?: Partitioning,
 ) => {
+  if (messages.length === 0)
+    throw new Error('cannot send an empty message batch');
+
   const streamIdentifier = serializeIdentifier(streamId);
   const topicIdentifier = serializeIdentifier(topicId);
   const bPartitioning = serializePartitioning(partitioning);
@@ -179,8 +241,15 @@
       bPartitioning.length + bMessagesCount.length
   );
 
-  const bMessagesArray = serializeMessages(messages);
-  const bMessageIndex = createMessagesIndex(bMessagesArray);
+  const sendTimestamp = BigInt(Date.now()) * 1000n;
+  const bBatch = encodeMessagesBatch(messages.map(
+    ({ id, headers, payload, originTimestamp }) => ({
+      id: resolveMessageId(id),
+      payload: 'string' === typeof payload ? Buffer.from(payload) : payload,
+      userHeaders: serializeHeaders(headers),
+      originTimestamp: originTimestamp ?? sendTimestamp
+    })
+  ));
 
   return Buffer.concat([
     bMetadataLen,
@@ -188,8 +257,7 @@
     topicIdentifier,
     bPartitioning,
     bMessagesCount,
-    bMessageIndex,
-    ...bMessagesArray
+    bBatch
   ]);
 };
 
diff --git a/foreign/node/src/wire/message/poll.utils.ts b/foreign/node/src/wire/message/poll.utils.ts
index a425eaa..a92e06a 100644
--- a/foreign/node/src/wire/message/poll.utils.ts
+++ b/foreign/node/src/wire/message/poll.utils.ts
@@ -18,12 +18,15 @@
 
 import { type Id } from "../identifier.utils.js";
 import { type ValueOf, reverseRecord } from "../../type.utils.js";
+import { toDate } from "../serialize.utils.js";
 import { serializeGetOffset, type Consumer } from "../offset/offset.utils.js";
 import { deserializeHeaders, type ParsedHeaderEntry } from "./header.utils.js";
 import { Transform, type TransformCallback } from "node:stream";
 import {
-  deserializeIggyMessageHeaders,
-  IGGY_MESSAGE_HEADER_SIZE,
+  BATCH_HEADER_SIZE,
+  FRAME_HEADER_SIZE,
+  deserializeBatchHeader,
+  deserializeFrameHeader,
   type IggyMessageHeader,
 } from "./iggy-header.utils.js";
 
@@ -237,44 +240,108 @@
 };
 
 /**
- * Deserializes an array of messages from a buffer.
- *
- * @param b - Buffer containing serialized messages
- * @returns Array of deserialized messages
+ * A message frame decoded from a batch record, with absolute values
+ * resolved against the record header.
  */
-export const deserializeMessages = (b: Buffer) => {
-  const messages: Message[] = [];
-  let pos = 0;
+export type BatchMessage = {
+  /** Frame checksum, passed through unverified */
+  checksum: bigint;
+  /** Unique message identifier */
+  id: bigint;
+  /** Absolute message offset within the partition */
+  offset: bigint;
+  /** Server timestamp of the record in microseconds */
+  timestamp: bigint;
+  /** Message origin timestamp in microseconds */
+  originTimestamp: bigint;
+  /** Message payload data */
+  payload: Buffer;
+  /** Raw user header bytes */
+  userHeaders: Buffer;
+};
+
+/**
+ * Deserializes batch records into message frames with absolute values.
+ * Each record is [batch header][frames], walked by the record's batch
+ * length. Records may be server-sliced, so the first frame of a record
+ * can carry a non-zero offset delta.
+ *
+ * @param b - Buffer containing serialized batch records
+ * @param pos - Starting position in the buffer
+ * @returns Array of decoded message frames
+ * @throws Error if a record or frame is malformed
+ */
+export const deserializeBatchMessages = (
+  b: Buffer,
+  pos = 0,
+): BatchMessage[] => {
+  const messages: BatchMessage[] = [];
   const len = b.length;
   while (pos < len) {
-    if (pos + IGGY_MESSAGE_HEADER_SIZE > len) break;
-    const bHead = b.subarray(pos, pos + IGGY_MESSAGE_HEADER_SIZE);
-    const headers = deserializeIggyMessageHeaders(bHead);
-    pos += IGGY_MESSAGE_HEADER_SIZE;
-    const plEnd = pos + headers.payloadLength;
-    if (plEnd > len) break;
-    const payload = b.subarray(pos, plEnd);
-    pos += headers.payloadLength;
-    let userHeaders: ParsedHeaderEntry[] = [];
-    if (
-      headers.userHeadersLength > 0 &&
-      plEnd + headers.userHeadersLength <= len
-    ) {
-      userHeaders = deserializeHeaders(
-        b.subarray(plEnd, plEnd + headers.userHeadersLength),
+    if (pos + BATCH_HEADER_SIZE > len)
+      throw new Error("truncated batch header in poll response");
+    const batch = deserializeBatchHeader(b, pos);
+    const recordEnd = pos + Number(batch.batchLength);
+    if (Number(batch.batchLength) < BATCH_HEADER_SIZE || recordEnd > len)
+      throw new Error(
+        `invalid batch length ${batch.batchLength} in poll response`,
       );
-      pos += headers.userHeadersLength;
+    pos += BATCH_HEADER_SIZE;
+    while (pos < recordEnd) {
+      if (pos + FRAME_HEADER_SIZE > recordEnd)
+        throw new Error("truncated message frame in poll response");
+      const frame = deserializeFrameHeader(b, pos);
+      if (frame.reserved !== 0n)
+        throw new Error(
+          `non-zero reserved field ${frame.reserved} in message frame`,
+        );
+      const payloadEnd = pos + FRAME_HEADER_SIZE + frame.payloadLength;
+      const frameEnd = payloadEnd + frame.userHeadersLength;
+      if (frameEnd > recordEnd)
+        throw new Error("truncated message frame in poll response");
+      messages.push({
+        checksum: frame.checksum,
+        id: frame.id,
+        offset: batch.baseOffset + BigInt(frame.offsetDelta),
+        timestamp: batch.baseTimestamp,
+        originTimestamp:
+          batch.originTimestamp + BigInt(frame.timestampDelta),
+        payload: b.subarray(pos + FRAME_HEADER_SIZE, payloadEnd),
+        userHeaders: b.subarray(payloadEnd, frameEnd),
+      });
+      pos = frameEnd;
     }
-    messages.push({
-      headers,
-      payload,
-      userHeaders,
-    });
   }
   return messages;
 };
 
 /**
+ * Deserializes an array of messages from a buffer of batch records.
+ *
+ * @param b - Buffer containing serialized batch records
+ * @param pos - Starting position in the buffer
+ * @returns Array of deserialized messages
+ */
+export const deserializeMessages = (b: Buffer, pos = 0): Message[] =>
+  deserializeBatchMessages(b, pos).map((message) => ({
+    headers: {
+      checksum: message.checksum,
+      id: message.id,
+      offset: message.offset,
+      timestamp: toDate(message.timestamp),
+      originTimestamp: toDate(message.originTimestamp),
+      userHeadersLength: message.userHeaders.length,
+      payloadLength: message.payload.length,
+      reserved: 0n,
+    },
+    payload: message.payload,
+    userHeaders:
+      message.userHeaders.length > 0
+        ? deserializeHeaders(message.userHeaders)
+        : ([] as ParsedHeaderEntry[]),
+  }));
+
+/**
  * Deserializes a poll messages response from a buffer.
  *
  * @param r - Response buffer
@@ -285,7 +352,7 @@
   const partitionId = r.readUInt32LE(pos);
   const currentOffset = r.readBigUInt64LE(pos + 4);
   const count = r.readUInt32LE(pos + 12);
-  const messages = deserializeMessages(r.subarray(16));
+  const messages = deserializeMessages(r, pos + 16);
 
   return {
     partitionId,
diff --git a/foreign/node/src/wire/message/send-messages.command.test.ts b/foreign/node/src/wire/message/send-messages.command.test.ts
index ac35b9c..01fa75e 100644
--- a/foreign/node/src/wire/message/send-messages.command.test.ts
+++ b/foreign/node/src/wire/message/send-messages.command.test.ts
@@ -76,7 +76,9 @@
     };
 
     it("serialize SendMessages into a buffer", () => {
-      assert.deepEqual(SEND_MESSAGES.serialize(t1).length, 589);
+      // metadata length prefix (4) + metadata (18) + batch header (256) +
+      // 7 frames of 48-byte header + 1-byte payload (343)
+      assert.deepEqual(SEND_MESSAGES.serialize(t1).length, 621);
     });
 
     it("serialize all kinds of messageId", () => {
diff --git a/foreign/node/src/wire/offset/delete-offset.command.ts b/foreign/node/src/wire/offset/delete-offset.command.ts
index 9cefe98..6d7f37f 100644
--- a/foreign/node/src/wire/offset/delete-offset.command.ts
+++ b/foreign/node/src/wire/offset/delete-offset.command.ts
@@ -19,7 +19,7 @@
 import { deserializeVoidResponse } from '../../client/client.utils.js';
 import type { GetOffset } from './get-offset.command.js';
 import { wrapCommand } from '../command.utils.js';
-import { serializeGetOffset } from './offset.utils.js';
+import { serializeDeleteOffset } from './offset.utils.js';
 import { COMMAND_CODE } from '../command.code.js';
 
 
@@ -37,7 +37,7 @@
   code: COMMAND_CODE.DeleteConsumerOffset,
 
   serialize: ({ streamId, topicId, consumer, partitionId = 0 }: DeleteOffset) => {
-    return serializeGetOffset(streamId, topicId, consumer, partitionId);
+    return serializeDeleteOffset(streamId, topicId, consumer, partitionId);
   },
 
   deserialize: deserializeVoidResponse
diff --git a/foreign/node/src/wire/offset/offset.utils.ts b/foreign/node/src/wire/offset/offset.utils.ts
index 84c9f2f..4a7c859 100644
--- a/foreign/node/src/wire/offset/offset.utils.ts
+++ b/foreign/node/src/wire/offset/offset.utils.ts
@@ -20,6 +20,9 @@
 import { serializeIdentifier, type Id } from '../identifier.utils.js';
 import { uint8ToBuf } from '../number.utils.js';
 
+/** Offset mutations always request quorum acknowledgement. */
+const ACK_QUORUM = 1;
+
 /**
  * Consumer kind options for offset operations.
  */
@@ -145,11 +148,32 @@
   partitionId: number | null,
   offset: bigint
 ) => {
-  const b = Buffer.allocUnsafe(8);
+  const b = Buffer.allocUnsafe(9);
   b.writeBigUInt64LE(offset, 0);
+  b.writeUInt8(ACK_QUORUM, 8);
 
   return Buffer.concat([
     serializeGetOffset(streamId, topicId, consumer, partitionId),
     b
   ]);
 }
+
+/**
+ * Serializes parameters for delete offset operation.
+ *
+ * @param streamId - Stream identifier (ID or name)
+ * @param topicId - Topic identifier (ID or name)
+ * @param consumer - Consumer identifier (single or group)
+ * @param partitionId - Partition ID. VSR requires an explicit partition.
+ * @returns Buffer containing serialized delete offset request
+ */
+export const serializeDeleteOffset = (
+  streamId: Id,
+  topicId: Id,
+  consumer: Consumer,
+  partitionId: number | null
+) =>
+  Buffer.concat([
+    serializeGetOffset(streamId, topicId, consumer, partitionId),
+    uint8ToBuf(ACK_QUORUM)
+  ]);
diff --git a/foreign/node/src/wire/vsr/header.test.ts b/foreign/node/src/wire/vsr/header.test.ts
index a8095c7..639e2a3 100644
--- a/foreign/node/src/wire/vsr/header.test.ts
+++ b/foreign/node/src/wire/vsr/header.test.ts
@@ -18,7 +18,7 @@
 import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 import {
-  Command2,
+  Command,
   encodeRequestHeader,
   HEADER_SIZE,
   REQUEST_OFFSET
@@ -38,7 +38,7 @@
 
     assert.equal(header.length, HEADER_SIZE);
     assert.equal(header.readUInt32LE(REQUEST_OFFSET.size), HEADER_SIZE + 3);
-    assert.equal(header.readUInt8(REQUEST_OFFSET.command), Command2.Request);
+    assert.equal(header.readUInt8(REQUEST_OFFSET.command), Command.Request);
     assert.equal(
       header.readBigUInt64LE(REQUEST_OFFSET.client),
       0x99AABBCCDDEEFF00n
@@ -70,7 +70,7 @@
     });
     const expected = Buffer.alloc(HEADER_SIZE);
     expected.writeUInt32LE(HEADER_SIZE, REQUEST_OFFSET.size);
-    expected.writeUInt8(Command2.Request, REQUEST_OFFSET.command);
+    expected.writeUInt8(Command.Request, REQUEST_OFFSET.command);
     expected.writeBigUInt64LE(1n, REQUEST_OFFSET.client);
     expected.writeUInt8(1, REQUEST_OFFSET.operation);
     assert.deepEqual(header, expected);
diff --git a/foreign/node/src/wire/vsr/header.ts b/foreign/node/src/wire/vsr/header.ts
index 9c51217..4b00e60 100644
--- a/foreign/node/src/wire/vsr/header.ts
+++ b/foreign/node/src/wire/vsr/header.ts
@@ -64,8 +64,8 @@
   reason: 255
 } as const;
 
-/** `Command2` frame discriminants a client encounters. */
-export const Command2 = {
+/** `Command` frame discriminants a client encounters. */
+export const Command = {
   Request: 5,
   Reply: 8,
   Eviction: 13
@@ -120,7 +120,7 @@
 export const encodeRequestHeader = (fields: RequestHeaderFields): Buffer => {
   const header = Buffer.alloc(HEADER_SIZE);
   header.writeUInt32LE(fields.size, REQUEST_OFFSET.size);
-  header.writeUInt8(Command2.Request, REQUEST_OFFSET.command);
+  header.writeUInt8(Command.Request, REQUEST_OFFSET.command);
   // u128 client id: two little-endian u64 halves, low half first.
   header.writeBigUInt64LE(fields.client & U64_MASK, REQUEST_OFFSET.client);
   header.writeBigUInt64LE(fields.client >> 64n, REQUEST_OFFSET.client + 8);
diff --git a/foreign/node/src/wire/vsr/operation.test.ts b/foreign/node/src/wire/vsr/operation.test.ts
index 2602373..daac920 100644
--- a/foreign/node/src/wire/vsr/operation.test.ts
+++ b/foreign/node/src/wire/vsr/operation.test.ts
@@ -53,9 +53,7 @@
   [COMMAND_CODE.LeaveGroup, Operation.LeaveConsumerGroup],
   [COMMAND_CODE.SendMessages, Operation.SendMessages],
   [COMMAND_CODE.StoreOffset, Operation.StoreConsumerOffset],
-  [COMMAND_CODE.DeleteConsumerOffset, Operation.DeleteConsumerOffset],
-  [COMMAND_CODE.StoreOffset2, Operation.StoreConsumerOffset2],
-  [COMMAND_CODE.DeleteConsumerOffset2, Operation.DeleteConsumerOffset2]
+  [COMMAND_CODE.DeleteConsumerOffset, Operation.DeleteConsumerOffset]
 ]);
 
 describe('VSR operation classification', () => {
@@ -92,16 +90,14 @@
     assert.equal(isPartition(Operation.SendMessages), true);
     assert.equal(isPartition(159), false);
     assert.equal(isResultFramed(Operation.StoreConsumerOffset), true);
-    assert.equal(isResultFramed(Operation.StoreConsumerOffset2), true);
     assert.equal(isResultFramed(Operation.DeleteConsumerOffset), true);
-    assert.equal(isResultFramed(Operation.DeleteConsumerOffset2), true);
     assert.equal(isResultFramed(Operation.SendMessages), false);
   });
 
   it('recognizes only declared operation discriminants', () => {
     assert.equal(isKnownOperation(Operation.Register), true);
     assert.equal(isKnownOperation(Operation.SendMessages), true);
-    for (const undeclared of [69, 127, 150, 159, 163, 166, 255])
+    for (const undeclared of [69, 127, 150, 159, 163, 164, 165, 166, 255])
       assert.equal(isKnownOperation(undeclared), false);
   });
 });
diff --git a/foreign/node/src/wire/vsr/operation.ts b/foreign/node/src/wire/vsr/operation.ts
index 4765e53..6c7520d 100644
--- a/foreign/node/src/wire/vsr/operation.ts
+++ b/foreign/node/src/wire/vsr/operation.ts
@@ -59,9 +59,7 @@
   LeaveConsumerGroup: 149,
   SendMessages: 160,
   StoreConsumerOffset: 161,
-  DeleteConsumerOffset: 162,
-  StoreConsumerOffset2: 164,
-  DeleteConsumerOffset2: 165
+  DeleteConsumerOffset: 162
 } as const;
 
 const INTERNAL_START = 64;
@@ -84,8 +82,6 @@
   [COMMAND_CODE.SendMessages, Operation.SendMessages],
   [COMMAND_CODE.StoreOffset, Operation.StoreConsumerOffset],
   [COMMAND_CODE.DeleteConsumerOffset, Operation.DeleteConsumerOffset],
-  [COMMAND_CODE.StoreOffset2, Operation.StoreConsumerOffset2],
-  [COMMAND_CODE.DeleteConsumerOffset2, Operation.DeleteConsumerOffset2],
   [COMMAND_CODE.CreateStream, Operation.CreateStream],
   [COMMAND_CODE.DeleteStream, Operation.DeleteStream],
   [COMMAND_CODE.UpdateStream, Operation.UpdateStream],
@@ -139,9 +135,7 @@
 export const isResultFramed = (operation: number): boolean =>
   isMetadata(operation) ||
   operation === Operation.StoreConsumerOffset ||
-  operation === Operation.StoreConsumerOffset2 ||
-  operation === Operation.DeleteConsumerOffset ||
-  operation === Operation.DeleteConsumerOffset2;
+  operation === Operation.DeleteConsumerOffset;
 
 /**
  * Picks the header operation for a command code. Unknown extension codes are
diff --git a/foreign/node/src/wire/vsr/reply.test.ts b/foreign/node/src/wire/vsr/reply.test.ts
index ea4a1a6..33c8099 100644
--- a/foreign/node/src/wire/vsr/reply.test.ts
+++ b/foreign/node/src/wire/vsr/reply.test.ts
@@ -20,7 +20,7 @@
 import { COMMAND_CODE } from '../command.code.js';
 import { ResponseError } from '../error.utils.js';
 import {
-  Command2,
+  Command,
   EVICTION_OFFSET,
   EvictionReason,
   HEADER_SIZE,
@@ -40,7 +40,7 @@
 ): Buffer => {
   const frame = Buffer.alloc(HEADER_SIZE + body.length);
   frame.writeUInt32LE(frame.length, REPLY_OFFSET.size);
-  frame.writeUInt8(Command2.Reply, REPLY_OFFSET.command);
+  frame.writeUInt8(Command.Reply, REPLY_OFFSET.command);
   frame.writeUInt8(operation, REPLY_OFFSET.operation);
   frame.writeUInt32LE(status, REPLY_OFFSET.status);
   body.copy(frame, HEADER_SIZE);
@@ -50,7 +50,7 @@
 const eviction = (reason: number): Buffer => {
   const frame = Buffer.alloc(HEADER_SIZE);
   frame.writeUInt32LE(HEADER_SIZE, REPLY_OFFSET.size);
-  frame.writeUInt8(Command2.Eviction, REPLY_OFFSET.command);
+  frame.writeUInt8(Command.Eviction, REPLY_OFFSET.command);
   frame.writeUInt8(reason, EVICTION_OFFSET.reason);
   return frame;
 };
@@ -64,7 +64,7 @@
     assert.throws(() => decodeResponse(undersized), ResponseError);
 
     const unsupported = reply(Operation.NonReplicated);
-    unsupported.writeUInt8(Command2.Request, REPLY_OFFSET.command);
+    unsupported.writeUInt8(Command.Request, REPLY_OFFSET.command);
     assert.throws(() => decodeResponse(unsupported), ResponseError);
   });
 
diff --git a/foreign/node/src/wire/vsr/reply.ts b/foreign/node/src/wire/vsr/reply.ts
index 9309f9a..b0871ea 100644
--- a/foreign/node/src/wire/vsr/reply.ts
+++ b/foreign/node/src/wire/vsr/reply.ts
@@ -26,7 +26,7 @@
 
 import { ResponseError, responseError } from '../error.utils.js';
 import {
-  Command2, EvictionReason, HEADER_SIZE,
+  Command, EvictionReason, HEADER_SIZE,
   peekCommand, readEviction, readReplyOperation, readSize, readStatus
 } from './header.js';
 import { Operation, isKnownOperation, isResultFramed } from './operation.js';
@@ -65,9 +65,9 @@
     throw responseError(commandCode, EMPTY_RESPONSE);
 
   switch (peekCommand(frame)) {
-    case Command2.Eviction:
+    case Command.Eviction:
       throw evictionError(frame);
-    case Command2.Reply:
+    case Command.Reply:
       break;
     default:
       throw responseError(commandCode, INVALID_COMMAND);
diff --git a/foreign/node/src/wire/vsr/vsr.test.ts b/foreign/node/src/wire/vsr/vsr.test.ts
index 9149938..61f8564 100644
--- a/foreign/node/src/wire/vsr/vsr.test.ts
+++ b/foreign/node/src/wire/vsr/vsr.test.ts
@@ -94,7 +94,7 @@
       serializeSendMessages(
         1,
         2,
-        [],
+        [{ payload: 'x' }],
         Partitioning.PartitionId(3)
       )
     );
diff --git a/foreign/python/Cargo.toml b/foreign/python/Cargo.toml
index cb6b328..9446a1c 100644
--- a/foreign/python/Cargo.toml
+++ b/foreign/python/Cargo.toml
@@ -17,7 +17,7 @@
 
 [package]
 name = "apache-iggy"
-version = "0.9.0-dev3"
+version = "0.9.0-dev4"
 edition = "2024"
 authors = ["Iggy Committers <dev@iggy.apache.org>"]
 license = "Apache-2.0"
diff --git a/foreign/python/pyproject.toml b/foreign/python/pyproject.toml
index 7f29630..00903f9 100644
--- a/foreign/python/pyproject.toml
+++ b/foreign/python/pyproject.toml
@@ -22,7 +22,7 @@
 [project]
 name = "apache-iggy"
 requires-python = ">=3.10"
-version = "0.9.0.dev3"
+version = "0.9.0.dev4"
 description = "Apache Iggy is the persistent message streaming platform written in Rust, supporting QUIC, TCP and HTTP transport protocols, capable of processing millions of messages per second."
 readme = "README.md"
 license = { file = "LICENSE" }
diff --git a/foreign/python/uv.lock b/foreign/python/uv.lock
index d263c59..739fc1c 100644
--- a/foreign/python/uv.lock
+++ b/foreign/python/uv.lock
@@ -12,7 +12,7 @@
 
 [[package]]
 name = "apache-iggy"
-version = "0.9.0.dev3"
+version = "0.9.0.dev4"
 source = { editable = "." }
 
 [package.optional-dependencies]
diff --git a/web/package.json b/web/package.json
index eb2e63f..7dd852f 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
 {
   "name": "iggy-web-ui",
-  "version": "0.4.0-edge.2",
+  "version": "0.4.0-edge.3",
   "private": true,
   "scripts": {
     "dev": "vite dev --port 3050",