diff --git a/CHANGELOG.md b/CHANGELOG.md
index e566e53..74f1020 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,14 @@ never breaks for consumers. `test/Fitz.PackageConsumer` asserts this on every CI
## [Unreleased]
+## [1.4.2] - 2026-09-21
+
+### Fixed
+
+- **`Cntryl.Fitz.Abstractions`:** expose `FitzErrorCodes.StreamConcurrencyConflict` (2001) and
+ `FitzErrorCodes.StreamSessionAlreadyActive` (2002), so callers can classify the broker's
+ structured stream write-contention responses without matching error text.
+
## [1.4.1] - 2026-09-21
### Added
diff --git a/Directory.Build.props b/Directory.Build.props
index d52ec64..a6ad2e9 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -11,7 +11,7 @@
- 1.4.1
+ 1.4.2
$(PackageVersion)
1.0.0.0
$(PackageVersion)
@@ -26,7 +26,7 @@
portable
README.md
fitz distributed-systems kv queue rpc lease notice stream schedule
- Adds bounded read-only primary-key paging for Cntryl.Fitz.Extensions directories. See CHANGELOG.md.
+ Exposes named stream write-contention error codes for broker codes 2001 and 2002. See CHANGELOG.md.
$(MSBuildThisFileDirectory)artifacts\packages
diff --git a/docs/guide.md b/docs/guide.md
index 6172761..a1a4b16 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -125,9 +125,10 @@ catch (RpcException ex) when (ex.DomainCode == FitzErrorCodes.RpcTimeout)
}
```
-`FitzErrorCodes` lists every code the broker defines. Transport and lifecycle problems surface
-as `ConnectionException`, `AuthenticationException`, `RequestTimeoutException`,
-`RequestQueueFullException`, or `ProtocolException`.
+`FitzErrorCodes` exposes named constants for the broker codes the SDK classifies. Other
+structured domain codes remain available on an exception's `DomainCode`; never match error text.
+Transport and lifecycle problems surface as `ConnectionException`, `AuthenticationException`,
+`RequestTimeoutException`, `RequestQueueFullException`, or `ProtocolException`.
Two exceptions sit outside that hierarchy and derive from `Exception` directly:
`SubscriptionBackpressureException` and `AsyncHandlerOverflowException`. They are declared in
@@ -313,8 +314,11 @@ await session.CommitAsync(ct);
```
`AppendAsync` takes the offset you expect to write at; a mismatch is an optimistic-concurrency
-failure, reported as `StreamException` with `DomainCode` 2001. Classify on that code, never on
-the message.
+failure, reported as `StreamException` with `DomainCode ==
+FitzErrorCodes.StreamConcurrencyConflict`. `BeginAsync` reports
+`FitzErrorCodes.StreamSessionAlreadyActive` when an append session currently owns the resource
+stream. These are distinct structured write-contention conditions; classify on the
+code, never on the message.
Use `ReadPageAsync` when you need to see what the broker withheld: a `StreamReadPage` contains
`StreamReadItem`s whose `Kind` distinguishes delivered records from filtered ones, so gaps in
diff --git a/src/Fitz.Abstractions/FitzErrorCodes.cs b/src/Fitz.Abstractions/FitzErrorCodes.cs
index af64482..21df8bd 100644
--- a/src/Fitz.Abstractions/FitzErrorCodes.cs
+++ b/src/Fitz.Abstractions/FitzErrorCodes.cs
@@ -30,6 +30,17 @@ public static class FitzErrorCodes
///
public const uint KvSubscriptionLimit = 1013;
+ ///
+ /// An append operation found that the stream's committed position no longer matches its
+ /// expected position.
+ ///
+ public const uint StreamConcurrencyConflict = 2001;
+
+ ///
+ /// An append session is already active for the resource stream.
+ ///
+ public const uint StreamSessionAlreadyActive = 2002;
+
///
/// The stream subscription selector was malformed or unsupported.
///
diff --git a/test/Fitz.Core.Tests/Unit/StreamErrorEnvelopeTests.cs b/test/Fitz.Core.Tests/Unit/StreamErrorEnvelopeTests.cs
index 8e56214..b4277ae 100644
--- a/test/Fitz.Core.Tests/Unit/StreamErrorEnvelopeTests.cs
+++ b/test/Fitz.Core.Tests/Unit/StreamErrorEnvelopeTests.cs
@@ -5,10 +5,20 @@ namespace Cntryl.Fitz.Core.Tests.Unit;
public sealed class StreamErrorEnvelopeTests
{
+ [Fact]
+ public void ShouldMatchWireProtocolGivenStreamWriteContentionCodesWhenValidated()
+ {
+ // Arrange
+ // Act
+ // Assert
+ Assert.Equal(2001u, FitzErrorCodes.StreamConcurrencyConflict);
+ Assert.Equal(2002u, FitzErrorCodes.StreamSessionAlreadyActive);
+ }
+
[Theory]
- [InlineData("APPEND", 2001u, "unrelated wording")]
- [InlineData("COMMIT", 2001u, "unrelated wording")]
- [InlineData("APPEND", 2002u, "concurrency conflict")]
+ [InlineData("APPEND", FitzErrorCodes.StreamConcurrencyConflict, "unrelated wording")]
+ [InlineData("COMMIT", FitzErrorCodes.StreamConcurrencyConflict, "unrelated wording")]
+ [InlineData("BEGIN", FitzErrorCodes.StreamSessionAlreadyActive, "unrelated wording")]
[InlineData("COMMIT", 2012u, "backend unavailable")]
[InlineData("BEGIN", 2003u, "session unavailable")]
[InlineData("ROLLBACK", 2003u, "session unavailable")]
diff --git a/test/Fitz.PackageConsumer/Program.cs b/test/Fitz.PackageConsumer/Program.cs
index 4d6fe73..1303e7d 100644
--- a/test/Fitz.PackageConsumer/Program.cs
+++ b/test/Fitz.PackageConsumer/Program.cs
@@ -56,6 +56,16 @@
throw new InvalidOperationException("RPC error code constants must cover the canonical 6001-6013 range.");
}
+uint[] streamWriteContentionCodes =
+[
+ FitzErrorCodes.StreamConcurrencyConflict,
+ FitzErrorCodes.StreamSessionAlreadyActive,
+];
+if (!streamWriteContentionCodes.SequenceEqual(Enumerable.Range(2001, 2).Select(static code => (uint)code)))
+{
+ throw new InvalidOperationException("Stream write-contention error code constants must cover the canonical 2001-2002 range.");
+}
+
static async Task CompilePreviewApiAsync(
INoticeClient notice,