diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 17998ff..35e50fb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -134,7 +134,7 @@ jobs:
- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal
- - name: Test against Eve 0.27.6
+ - name: Test against the pinned Eve fixture
env:
EVE_PROBE_NO_BUILD: 1
run: npm run test:client --prefix test/fixtures/eve-agent
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4bf1197..9eec98c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -148,7 +148,7 @@ jobs:
- name: Test
run: dotnet test --configuration Release --no-build --verbosity normal
- - name: Test against Eve 0.27.6
+ - name: Test against the pinned Eve fixture
env:
EVE_PROBE_NO_BUILD: 1
run: npm run test:client --prefix test/fixtures/eve-agent
diff --git a/AGENTS.md b/AGENTS.md
index 6a5b7ae..a4c4816 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -36,7 +36,7 @@ and structured-output support without using the TypeScript SDK.
- `EveStreamFollower` mirrors upstream absolute-index reconnect and idle-budget behavior.
- Preview event and inspection payloads retain raw `JsonElement` values so unknown
upstream fields and event types remain available.
-- The compatibility baseline is Vercel eve 0.27.6, stream protocol version 19.
+- The compatibility baseline is Vercel eve 0.29.4, stream protocol version 20.
- `test/fixtures/eve-agent` is a pinned, deterministic real Eve server used by the
compatibility probe under `tests/NexusLabs.Eve.CompatibilityProbe`.
- `version.json` is the only release-version source; package versions come from NBGV.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 61858df..cf50c2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,13 +16,25 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
- `EveStreamOptions.Follow` for bounded catch-up reads that stop at the durable stream tail
observed when the stream opens, using the `includeTailIndex=1` query parameter and the
`x-eve-stream-tail-index` response header.
+- `EveStreamEventMetadata.Id` projecting the stable `evt_`-prefixed identifier that
+ message-stream protocol `20` stamps on every persisted event, plus
+ `EveStreamEventDeduplicator` for dropping re-delivered events across reconnects and
+ rewinds. Events persisted under protocol `19` report `null` and are always admitted.
+- `EveInputRequest.Kind` and `EveInputRequest.RawKind`, projecting eve's framework-owned
+ input-request discriminator through the `EveInputRequestKind` enum so `question`,
+ `tool-approval`, and `session-limit` requests are routed by contract instead of by
+ option shape. A server that predates the discriminator reports `Unknown` with a `null`
+ raw value.
### Changed
-- The compatibility reference moved to eve `0.27.6` (message-stream protocol `19`).
+- The compatibility reference moved to eve `0.29.4` (message-stream protocol `20`). The
+ pinned CI fixture runs that release, and the compatibility probe now verifies stamped
+ event identifiers, a real bounded catch-up read against the durable tail header, and an
+ approval-gated human-input pause end to end.
- Accepted session IDs and continuation tokens are persisted in `EveSession.State`
as soon as `SendAsync` returns, before the response stream is consumed.
-- Non-protected per-request headers now override client-level values, matching eve `0.27.6`.
+- Non-protected per-request headers now override client-level values, matching upstream eve.
Authentication-owned and explicitly protected headers remain authoritative by default and
require an allowlisted, dedicated per-call override.
diff --git a/README.md b/README.md
index 42b44bd..731eac4 100644
--- a/README.md
+++ b/README.md
@@ -20,9 +20,9 @@ health and agent inspection, authentication, durable sessions, human-input respo
cooperative cancellation, session reset, NDJSON streaming, reconnect-by-index,
attachments, and structured output.
-The initial compatibility target is Vercel `eve` **0.27.6** at commit
-`05f348023d4268c974c225c1189a283ace20b742`, whose message stream protocol is version
-**19**. eve is still a preview, so pin and test compatible versions before upgrading.
+The initial compatibility target is Vercel `eve` **0.29.4** at commit
+`85c1dd7a647a04cc1bd74879ba8d27a3ba0bdd9d`, whose message stream protocol is version
+**20**. eve is still a preview, so pin and test compatible versions before upgrading.
## Prerequisites
diff --git a/docs/attachments-and-input.md b/docs/attachments-and-input.md
index 3a7b785..8b55861 100644
--- a/docs/attachments-and-input.md
+++ b/docs/attachments-and-input.md
@@ -67,7 +67,7 @@ distinguishes them:
|---|---|---|
| `Question`, `ToolApproval`, `SessionLimit` | matching wire value | A modelled request kind |
| `Unknown` | the wire value | A newer eve emitted a kind this package does not model |
-| `Unknown` | `null` | The server predates the discriminator, such as eve `0.27.6` |
+| `Unknown` | `null` | The server predates the discriminator, such as eve `0.27.6` or earlier |
A `kind` that is present but not a string is a malformed request and throws
`EveProtocolException` rather than being reported as a legacy server.
diff --git a/docs/authentication.md b/docs/authentication.md
index c6d3599..10606b7 100644
--- a/docs/authentication.md
+++ b/docs/authentication.md
@@ -142,7 +142,7 @@ credentials, even when that name is allowlisted.
## Migration
Existing consumers keep authentication-authoritative behavior by default. To opt into the
-eve 0.27.6 identity-forwarding use case:
+upstream eve identity-forwarding use case:
1. Identify the exact protected header that must be replaceable.
2. Add only that name to `AllowedProtectedHeaderOverrides`.
diff --git a/docs/compatibility.md b/docs/compatibility.md
index 6c22b0d..0c062fa 100644
--- a/docs/compatibility.md
+++ b/docs/compatibility.md
@@ -6,7 +6,8 @@ description: Understand supported eve versions, stream protocol compatibility, a
| NexusLabs.Eve | Reference eve | Stream protocol | Status |
|---|---:|---:|---|
-| 0.1.x | 0.27.6 | 19 | Primary compatibility target |
+| 0.1.x | 0.29.4 | 20 | Primary compatibility target |
+| 0.1.x | 0.27.6 | 19 | Previous baseline; tolerated, not gated by CI |
| 0.1.x | 0.24.6 | 19 | End-to-end verified with `bg-eve` |
eve remains preview software. Package upgrades should therefore validate both:
@@ -14,12 +15,17 @@ eve remains preview software. Package upgrades should therefore validate both:
1. The public HTTP route and body contracts.
2. The durable message-stream protocol version and event shapes.
-The repository contains a pinned eve `0.27.6` fixture with a deterministic
+The repository contains a pinned eve `0.29.4` fixture with a deterministic
model. CI builds the real server and verifies health, info, text turns,
-attachment staging, streaming, cooperative cancellation, and session reset
-through the C# client.
+attachment staging, streaming, bounded catch-up reads, cooperative cancellation,
+approval-gated human input, and session reset through the C# client.
-Upstream eve 0.27.6 lets generic per-request headers replace authentication.
+The client stays readable against protocol 19 servers: durable event
+identifiers and input-request discriminators are both projected as absent
+rather than causing a failure. That path is covered by contract tests, not by
+the pinned fixture.
+
+Upstream eve lets generic per-request headers replace authentication.
NexusLabs.Eve requires an explicit client allowlist and dedicated per-call override
for protected headers so existing generic header bags cannot silently replace credentials.
@@ -31,9 +37,9 @@ instead of causing deserialization failure.
Stream protocol version 20 stamps every persisted event with a stable
`evt_`-prefixed identifier. `EveStreamEvent.Metadata.Id` projects it when
present and reports `null` for events persisted under earlier protocol
-versions, which cannot be deduplicated. eve `0.27.6` emits protocol version 19,
-so the pinned compatibility probe asserts that durable timestamps still arrive
-while identifiers stay absent.
+versions, which cannot be deduplicated. The compatibility probe asserts that
+the pinned server stamps a well-formed identifier on every event of a turn and
+never repeats one.
## Upstream parity radar
@@ -50,11 +56,13 @@ deliberately not committed.
Bounded catch-up reads (`EveStreamOptions.Follow = false`) depend on the
`includeTailIndex=1` stream query parameter and the `x-eve-stream-tail-index`
-response header. eve `0.27.6` accepts the query parameter but does not report the
-header, so bounded reads against that baseline fail with `EveProtocolException`
-instead of silently degrading to a live follow. The compatibility probe asserts
-both halves of that contract and switches to verifying a real bounded read once
-the pinned server reports the header.
+response header. The pinned server reports the header, so the compatibility
+probe verifies a real bounded read: the first request asks for the tail,
+reconnects never re-request it, and the read stops exactly at the durable bound
+while advancing the stored cursor. A server that omits the header, or reports a
+malformed or out-of-range value, fails with `EveProtocolException` instead of
+silently degrading to a live follow. eve `0.27.6` accepted the query parameter
+without reporting the header, so bounded reads against that release fail.
## Input request kinds
@@ -63,7 +71,7 @@ eve stamps each human-input request with a framework-owned `kind` of `question`,
`EveInputRequest.RawKind` preserves the wire value, so an unmodelled future kind
stays inspectable instead of being misclassified from its option shape.
-eve `0.27.6` predates the discriminator and omits it, which reports
-`EveInputRequestKind.Unknown` with a `null` raw value. The compatibility probe
-drives a real approval-gated tool against the pinned fixture and asserts that
-behavior end to end.
+The compatibility probe drives a real approval-gated tool against the pinned
+fixture, asserts the request arrives as `tool-approval`, answers it, and
+verifies the turn resumes. A server that predates the discriminator reports
+`EveInputRequestKind.Unknown` with a `null` raw value.
diff --git a/docs/index.md b/docs/index.md
index 68ec682..24b7709 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -24,7 +24,7 @@ part of the .NET API.
## Compatibility
-The initial release targets eve `0.27.6`, message-stream protocol version `19`.
+The initial release targets eve `0.29.4`, message-stream protocol version `20`.
The client has also completed an end-to-end session against a real eve `0.24.6`
application using the same protocol version.
diff --git a/docs/streaming.md b/docs/streaming.md
index 2ba0ab6..e46776e 100644
--- a/docs/streaming.md
+++ b/docs/streaming.md
@@ -32,7 +32,6 @@ retried step is not a replay: it is emitted again under a new identifier.
Events persisted before protocol version 20 carry no identifier and report
`null`, so they cannot be deduplicated. eve `0.27.6` emits protocol version 19
and never stamps one.
-
`EveStreamEventDeduplicator` encodes that contract, so a caller that resumes a
stream can drop events it already processed:
@@ -108,7 +107,7 @@ Bounded reads require a nonnegative effective start cursor, so combining
`Follow = false` with a tail-relative `StartIndex` throws
`ArgumentOutOfRangeException`. A server that omits the tail header, or reports a
malformed or out-of-range value, throws `EveProtocolException`; eve `0.27.6`
-does not report the header yet.
+never reported the header, so bounded reads against that release fail.
## Bound individual events
diff --git a/src/NexusLabs.Eve/EveProtocol.cs b/src/NexusLabs.Eve/EveProtocol.cs
index d2d8e67..6dfd035 100644
--- a/src/NexusLabs.Eve/EveProtocol.cs
+++ b/src/NexusLabs.Eve/EveProtocol.cs
@@ -8,12 +8,12 @@ public static class EveProtocol
///
/// Gets the upstream TypeScript package version used as the compatibility reference.
///
- public const string ReferenceEveVersion = "0.27.6";
+ public const string ReferenceEveVersion = "0.29.4";
///
/// Gets the durable message-stream protocol version used by the reference client.
///
- public const string MessageStreamVersion = "19";
+ public const string MessageStreamVersion = "20";
///
/// Gets the media type returned by eve session streams.
diff --git a/test/fixtures/eve-agent/package-lock.json b/test/fixtures/eve-agent/package-lock.json
index 5b95632..909bad2 100644
--- a/test/fixtures/eve-agent/package-lock.json
+++ b/test/fixtures/eve-agent/package-lock.json
@@ -6,8 +6,8 @@
"": {
"name": "nexuslabs-eve-compatibility-agent",
"dependencies": {
- "ai": "7.0.34",
- "eve": "0.27.6",
+ "ai": "7.0.42",
+ "eve": "0.29.4",
"just-bash": "3.1.0"
},
"engines": {
@@ -15,13 +15,13 @@
}
},
"node_modules/@ai-sdk/gateway": {
- "version": "4.0.26",
- "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.26.tgz",
- "integrity": "sha512-b/nc3COKtk8IxzgcCi418IoZFky/Bw+Jg8+J0/SBBnu/mOXA4bkIKCu81coEtJAWuH2g5Fvvsa0ar7EpmzNWTw==",
+ "version": "4.0.32",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.32.tgz",
+ "integrity": "sha512-U82ZbFEQY80YTyzOEI0jrCjV4Q52uN7/ln/KyI1AvSNR/IX3XwePOfsQWOfDhvmqXkb3TcYbVzWr4p85msKgOw==",
"license": "Apache-2.0",
"dependencies": {
- "@ai-sdk/provider": "4.0.3",
- "@ai-sdk/provider-utils": "5.0.12",
+ "@ai-sdk/provider": "4.0.4",
+ "@ai-sdk/provider-utils": "5.0.15",
"@vercel/oidc": "3.2.0"
},
"engines": {
@@ -32,9 +32,9 @@
}
},
"node_modules/@ai-sdk/provider": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.3.tgz",
- "integrity": "sha512-e0CpNWJUY7OxAFAnCZkw+ri9QOHWwTs1tXP42782KFGCU07qt8NiXCrCVowyCB5dP2r5/Uls+g2oPd8kOJn9dw==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.4.tgz",
+ "integrity": "sha512-tbHKNLirllUNF3ZlkCsXnwab2ZV1Sl4b1H/Cp9ruCce15IBmskE8Gwkk0yo9xDWY+jho2of7lVXtwSsyrq7cwQ==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
@@ -44,15 +44,16 @@
}
},
"node_modules/@ai-sdk/provider-utils": {
- "version": "5.0.12",
- "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.12.tgz",
- "integrity": "sha512-bbhlOgHeYwrIGheLkM6fhS8hVger8uFPmcOLg+kxc9EFh7y30XYorWhthlYAgpadO3SJhFZrIcEknN7qEqEVvA==",
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.15.tgz",
+ "integrity": "sha512-dnDM4/fS17qO+D7LxVU4003V1+8ZDEWgG7rPhvcDNNFs269qLx+Jnm2mTf72ejyjdzu+f0J+rKNSLXWjZWzxwA==",
"license": "Apache-2.0",
"dependencies": {
- "@ai-sdk/provider": "4.0.3",
+ "@ai-sdk/provider": "4.0.4",
"@standard-schema/spec": "^1.1.0",
"@workflow/serde": "4.1.0",
- "eventsource-parser": "^3.0.8"
+ "eventsource-parser": "^3.0.8",
+ "undici": "^7.28.0"
},
"engines": {
"node": ">=22"
@@ -525,14 +526,14 @@
"license": "Apache-2.0"
},
"node_modules/ai": {
- "version": "7.0.34",
- "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.34.tgz",
- "integrity": "sha512-jDqclWYqPGFKcUG4CQiKcBiwi5XqVMqvYy6eJdujVpvE1SDoB6j7RtjrGYz3Bn5B1dzTj1StAlrOebY//WK9/Q==",
+ "version": "7.0.42",
+ "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.42.tgz",
+ "integrity": "sha512-61AEmo8DynuQmfxt1yOJHLJRonCnzK5baFvN6bsDiuOTt/qYk9ZexpbG4aRv3F6rx418BV6Eyblj6fA7UcH5vQ==",
"license": "Apache-2.0",
"dependencies": {
- "@ai-sdk/gateway": "4.0.26",
- "@ai-sdk/provider": "4.0.3",
- "@ai-sdk/provider-utils": "5.0.12"
+ "@ai-sdk/gateway": "4.0.32",
+ "@ai-sdk/provider": "4.0.4",
+ "@ai-sdk/provider-utils": "5.0.15"
},
"engines": {
"node": ">=22"
@@ -813,12 +814,13 @@
}
},
"node_modules/eve": {
- "version": "0.27.6",
- "resolved": "https://registry.npmjs.org/eve/-/eve-0.27.6.tgz",
- "integrity": "sha512-t2Xr/nlVGc0+Fy9fav8p/CJrwMSQM0zyuOGY1F1XapN0UFssGYBGa6Ml/Q5OnowvWwsGH4m0Tf2QAccayVCvtw==",
+ "version": "0.29.4",
+ "resolved": "https://registry.npmjs.org/eve/-/eve-0.29.4.tgz",
+ "integrity": "sha512-EwOmL37l+Iuu7Umno7at3flFpMs4AtT9cg1J4dt7EZ8ZdxaCvGXwvKGwiulMkG8oXkMQ5CNhxMi2ruHJwrtpwQ==",
"license": "Apache-2.0",
"dependencies": {
- "nitro": "3.0.260610-beta"
+ "nitro": "3.0.260610-beta",
+ "undici": "8.9.0"
},
"bin": {
"eve": "bin/eve.js"
@@ -828,7 +830,7 @@
},
"peerDependencies": {
"@opentelemetry/api": "^1.0.0",
- "ai": "^7.0.34",
+ "ai": "^7.0.38",
"braintrust": "^3.0.0",
"just-bash": "^3.0.0",
"microsandbox": "^0.5.0"
@@ -848,6 +850,15 @@
}
}
},
+ "node_modules/eve/node_modules/undici": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz",
+ "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.19.0"
+ }
+ },
"node_modules/eventsource-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz",
@@ -1738,6 +1749,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/undici": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
+ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
"node_modules/unenv": {
"version": "2.0.0-rc.24",
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
diff --git a/test/fixtures/eve-agent/package.json b/test/fixtures/eve-agent/package.json
index 93ad46c..bee6f4c 100644
--- a/test/fixtures/eve-agent/package.json
+++ b/test/fixtures/eve-agent/package.json
@@ -8,8 +8,8 @@
"test:client": "node run-client-probe.mjs"
},
"dependencies": {
- "ai": "7.0.34",
- "eve": "0.27.6",
+ "ai": "7.0.42",
+ "eve": "0.29.4",
"just-bash": "3.1.0"
},
"engines": {
diff --git a/test/fixtures/eve-agent/run-client-probe.mjs b/test/fixtures/eve-agent/run-client-probe.mjs
index ce18003..4aaf10f 100644
--- a/test/fixtures/eve-agent/run-client-probe.mjs
+++ b/test/fixtures/eve-agent/run-client-probe.mjs
@@ -1,12 +1,18 @@
import { once } from "node:events";
import { spawn } from "node:child_process";
-import { rm } from "node:fs/promises";
+import { readFile, rm } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import path from "node:path";
const fixtureDirectory = path.dirname(fileURLToPath(import.meta.url));
const repositoryRoot = path.resolve(fixtureDirectory, "..", "..", "..");
const eveBin = path.join(fixtureDirectory, "node_modules", "eve", "bin", "eve.js");
+const eveVersion = JSON.parse(
+ await readFile(
+ path.join(fixtureDirectory, "node_modules", "eve", "package.json"),
+ "utf8",
+ ),
+).version;
const probeProject = path.join(
repositoryRoot,
"tests",
@@ -79,7 +85,7 @@ try {
if (code !== 0) {
throw new Error(`The C# compatibility probe exited with code ${code}.`);
}
- console.log("Eve 0.27.6 compatibility probe passed.");
+ console.log(`Eve ${eveVersion} compatibility probe passed.`);
} finally {
await stopProcess(server);
}
diff --git a/tests/NexusLabs.Eve.CompatibilityProbe/Program.cs b/tests/NexusLabs.Eve.CompatibilityProbe/Program.cs
index 1f72c9b..baa2d54 100644
--- a/tests/NexusLabs.Eve.CompatibilityProbe/Program.cs
+++ b/tests/NexusLabs.Eve.CompatibilityProbe/Program.cs
@@ -142,46 +142,41 @@
string? observedTailIndex = catchUpRequests[0].TailIndex;
if (observedTailIndex is null)
{
- // eve 0.27.6, the pinned compatibility baseline, does not report the durable tail index.
- if (catchUpFailure is null)
- {
- throw new InvalidOperationException(
- "The Eve fixture omitted x-eve-stream-tail-index, but the bounded read did not fail.");
- }
+ throw new InvalidOperationException(
+ $"eve {EveProtocol.ReferenceEveVersion} omitted the x-eve-stream-tail-index response " +
+ "header, so a bounded catch-up read cannot be verified.");
}
-else
+
+if (catchUpFailure is not null)
{
- if (catchUpFailure is not null)
- {
- throw new InvalidOperationException(
- $"The Eve fixture reported tail index '{observedTailIndex}', " +
- $"but the bounded read failed: {catchUpFailure.Message}");
- }
+ throw new InvalidOperationException(
+ $"The Eve fixture reported tail index '{observedTailIndex}', " +
+ $"but the bounded read failed: {catchUpFailure.Message}");
+}
- if (!int.TryParse(
- observedTailIndex,
- NumberStyles.AllowLeadingSign,
- CultureInfo.InvariantCulture,
- out int tailIndex)
- || tailIndex < 0)
- {
- throw new InvalidOperationException(
- $"The Eve fixture reported an invalid tail index: '{observedTailIndex}'.");
- }
+if (!int.TryParse(
+ observedTailIndex,
+ NumberStyles.AllowLeadingSign,
+ CultureInfo.InvariantCulture,
+ out int tailIndex)
+ || tailIndex < 0)
+{
+ throw new InvalidOperationException(
+ $"The Eve fixture reported an invalid tail index: '{observedTailIndex}'.");
+}
- if (catchUpEvents.Count != tailIndex + 1)
- {
- throw new InvalidOperationException(
- $"The bounded catch-up read returned {catchUpEvents.Count} events " +
- $"for tail index {tailIndex}.");
- }
+if (catchUpEvents.Count != tailIndex + 1)
+{
+ throw new InvalidOperationException(
+ $"The bounded catch-up read returned {catchUpEvents.Count} events " +
+ $"for tail index {tailIndex}.");
+}
- if (catchUpSession.State.StreamIndex != catchUpEvents.Count)
- {
- throw new InvalidOperationException(
- "The bounded catch-up read did not advance the session cursor: " +
- $"{catchUpSession.State.StreamIndex} of {catchUpEvents.Count} events.");
- }
+if (catchUpSession.State.StreamIndex != catchUpEvents.Count)
+{
+ throw new InvalidOperationException(
+ "The bounded catch-up read did not advance the session cursor: " +
+ $"{catchUpSession.State.StreamIndex} of {catchUpEvents.Count} events.");
}
EveSession approvalSession = client.CreateSession();
@@ -203,10 +198,8 @@
EveInputRequest approvalRequest = approvalOutcome.InputRequests[0];
-// eve 0.27.6, the pinned compatibility baseline, predates the framework-owned discriminator.
-// A newer fixture stamps 'tool-approval' here, which flips both assertions.
-if (approvalRequest.RawKind is not null
- || approvalRequest.Kind != EveInputRequestKind.Unknown)
+if (approvalRequest.RawKind != "tool-approval"
+ || approvalRequest.Kind != EveInputRequestKind.ToolApproval)
{
throw new InvalidOperationException(
$"eve {EveProtocol.ReferenceEveVersion} reported input request kind " +
@@ -309,15 +302,12 @@ static void RequireDurableEventEnvelope(EveTurnOutcome outcome, string operation
$"The {operation} produced '{streamEvent.Type}' without a durable timestamp.");
}
- // The pinned eve 0.27.6 baseline emits stream protocol 19 and stamps no event identifier.
- // Protocol 20 adds one, so this assertion flips to requiring an 'evt_' identifier when the
- // fixture advances.
- if (metadata.Id is not null)
+ if (metadata.Id is not string identifier || !IsEventIdentifier(identifier))
{
throw new InvalidOperationException(
- $"The {operation} produced '{streamEvent.Type}' with identifier '{metadata.Id}', " +
- $"but eve {EveProtocol.ReferenceEveVersion} emits stream protocol " +
- $"{EveProtocol.MessageStreamVersion}, which stamps none.");
+ $"The {operation} produced '{streamEvent.Type}' with durable identifier " +
+ $"'{metadata.Id ?? ""}', which is not an eve stream protocol " +
+ $"{EveProtocol.MessageStreamVersion} event id.");
}
if (deduplicator.Admit(streamEvent))
@@ -326,10 +316,41 @@ static void RequireDurableEventEnvelope(EveTurnOutcome outcome, string operation
}
}
- if (admitted != outcome.Events.Count || deduplicator.Count != 0)
+ if (admitted != outcome.Events.Count)
{
throw new InvalidOperationException(
- $"The {operation} dropped identifier-less events: admitted {admitted} of " +
- $"{outcome.Events.Count} while remembering {deduplicator.Count} identifiers.");
+ $"The {operation} repeated a durable identifier: admitted {admitted} of " +
+ $"{outcome.Events.Count} events.");
}
+
+ if (deduplicator.Count != outcome.Events.Count)
+ {
+ throw new InvalidOperationException(
+ $"The {operation} remembered {deduplicator.Count} identifiers for " +
+ $"{outcome.Events.Count} events.");
+ }
+}
+
+// Mirrors the upstream shape check: the 'evt_' prefix followed by a Crockford base32 ULID.
+static bool IsEventIdentifier(string value)
+{
+ const string prefix = "evt_";
+ const int ulidLength = 26;
+ const string crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
+
+ if (!value.StartsWith(prefix, StringComparison.Ordinal)
+ || value.Length != prefix.Length + ulidLength)
+ {
+ return false;
+ }
+
+ foreach (char character in value.AsSpan(prefix.Length))
+ {
+ if (!crockfordAlphabet.Contains(character, StringComparison.Ordinal))
+ {
+ return false;
+ }
+ }
+
+ return true;
}