Vibed, trust at your own peril.
# 9P2026 Protocol Specification
**Status:** Draft
**Extends:** 9P2000
**Version string:** `9P2026`
---
## 1. Overview
9P2026 is a backward-compatible extension of 9P2000 that modernizes the wire format and adds
targeted capabilities for parallel operation, amortized writes, bulk directory reads, and
filesystem event notification. It preserves Plan 9's core ethos: simplicity of mechanism,
explicitness of semantics, and composition through fileservers rather than protocol complexity.
### 1.1 Design Principles
- No new hidden state. All new capabilities are requested explicitly by the client and confirmed
by the server.
- Servers that implement a subset of extensions remain valid. Clients discover capability through
negotiation and error responses, not capability bitmasks.
- The protocol remains transport-agnostic. No changes depend on shared memory, OS-specific
mechanisms, or anything below the message boundary.
- Event notification is a convention over existing read semantics, not a new message class.
### 1.2 Changes from 9P2000
| Area | Change |
|---|---|
| Tag field | u16 → u32 |
| Timestamps | u32 (epoch seconds) → u64 (nanoseconds since epoch) |
| Open flags | New `OASYNC` flag |
| New messages | `Treaddir`/`Rreaddir`, `Trenegotiate`/`Rrenegotiate`, `Tsync`/`Rsync` |
| Pipelining | Mandated for servers; servers MUST support out-of-order response |
| Events | Convention over blocking `Tread` on `events` files (no new messages) |
---
## 2. Wire Format
### 2.1 Primitive Types
All multi-byte integers are little-endian. Sizes are in bytes unless noted.
| Notation | Type | Size |
|---|---|---|
| `u8` | unsigned 8-bit integer | 1 |
| `u16` | unsigned 16-bit integer | 2 |
| `u32` | unsigned 32-bit integer | 4 |
| `u64` | unsigned 64-bit integer | 8 |
| `str` | u16 length prefix followed by UTF-8 bytes | 2 + n |
| `qid` | See §2.3 | 13 |
| `stat` | See §2.4 | variable |
### 2.2 Message Header
Every message begins with:
```
size[4] u32 Total message size including this field
type[1] u8 Message type (see §3)
tag[4] u32 Client-assigned request identifier
```
**Note:** The tag field is 4 bytes in 9P2026, compared to 2 bytes in 9P2000. This change takes
effect only after successful `Tversion`/`Rversion` negotiation of `9P2026`. During `Tversion`
itself the tag MUST be `NOTAG` (see §2.6); the 2-byte vs 4-byte distinction does not apply to
that message.
**NOTAG:** The reserved tag value for messages that precede tag assignment is `0xFFFFFFFF`.
**NOFID:** The reserved fid value indicating no fid is `0xFFFFFFFF` (unchanged from 9P2000).
### 2.3 Qid
A Qid uniquely identifies a file on a server. Structure is unchanged from 9P2000:
```
type[1] u8 File type flags (see below)
vers[4] u32 Version; incremented on file modification
path[8] u64 Server-unique file identifier
```
Qid type flags:
| Flag | Value | Meaning |
|---|---|---|
| `QTDIR` | 0x80 | Directory |
| `QTAPPEND` | 0x40 | Append-only file |
| `QTEXCL` | 0x20 | Exclusive-use file |
| `QTMOUNT` | 0x10 | Mounted channel |
| `QTAUTH` | 0x08 | Auth file |
| `QTTMP` | 0x04 | Non-backed (temporary) file |
| `QTSYMLINK` | 0x02 | Symbolic link (9P2000.u extension) |
| `QTFILE` | 0x00 | Plain file |
### 2.4 Stat
The stat structure is used in `Rstat`, `Twstat`, and `Rreaddir`. In 9P2026, timestamp fields
are 64-bit nanoseconds since the Unix epoch (January 1, 1970 00:00:00 UTC).
```
size[2] u16 Total byte count of the following fields
type[2] u16 For kernel use
dev[4] u32 For kernel use
qid[13] qid Server's unique identifier for the file
mode[4] u32 Permissions and flags (see §2.5)
atime[8] u64 Last access time, nanoseconds since epoch
mtime[8] u64 Last modification time, nanoseconds since epoch
length[8] u64 Length of file in bytes
name str File name; "/" for root
uid str Owner name
gid str Group name
muid str Name of user who last modified the file
```
The `size` field counts all bytes after itself. Servers MUST set `type` and `dev` to zero;
clients MUST ignore them.
**Permissions (mode field):**
| Bit | Meaning |
|---|---|
| 0x80000000 | Directory |
| 0x40000000 | Append-only |
| 0x20000000 | Exclusive use |
| 0x04000000 | Authentication file |
| 0x00000100 | Owner read |
| 0x00000080 | Owner write |
| 0x00000040 | Owner execute |
| 0x00000020 | Group read |
| 0x00000010 | Group write |
| 0x00000008 | Group execute |
| 0x00000004 | Other read |
| 0x00000002 | Other write |
| 0x00000001 | Other execute |
### 2.5 Open Mode Flags
Used in `Topen` and `Tcreate`:
| Flag | Value | Meaning |
|---|---|---|
| `OREAD` | 0x00 | Open for reading |
| `OWRITE` | 0x01 | Open for writing |
| `ORDWR` | 0x02 | Open for reading and writing |
| `OEXEC` | 0x03 | Open for execution |
| `OTRUNC` | 0x10 | Truncate file on open |
| `OCEXEC` | 0x20 | Close on exec |
| `ORCLOSE` | 0x40 | Remove on clunk |
| `OASYNC` | 0x80 | Async write mode (9P2026) |
`OASYNC` may be combined with `OWRITE` or `ORDWR`. Its semantics are specified in §6.
### 2.6 Special Tag Values
| Value | Meaning |
|---|---|
| `0xFFFFFFFF` | `NOTAG` — used for `Tversion` only |
---
## 3. Message Types
### 3.1 Type Assignments
T-messages (client to server) have even type values. R-messages (server to client, responses)
have odd values equal to T-message type plus one.
| Value | Name | Description |
|---|---|---|
| 100 | `Tversion` | Negotiate protocol version and msize |
| 101 | `Rversion` | |
| 102 | `Tauth` | Request auth fid |
| 103 | `Rauth` | |
| 104 | `Tattach` | Attach to server |
| 105 | `Rattach` | |
| 106 | — | Reserved (Terror, unused) |
| 107 | `Rerror` | Error response to any T-message |
| 108 | `Tflush` | Abort a pending request |
| 109 | `Rflush` | |
| 110 | `Twalk` | Walk directory tree |
| 111 | `Rwalk` | |
| 112 | `Topen` | Open a fid |
| 113 | `Ropen` | |
| 114 | `Tcreate` | Create a file |
| 115 | `Rcreate` | |
| 116 | `Tread` | Read from a fid |
| 117 | `Rread` | |
| 118 | `Twrite` | Write to a fid |
| 119 | `Rwrite` | |
| 120 | `Tclunk` | Forget a fid |
| 121 | `Rclunk` | |
| 122 | `Tremove` | Remove a file |
| 123 | `Rremove` | |
| 124 | `Tstat` | Request file metadata |
| 125 | `Rstat` | |
| 126 | `Twstat` | Write file metadata |
| 127 | `Rwstat` | |
| 128 | `Treaddir` | Read directory entries with stat (9P2026) |
| 129 | `Rreaddir` | |
| 130 | `Trenegotiate` | Renegotiate msize (9P2026) |
| 131 | `Rrenegotiate` | |
| 132 | `Tsync` | Flush buffered async writes (9P2026) |
| 133 | `Rsync` | |
---
## 4. Existing Messages
This section documents all 9P2000 messages with their 9P2026 wire formats. The only structural
change to existing messages is the tag field width (u16 → u32) and timestamp width in stat
(u32 → u64).
### 4.1 Tversion / Rversion
Negotiate protocol version and maximum message size.
```
Tversion
size[4] u32
type[1] u8 = 100
tag[4] u32 MUST be NOTAG (0xFFFFFFFF)
msize[4] u32 Client's proposed maximum message size
version str "9P2026"
Rversion
size[4] u32
type[1] u8 = 101
tag[4] u32 NOTAG
msize[4] u32 Agreed maximum message size (≤ client's proposed)
version str "9P2026" if accepted, "unknown" if rejected
```
The server MUST respond with a version string of `"9P2026"` if it supports this specification,
or `"unknown"` if it does not, in which case the client MAY retry with `"9P2000"` using 2-byte
tags.
The agreed `msize` applies to all subsequent messages in both directions, including the header.
Neither side may send a message larger than `msize` bytes. Minimum `msize` is 256.
A `Tversion` resets the connection: all outstanding fids are clunked and all pending requests
are aborted.
### 4.2 Tauth / Rauth
Request an auth fid prior to attach. May be elided if the server does not require
authentication.
```
Tauth
size[4] u32
type[1] u8 = 102
tag[4] u32
afid[4] u32 Fid to use for auth protocol
uname str User name
aname str Attach target name
Rauth
size[4] u32
type[1] u8 = 103
tag[4] u32
aqid[13] qid Qid of the auth file
```
If the server does not require authentication it MUST return `Rerror` in response to `Tauth`.
The client then attaches without an afid (using `NOFID`).
### 4.3 Tattach / Rattach
Establish a session root.
```
Tattach
size[4] u32
type[1] u8 = 104
tag[4] u32
fid[4] u32 Fid to assign to the root
afid[4] u32 Auth fid, or NOFID
uname str User name
aname str Attach target name
Rattach
size[4] u32
type[1] u8 = 105
tag[4] u32
qid[13] qid Qid of the attach root
```
### 4.4 Rerror
Returned by the server in place of any R-message to indicate failure.
```
Rerror
size[4] u32
type[1] u8 = 107
tag[4] u32 Tag of the failing T-message
ename str Error description
```
The tag in `Rerror` MUST match the tag of the `T`-message that caused the error. The `ename`
is a UTF-8 string; no error code is defined at the protocol level.
### 4.5 Tflush / Rflush
Abort a pending T-message.
```
Tflush
size[4] u32
type[1] u8 = 108
tag[4] u32
oldtag[4] u32 Tag of the message to abort
Rflush
size[4] u32
type[1] u8 = 109
tag[4] u32
```
The server MUST respond to `Tflush` with `Rflush`. It MUST NOT subsequently send a response
for the flushed tag. If the original request completed before the flush arrived, the server
sends `Rflush` anyway and the original response is discarded. The client MUST NOT reuse the
old tag until `Rflush` is received.
A `Tflush` for an async-write fid (see §6) does not imply `Tsync`; buffered writes that have
not yet been sent MAY be discarded.
### 4.6 Twalk / Rwalk
Walk a sequence of path elements from an existing fid, assigning a new fid at the walk target.
```
Twalk
size[4] u32
type[1] u8 = 110
tag[4] u32
fid[4] u32 Starting fid (must not be open)
newfid[4] u32 Fid to assign at walk target; may equal fid
nwname[2] u16 Number of path elements (0..16)
nwname × wname str Path elements
Rwalk
size[4] u32
type[1] u8 = 111
tag[4] u32
nwqid[2] u16 Number of qids returned (0..nwname)
nwqid × wqid qid Qid for each successfully walked element
```
If `nwname` is 0, the walk clones `fid` into `newfid` without moving. If `nwname > 0` and
`nwqid < nwname`, the walk partially succeeded; `newfid` is not created. If `nwqid == nwname`
the walk fully succeeded and `newfid` is set to the final element.
Maximum path elements per walk: 16. Clients that need to walk deeper must chain multiple walks.
### 4.7 Topen / Ropen
Open a fid for I/O.
```
Topen
size[4] u32
type[1] u8 = 112
tag[4] u32
fid[4] u32
mode[1] u8 Open mode flags (see §2.5)
Ropen
size[4] u32
type[1] u8 = 113
tag[4] u32
qid[13] qid Qid of the opened file
iounit[4] u32 Suggested I/O unit; 0 means no suggestion
```
`iounit` is advisory. If non-zero it indicates the maximum size of a guaranteed-atomic read or
write. Requests larger than `iounit` may be split by the server.
If `OASYNC` is set in `mode`, async write semantics apply (§6). The server MUST return `Rerror`
if it does not support `OASYNC`.
### 4.8 Tcreate / Rcreate
Create a new file and open it.
```
Tcreate
size[4] u32
type[1] u8 = 114
tag[4] u32
fid[4] u32 Fid of the directory in which to create
name str Name of the new file
perm[4] u32 Permission bits
mode[1] u8 Open mode flags
Rcreate
size[4] u32
type[1] u8 = 115
tag[4] u32
qid[13] qid Qid of the new file
iounit[4] u32
```
After `Rcreate`, `fid` refers to the newly created file, not the directory. `perm` is
ANDed with the parent directory's permissions. If the `DMDIR` bit (0x80000000) is set
in `perm`, a directory is created and `mode` MUST be `OREAD`.
### 4.9 Tread / Rread
Read data from an open fid.
```
Tread
size[4] u32
type[1] u8 = 116
tag[4] u32
fid[4] u32
offset[8] u64 Byte offset for the read
count[4] u32 Maximum bytes to return
Rread
size[4] u32
type[1] u8 = 117
tag[4] u32
count[4] u32 Bytes returned
data[count] u8[]
```
A `Tread` on a directory returns stat records sequentially packed. Clients must use `offset`
and the byte count returned to iterate. Prefer `Treaddir` (§5.1) over `Tread` on directories.
A `Tread` returning `count == 0` indicates EOF.
For files opened `OASYNC`, a `Tread` on the same fid is permitted; the server MUST ensure the
read reflects all previously acknowledged writes (i.e., writes for which `Rwrite` has been sent)
but need not reflect buffered writes not yet acked.
Reads on stream resources (pipes, network connections) block until data is available. `count
== 0` indicates the stream is closed at the remote end.
### 4.10 Twrite / Rwrite
Write data to an open fid.
```
Twrite
size[4] u32
type[1] u8 = 118
tag[4] u32
fid[4] u32
offset[8] u64 Byte offset for the write
count[4] u32 Bytes to write
data[count] u8[]
Rwrite
size[4] u32
type[1] u8 = 119
tag[4] u32
count[4] u32 Bytes written
```
For files opened without `OASYNC`, the server MUST NOT respond with `Rwrite` until the data is
durable (or as durable as the server's backing store guarantees). This is unchanged from 9P2000.
For files opened with `OASYNC`, semantics differ; see §6.
Servers MAY return a `count` less than requested. Clients MUST retry with the remainder.
### 4.11 Tclunk / Rclunk
Release a fid. After `Rclunk` the fid is invalid.
```
Tclunk
size[4] u32
type[1] u8 = 120
tag[4] u32
fid[4] u32
Rclunk
size[4] u32
type[1] u8 = 121
tag[4] u32
```
Clunking a fid opened `OASYNC` does NOT imply sync. Buffered writes not yet acknowledged by
the server MAY be discarded. Clients MUST call `Tsync` before `Tclunk` if they require
durability.
### 4.12 Tremove / Rremove
Remove a file and clunk its fid. The fid is invalid after `Rremove` regardless of success or
failure.
```
Tremove
size[4] u32
type[1] u8 = 122
tag[4] u32
fid[4] u32
Rremove
size[4] u32
type[1] u8 = 123
tag[4] u32
```
### 4.13 Tstat / Rstat
Request metadata for a file.
```
Tstat
size[4] u32
type[1] u8 = 124
tag[4] u32
fid[4] u32
Rstat
size[4] u32
type[1] u8 = 125
tag[4] u32
nstat[2] u16 Byte count of the following stat (redundant per spec, retained)
stat stat See §2.4
```
### 4.14 Twstat / Rwstat
Modify metadata for a file. Fields set to their "don't care" values are left unchanged by the
server.
```
Twstat
size[4] u32
type[1] u8 = 126
tag[4] u32
fid[4] u32
nstat[2] u16 Byte count of the following stat
stat stat See §2.4
```
Don't-care values:
| Field | Don't-care |
|---|---|
| name | empty string |
| uid, gid, muid | empty string |
| mode | 0xFFFFFFFF |
| atime, mtime | 0xFFFFFFFFFFFFFFFF |
| length | 0xFFFFFFFFFFFFFFFF |
| qid, type, dev | ignored |
```
Rwstat
size[4] u32
type[1] u8 = 127
tag[4] u32
```
---
## 5. New Messages
### 5.1 Treaddir / Rreaddir
Read directory entries with full stat data in a single round trip. `fid` must refer to an open
directory (opened with `OREAD`).
```
Treaddir
size[4] u32
type[1] u8 = 128
tag[4] u32
fid[4] u32 An open directory fid
offset[8] u64 Byte offset into the directory stream
count[4] u32 Maximum bytes of stat data to return
Rreaddir
size[4] u32
type[1] u8 = 129
tag[4] u32
count[4] u32 Bytes of stat data returned
data[count] u8[] Sequence of packed stat records (§2.4)
```
The `data` field is a sequence of `stat` records packed end-to-end, each preceded by its
`size[2]` field. Clients iterate by consuming `size` bytes at a time.
`offset` is an opaque byte offset into the server's directory stream, not an entry index.
Clients MUST use the byte offset of the last consumed record plus its size as the offset for
the next `Treaddir`. The only valid starting offset is 0.
`count == 0` in `Rreaddir` indicates the end of the directory.
Servers that do not implement `Treaddir` MUST return `Rerror`. Clients MUST fall back to
`Tread`/`Tstat` in that case.
**Interaction with concurrent modification:** Servers provide no snapshot guarantee. Entries
added or removed during iteration may appear or be absent. Clients MUST tolerate this.
### 5.2 Trenegotiate / Rrenegotiate
Renegotiate `msize` mid-session without resetting connection state.
```
Trenegotiate
size[4] u32
type[1] u8 = 130
tag[4] u32
msize[4] u32 Proposed new msize
Rrenegotiate
size[4] u32
type[1] u8 = 131
tag[4] u32
msize[4] u32 Agreed new msize (≤ proposed)
```
The agreed `msize` takes effect immediately upon the client receiving `Rrenegotiate`. All
messages sent after that point, in both directions, MUST conform to the new `msize`.
A client SHOULD NOT issue `Trenegotiate` while requests other than `Tflush` are in flight.
The server MAY return `Rerror` if outstanding requests exist. After `Rrenegotiate` all
existing fids remain valid.
Servers that do not support `Trenegotiate` MUST return `Rerror`. The previous `msize` remains
in effect.
Minimum `msize`: 256. Neither side may propose an `msize` smaller than 256.
### 5.3 Tsync / Rsync
Flush all buffered async writes on a fid to durable storage.
```
Tsync
size[4] u32
type[1] u8 = 132
tag[4] u32
fid[4] u32 An open fid with OASYNC
Rsync
size[4] u32
type[1] u8 = 133
tag[4] u32
```
The server MUST NOT respond with `Rsync` until all writes previously issued on `fid` for which
`Rwrite` has been sent are durable. After `Rsync` returns, the file's data is as persistent as
the server's backing store guarantees.
`Tsync` on a fid not opened `OASYNC` is a no-op: the server returns `Rsync` immediately.
`Tsync` on a non-file fid (e.g. a directory) MUST return `Rerror`.
Durability guarantee: `Rsync` provides the same durability guarantee as a synchronous `Rwrite`
in standard 9P2000. It is not stronger.
---
## 6. Async Write Mode (OASYNC)
### 6.1 Semantics
When `OASYNC` is set in `Topen` or `Tcreate`, the server MAY return `Rwrite` before the data
is durable. The server takes ownership of the data as soon as `Rwrite` is sent; the client
MUST NOT assume the data is durable until a subsequent `Rsync`.
This enables a client to maintain a pipeline of outstanding `Twrite` messages without waiting
for each `Rwrite`. Combined with the mandated pipelining (§7), this is the primary mechanism
for improving raw write throughput.
### 6.2 Client Obligations
- The client MUST call `Tsync` before `Tclunk` if durability is required.
- The client MUST call `Tsync` before relying on the written data being visible to other clients
or after a crash.
- The client MUST NOT reuse the data buffer for a write until it has received the corresponding
`Rwrite`.
### 6.3 Server Obligations
- The server MUST accept all data immediately (or return `Rerror`). It MUST NOT block `Rwrite`
waiting for I/O to complete.
- The server MUST ensure that data for which `Rwrite` has been sent will survive at least until
`Tsync` is responded to. If the server loses buffered data (e.g. due to resource exhaustion)
before `Tsync`, it MUST return `Rerror` to the subsequent `Tsync`.
- The server MUST handle concurrent `Twrite` messages on the same async fid and apply them in
offset order. Out-of-order `Twrite` messages for non-overlapping ranges are permitted; the
server MUST apply all of them.
### 6.4 Interaction with Tread
A `Tread` on a fid opened `OASYNC` reflects all writes for which `Rwrite` has been returned.
It need not reflect writes still in flight (i.e. for which no `Rwrite` has been sent).
### 6.5 Typical Write Pipeline Pattern
```
Topen(fid, OWRITE|OASYNC) → Ropen
Twrite(fid, offset=0, data=chunk0) ─┐
Twrite(fid, offset=N, data=chunk1) ├─ all in flight simultaneously
Twrite(fid, offset=2N, data=chunk2) ─┘
← Rwrite(count=N)
← Rwrite(count=N)
← Rwrite(count=N)
Tsync(fid) → Rsync ← durable
Tclunk(fid) → Rclunk
```
---
## 7. Pipelining
### 7.1 Requirement
In 9P2026, servers MUST support out-of-order responses. A server MUST be able to process
multiple in-flight T-messages concurrently and MAY respond to them in any order, using the
`tag` field to correlate responses.
This is a hard requirement, not an option. Clients may rely on it.
### 7.2 Tag Space
Tags are u32. The valid range for client-assigned tags is 0x00000000 through 0xFFFFFFFE.
Tag 0xFFFFFFFF is reserved as `NOTAG`.
Clients MUST NOT reuse a tag until they have received the corresponding R-message (or
`Rflush` for a flushed request).
### 7.3 Ordering Guarantees
Despite out-of-order responses being permitted:
- A server MUST process `Twrite` messages on the same fid in the order received relative to
each other, regardless of response order. (Writes on the same fid are sequenced by arrival.)
- `Tread` on the same fid is sequenced with respect to `Twrite` on that fid by arrival order.
- Operations on different fids have no ordering guarantees relative to each other.
### 7.4 Head-of-Line Blocking Guidance
Clients SHOULD use separate fids for large data transfers and for metadata operations to avoid
a large write pipeline stalling small stat requests. The tag system allows the server to
interleave responses but servers are not required to preempt in-progress large writes.
---
## 8. Event Notification Convention
9P2026 defines no new messages for filesystem change notification. Instead, servers that wish
to expose events SHOULD do so via a synthetic readable file named `events` within any directory
that supports notification. This is a convention; clients discover it by attempting to open the
file.
### 8.1 The events File
- The `events` file appears in directory listings like any other file.
- It is opened with `OREAD`.
- `Tread` on an open `events` fid blocks until at least one event is available, then returns
one or more event records.
- `Tread` returning `count == 0` indicates the events stream has closed (e.g. the server is
shutting down).
- Multiple clients may open the same `events` file; each receives an independent event stream.
### 8.2 Event Record Format
Each record returned in a `Tread` response:
```
reclen[2] u16 Byte length of this record (not including reclen itself)
evtype[2] u16 Event type (see below)
mtime[8] u64 Nanosecond timestamp of the event
namelen[2] u16 Byte length of name
name[namelen] u8[] Name of affected file (relative to the directory, UTF-8)
```
Records are packed consecutively in the `Rread` data field. Clients consume `reclen` bytes per
record.
Event types:
| Value | Name | Meaning |
|---|---|---|
| 0x0001 | `EVCREATE` | File or directory created |
| 0x0002 | `EVDELETE` | File or directory removed |
| 0x0003 | `EVMODIFY` | File data modified |
| 0x0004 | `EVATTR` | File metadata modified (stat changed) |
| 0x0005 | `EVRENAME` | File renamed within this directory |
For `EVRENAME`, a second record with the new name is appended immediately after, with the same
`evtype`, `mtime`, and `reclen`. Clients detect this by the pair of consecutive `EVRENAME`
records with matching `mtime`.
### 8.3 Multiplexing Events Across Directories
Clients that wish to watch multiple directories concurrently SHOULD open each directory's
`events` file in a separate process and communicate results over channels, using the
`ioproc`/`alt` pattern. No single-fid multiplexing is provided at the protocol level.
### 8.4 Non-Implementing Servers
Servers that do not support events MUST return `Rerror` on `Topen` of the `events` file.
Servers MUST NOT expose a plain file named `events` that is not an event stream; if a real
file with that name would conflict, the server should choose a different synthetic name
(e.g. `.events`) and document it.
---
## 9. Compatibility
### 9.1 Version Detection
A client that wishes to support both 9P2000 and 9P2026 SHOULD:
1. Send `Tversion` with version `"9P2026"` and the 4-byte tag field.
2. If the server responds with `"9P2026"`, proceed with this specification.
3. If the server responds with `"unknown"`, reconnect and send `Tversion` with version
`"9P2000"` and the 2-byte tag field per the original spec.
Because `Tversion` uses `NOTAG` (all ones) for its tag, the 2-byte vs 4-byte distinction is
unambiguous in the initial handshake: a 9P2026 client sends a 9-byte fixed header
(`size[4] type[1] tag[4]`) while a 9P2000 client sends a 7-byte fixed header
(`size[4] type[1] tag[2]`). The `size` field disambiguates these in practice.
### 9.2 Extension Fallback
Each 9P2026 extension can degrade gracefully:
| Feature | Fallback |
|---|---|
| `Treaddir` | `Tread` directory + `Tstat` per entry |
| `Trenegotiate` | Set msize conservatively at session start |
| `OASYNC` | Open without `OASYNC`; issue synchronous writes |
| `Tsync` | Not needed without `OASYNC` |
| `events` file | Poll with `Tstat` |
### 9.3 Servers Implementing a Subset
A 9P2026 server MAY decline to implement any extension by returning `Rerror` to the relevant
message. Advertising `"9P2026"` in `Rversion` commits the server only to the wire format
changes (tag width, timestamp width) and the pipelining mandate. All extension messages are
individually optional from the server's perspective, with defined fallback behavior.
---
## 10. Summary of Wire Format Differences from 9P2000
| Field | 9P2000 | 9P2026 |
|---|---|---|
| `tag` in header | u16 (2 bytes) | u32 (4 bytes) |
| `NOTAG` | 0xFFFF | 0xFFFFFFFF |
| `atime` in stat | u32 seconds | u64 nanoseconds |
| `mtime` in stat | u32 seconds | u64 nanoseconds |
| `OASYNC` open flag | not defined | 0x80 |
| New message types | — | 128–133 |
| Pipelining | implementation-defined | mandatory |