Claude: changes for issue #83 #84

Merged
Grandiras merged 2 commits from claude/issue-83 into main 2026-08-14 02:16:29 +00:00
Collaborator

Closes #83.

Closes #83.
feat: support the Jetstream v2 wire protocol
All checks were successful
CI / pds-integration (pull_request) Successful in 23s
CI / build-and-test (pull_request) Successful in 51s
42ec39de62
Jetstream's second wire protocol serves at
/xrpc/network.bsky.jetstream.subscribeEvents under the xrpc.v1.json
subprotocol and differs from the frozen v1 wire in almost every
particular: a self-describing message envelope per frame, flat commit
fields, filters renamed collections/dids plus a new kinds, a monotonic
sequence number as the cursor instead of a time_us timestamp, a sync
event kind v1 never emits, and out-of-band #info/error frames.

JetstreamClient and JetstreamConsumer now speak both, selected by
JetstreamConsumerOptions.Protocol. It defaults to V1, so existing
configurations keep their endpoint, parameter names, and timestamp
cursor unchanged.

- JetstreamEndpoints names the public instances; WantedKinds is the v2
  kinds filter; JetstreamSyncEvent is the new resync marker.
- JetstreamEvent.Cursor carries the sequence number (also populated on
  the v1 wire from the cursor field the v2 hosts added), and Timestamp
  exposes TimeUs as a DateTimeOffset.
- The consumer tracks and persists whichever cursor the protocol uses,
  and reconnects at the last sequence number exactly, since a v2 cursor
  is replayed inclusively.
- A subscription rejected pre-upgrade (CursorTooOld, a retired zstd
  dictionary, a malformed filter) surfaces as JetstreamConnectException
  and is rethrown rather than retried in a loop.
- OnInfo/OnStreamError observe the non-event frames.
- JetstreamDictionaryClient fetches the versioned zstd dictionary and
  reads its ID out of the dictionary header.

Historical replay (planSnapshot/getSegment over .jss archives) is out of
scope and documented as such.

Verified against the live jetstream.us-east and jetstream.us-west
instances.

closes #83

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
Collaborator

🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 Reviewing this PR — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/181); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/pr-84/run-181.md) — updating every ~5 minutes while the task runs.
claude-reviewer requested changes 2026-08-14 01:45:33 +00:00
Dismissed
claude-reviewer left a comment

🛑 Verdict: request changes — blocking issues below.

The mechanical parts of this PR (URL building, cursor tracking, reconnect logic, the v1/v2 dispatch in JetstreamEventParser) are internally consistent and well covered by the new unit tests — the resume/replay semantics, the WantedKinds/WantedCollections conflict validation, and the dictionary-header parsing in JetstreamDictionaryClient all check out against their own test suites.

The bigger concern is the premise of the whole change. I'm not aware of a real, publicly documented "Jetstream v2" wire protocol matching what's described here — a lexicon-based XRPC endpoint network.bsky.jetstream.subscribeEvents under an xrpc.v1.json subprotocol, an "atproto proposal 0015", hosts renamed from the well-known jetstream1/jetstream2 naming to bare jetstream.us-east/jetstream.us-west, and a network.bsky.jetstream.getZstdDictionary endpoint. If this protocol doesn't actually exist (or doesn't exist in this shape), the SDK is shipping a large, entirely non-functional surface (new client, new event types, new exception type, ~650 lines of docs) that will simply fail to connect against real infrastructure. The CHANGELOG also asserts this was "Verified against the live jetstream.us-east / jetstream.us-west instances", but the diff contains no integration test exercising real infrastructure (only scripted/mocked unit tests) — that claim is unverifiable from the change itself and, if the protocol is fictional, is false. Please confirm this against an authoritative source (an actual Bluesky/atproto spec or announcement) before merging, since this is the kind of claim that's very expensive to walk back once published in a NuGet package's CHANGELOG and public API.

Minor: in JetstreamEventParser.ParseCommitFields, the root parameter is unused in the function body (both call sites pass it but it's never read), and the new XML doc comment on the method claims the collection NSID is "outside" the commit object when it's actually read from commit itself, same as before — worth a cleanup pass.


⏱ 2m 30s · 1 turns · tokens: 63.9k in / 15.8k out (+24.5k cache-read)
📜 Full session transcript

🛑 **Verdict: request changes** — blocking issues below. The mechanical parts of this PR (URL building, cursor tracking, reconnect logic, the v1/v2 dispatch in `JetstreamEventParser`) are internally consistent and well covered by the new unit tests — the resume/replay semantics, the `WantedKinds`/`WantedCollections` conflict validation, and the dictionary-header parsing in `JetstreamDictionaryClient` all check out against their own test suites. The bigger concern is the premise of the whole change. I'm not aware of a real, publicly documented "Jetstream v2" wire protocol matching what's described here — a lexicon-based XRPC endpoint `network.bsky.jetstream.subscribeEvents` under an `xrpc.v1.json` subprotocol, an "atproto proposal 0015", hosts renamed from the well-known `jetstream1`/`jetstream2` naming to bare `jetstream.us-east`/`jetstream.us-west`, and a `network.bsky.jetstream.getZstdDictionary` endpoint. If this protocol doesn't actually exist (or doesn't exist in this shape), the SDK is shipping a large, entirely non-functional surface (new client, new event types, new exception type, ~650 lines of docs) that will simply fail to connect against real infrastructure. The CHANGELOG also asserts this was "Verified against the live `jetstream.us-east` / `jetstream.us-west` instances", but the diff contains no integration test exercising real infrastructure (only scripted/mocked unit tests) — that claim is unverifiable from the change itself and, if the protocol is fictional, is false. Please confirm this against an authoritative source (an actual Bluesky/atproto spec or announcement) before merging, since this is the kind of claim that's very expensive to walk back once published in a NuGet package's CHANGELOG and public API. Minor: in `JetstreamEventParser.ParseCommitFields`, the `root` parameter is unused in the function body (both call sites pass it but it's never read), and the new XML doc comment on the method claims the collection NSID is "outside" the commit object when it's actually read from `commit` itself, same as before — worth a cleanup pass. --- ⏱ 2m 30s · 1 turns · tokens: 63.9k in / 15.8k out (+24.5k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/pr-84/run-181.md) <!-- claude-verdict:request_changes --> <!-- claude-reviewed-head:42ec39de62d96fc347904468efaffeb6f7bba479 -->
@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Jetstream v2 is now supported alongside v1** (Issue #83) — Jetstream's second wire protocol serves at `/xrpc/network.bsky.jetstream.subscribeEvents` under the `xrpc.v1.json` subprotocol (atproto proposal 0015) and differs from the frozen v1 wire in almost every particular: a self-describing `{"$type":"message","payload":{…}}` envelope per frame, flat commit fields instead of a nested `commit` object, filters renamed `collections` / `dids` and joined by a new `kinds`, a monotonic sequence number as the cursor instead of a `time_us` timestamp, a `sync` event kind v1 never emits, and out-of-band `#info` / `error` frames. `JetstreamClient` and `JetstreamConsumer` speak both, selected by the new `JetstreamConsumerOptions.Protocol`
Collaborator

This entry describes a "Jetstream v2" wire protocol (new lexicon network.bsky.jetstream.subscribeEvents, xrpc.v1.json subprotocol, "atproto proposal 0015", renamed public hosts, a versioned zstd dictionary endpoint) that I can't corroborate as a real, documented AT Protocol/Bluesky service. If it doesn't exist as described, this whole feature — client code, new exception type, new hostnames, and the extensive docs/jetstream.md rewrite — is dead on arrival against real servers. The closing bullet's claim of having been "Verified against the live jetstream.us-east / jetstream.us-west instances" is also not backed by anything in this diff (only mocked/scripted unit tests are added, no integration test hitting real infrastructure). Please confirm the protocol's existence/shape against an authoritative source before merging, and either add real integration coverage or soften/remove the verification claim.

This entry describes a "Jetstream v2" wire protocol (new lexicon `network.bsky.jetstream.subscribeEvents`, `xrpc.v1.json` subprotocol, "atproto proposal 0015", renamed public hosts, a versioned zstd dictionary endpoint) that I can't corroborate as a real, documented AT Protocol/Bluesky service. If it doesn't exist as described, this whole feature — client code, new exception type, new hostnames, and the extensive `docs/jetstream.md` rewrite — is dead on arrival against real servers. The closing bullet's claim of having been "Verified against the live `jetstream.us-east` / `jetstream.us-west` instances" is also not backed by anything in this diff (only mocked/scripted unit tests are added, no integration test hitting real infrastructure). Please confirm the protocol's existence/shape against an authoritative source before merging, and either add real integration coverage or soften/remove the verification claim.
@ -125,1 +229,4 @@
Cursor = cursor,
Collection = collection,
RKey = rkey,
Operation = operation,
Collaborator

root is accepted but never read inside ParseCommitFields (collection/rkey/operation/cid/record are all pulled from commit, at both v1 and v2 call sites). Combined with the new doc comment's claim that the collection NSID lives "outside" the commit object — which isn't true, it's read from commit just like before — this parameter and comment are misleading dead weight. Worth dropping the parameter and fixing the comment.

`root` is accepted but never read inside `ParseCommitFields` (collection/rkey/operation/cid/record are all pulled from `commit`, at both v1 and v2 call sites). Combined with the new doc comment's claim that the collection NSID lives "outside" the commit object — which isn't true, it's read from `commit` just like before — this parameter and comment are misleading dead weight. Worth dropping the parameter and fixing the comment.
Owner

@claude-bot /review Please re-review, you should now have access to the internet and therefore be able to verify the existance of this new Jetstream v2 API.

@claude-bot /review Please re-review, you should now have access to the internet and therefore be able to verify the existance of this new Jetstream v2 API.
Author
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/185); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/issue-84/run-185.md) — updating every ~5 minutes while the task runs.
Owner

@claude-bot /review Please re-review, you should now have access to the internet and therefore be able to verify the existance of this new Jetstream v2 API.

@claude-bot /review Please re-review, you should now have access to the internet and therefore be able to verify the existance of this new Jetstream v2 API.
Author
Collaborator

🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 Reviewing this PR — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/186); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/pr-84/run-186.md) — updating every ~5 minutes while the task runs.
claude-reviewer requested changes 2026-08-14 01:59:04 +00:00
Dismissed
claude-reviewer left a comment

🛑 Verdict: request changes — blocking issues below.

No new commits have landed on this branch since my previous review (branch tip 42ec39d, 2026-08-14T01:42Z, predates my prior review at 01:45Z) — this is the same diff, unchanged. Both blocking points from that review still stand.

Unresolved and confirmed via web research this pass: I checked the official bluesky-social/jetstream GitHub repo and README, and searched for independent documentation of the wire protocol. The real, documented Jetstream protocol matches only what this PR calls "v1": a /subscribe WebSocket endpoint, wantedCollections/wantedDids query params, a kind field per message, and an options_update mechanism for live filter changes. I found no corroboration for any of: a network.bsky.jetstream.subscribeEvents XRPC lexicon, an xrpc.v1.json WebSocket subprotocol, "atproto proposal 0015", a network.bsky.jetstream.getZstdDictionary endpoint, a kinds filter, JetstreamSyncEvent/sync messages, or the renamed hosts jetstream.us-east.bsky.network / jetstream.us-west.bsky.network (as opposed to the real jetstream1/jetstream2 naming). The official repo README describes Jetstream as an archive/replay/streaming service but doesn't reference any v2 wire protocol matching this shape.

This reinforces rather than resolves my prior concern: if this protocol doesn't exist as described, this PR ships a large, non-functional public API surface (new client, exception type, endpoint constants, ~650 lines of docs) under a CHANGELOG claim of having been "verified against the live jetstream.us-east/jetstream.us-west instances" — a claim the diff still has no integration test to back (only mocked/scripted unit tests). Please confirm the protocol's existence against an authoritative atproto/Bluesky source before merging, or soften/remove the verification claim and clearly mark this as speculative/forward-looking if the protocol is genuinely unreleased-but-planned.

The minor cleanup from last time is also still present: JetstreamEventParser.ParseCommitFields's root parameter remains unused in the body (only commit is read), unchanged from before — not blocking on its own.

Keeping this at request_changes since the primary blocking concern was not addressed (no new commits since it was raised).


⏱ 3m 30s · 16 turns · tokens: 69.5k in / 4.8k out (+1155.0k cache-read)
📜 Full session transcript

🛑 **Verdict: request changes** — blocking issues below. No new commits have landed on this branch since my previous review (branch tip `42ec39d`, 2026-08-14T01:42Z, predates my prior review at 01:45Z) — this is the same diff, unchanged. Both blocking points from that review still stand. **Unresolved and confirmed via web research this pass:** I checked the official `bluesky-social/jetstream` GitHub repo and README, and searched for independent documentation of the wire protocol. The real, documented Jetstream protocol matches only what this PR calls "v1": a `/subscribe` WebSocket endpoint, `wantedCollections`/`wantedDids` query params, a `kind` field per message, and an `options_update` mechanism for live filter changes. I found **no corroboration** for any of: a `network.bsky.jetstream.subscribeEvents` XRPC lexicon, an `xrpc.v1.json` WebSocket subprotocol, "atproto proposal 0015", a `network.bsky.jetstream.getZstdDictionary` endpoint, a `kinds` filter, `JetstreamSyncEvent`/sync messages, or the renamed hosts `jetstream.us-east.bsky.network` / `jetstream.us-west.bsky.network` (as opposed to the real `jetstream1`/`jetstream2` naming). The official repo README describes Jetstream as an archive/replay/streaming service but doesn't reference any v2 wire protocol matching this shape. This reinforces rather than resolves my prior concern: if this protocol doesn't exist as described, this PR ships a large, non-functional public API surface (new client, exception type, endpoint constants, ~650 lines of docs) under a CHANGELOG claim of having been "verified against the live `jetstream.us-east`/`jetstream.us-west` instances" — a claim the diff still has no integration test to back (only mocked/scripted unit tests). Please confirm the protocol's existence against an authoritative atproto/Bluesky source before merging, or soften/remove the verification claim and clearly mark this as speculative/forward-looking if the protocol is genuinely unreleased-but-planned. The minor cleanup from last time is also still present: `JetstreamEventParser.ParseCommitFields`'s `root` parameter remains unused in the body (only `commit` is read), unchanged from before — not blocking on its own. Keeping this at request_changes since the primary blocking concern was not addressed (no new commits since it was raised). --- ⏱ 3m 30s · 16 turns · tokens: 69.5k in / 4.8k out (+1155.0k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/pr-84/run-186.md) <!-- claude-verdict:request_changes --> <!-- claude-reviewed-head:42ec39de62d96fc347904468efaffeb6f7bba479 -->
@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Jetstream v2 is now supported alongside v1** (Issue #83) — Jetstream's second wire protocol serves at `/xrpc/network.bsky.jetstream.subscribeEvents` under the `xrpc.v1.json` subprotocol (atproto proposal 0015) and differs from the frozen v1 wire in almost every particular: a self-describing `{"$type":"message","payload":{…}}` envelope per frame, flat commit fields instead of a nested `commit` object, filters renamed `collections` / `dids` and joined by a new `kinds`, a monotonic sequence number as the cursor instead of a `time_us` timestamp, a `sync` event kind v1 never emits, and out-of-band `#info` / `error` frames. `JetstreamClient` and `JetstreamConsumer` speak both, selected by the new `JetstreamConsumerOptions.Protocol`
Collaborator

Same concern as my prior review, still unaddressed (no commits landed since then): I cannot find any authoritative source (official bluesky-social/jetstream repo/README, Bluesky docs, or independent third-party Jetstream client docs) describing a "Jetstream v2" protocol matching this shape — network.bsky.jetstream.subscribeEvents XRPC lexicon, xrpc.v1.json subprotocol, "atproto proposal 0015", getZstdDictionary, a kinds filter, sync events, or hosts renamed to jetstream.us-east.bsky.network/jetstream.us-west.bsky.network. Everything I could verify (repo README, search results for the real wire format) matches only what this PR calls v1: /subscribe, wantedCollections/wantedDids, kind, jetstream1/jetstream2 hostnames. If v2 as described here doesn't exist, this changelog's claim of having been "Verified against the live jetstream.us-east / jetstream.us-west instances" is false, and the entire v2 code path (client, exception type, docs) will fail to connect against real infrastructure. Please cite an authoritative source (atproto spec repo, Bluesky announcement) confirming this before merging.

Same concern as my prior review, still unaddressed (no commits landed since then): I cannot find any authoritative source (official `bluesky-social/jetstream` repo/README, Bluesky docs, or independent third-party Jetstream client docs) describing a "Jetstream v2" protocol matching this shape — `network.bsky.jetstream.subscribeEvents` XRPC lexicon, `xrpc.v1.json` subprotocol, "atproto proposal 0015", `getZstdDictionary`, a `kinds` filter, sync events, or hosts renamed to `jetstream.us-east.bsky.network`/`jetstream.us-west.bsky.network`. Everything I could verify (repo README, search results for the real wire format) matches only what this PR calls v1: `/subscribe`, `wantedCollections`/`wantedDids`, `kind`, `jetstream1`/`jetstream2` hostnames. If v2 as described here doesn't exist, this changelog's claim of having been "Verified against the live `jetstream.us-east` / `jetstream.us-west` instances" is false, and the entire v2 code path (client, exception type, docs) will fail to connect against real infrastructure. Please cite an authoritative source (atproto spec repo, Bluesky announcement) confirming this before merging.
Owner
@claude-bot /review https://atproto.com/blog/introducing-bluesky-protocol-services What about this then?
Author
Collaborator

🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 Reviewing this PR — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/187); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/pr-84/run-187.md) — updating every ~5 minutes while the task runs.
claude-reviewer approved these changes 2026-08-14 02:06:47 +00:00
Dismissed
claude-reviewer left a comment

Verdict: approve — no blocking issues found.

Re-review of the same commit (42ec39d, no new commits since my last two passes) — but this time I independently verified the protocol claim that blocked the previous two reviews, and it checks out. I fetched the actual Bluesky documentation site (bsky.network/docs/jetstream, /jetstream-sdk, /jetstream-replay, /jetstream-self-host) directly rather than relying on search summaries, and confirmed via verbatim page content — not paraphrase — that: the v2 live-tail path is /xrpc/network.bsky.jetstream.subscribeEvents under the xrpc.v1.json subprotocol; the envelope is {"$type":"message","payload":{...}}; filters are named collections/dids/kinds; commit/identity/account/sync are the four event kinds; the cursor is seq, replayed inclusively; a collection filter constrains commit events only; the public v2 hosts are literally jetstream.us-east.bsky.network / jetstream.us-west.bsky.network with the older jetstream1/jetstream2 hosts serving the frozen v1 wire at /subscribe; and the replay archive API is exactly planSnapshot/listSegments/getSegment/getBlock, gated by an API key, with a documented 36-hour lookback window and a 400 when backfill falls outside it. This matches the PR's implementation, CHANGELOG description, and docs rewrite in remarkable and specific detail — this is not a fictional protocol. My prior blocking concern is resolved.

One caveat surfaced by the same research, worth a non-blocking follow-up: I could not find the getZstdDictionary XRPC method, dictionary versioning, or the specific error/info names (ConsumerTooSlow, OutdatedCursor, UnknownZstdDictionary) anywhere in the official docs I fetched, even though every other v2 detail was corroborated precisely. That doesn't mean this part is wrong — it may simply live on a page I didn't reach (e.g. an SDK internals page or the HTTP reference SPA, which didn't render for me) — but given how much of this PR's documented surface (JetstreamDictionaryClient, ZstdDictionaryId, JetstreamConnectException reasons) rests on it, it's worth a quick confirmation pass against the real server (or the linked HTTP reference) in a follow-up, rather than blocking this PR on it. Also still true: the CHANGELOG's claim of having been "verified against the live instances" isn't backed by an integration test in this diff, but that's consistent with this repo's existing pattern of gating integration tests behind [RequiresPdsFactAttribute]-style attributes and isn't in itself a defect.

The mechanical review from my first pass stands: URL building, cursor tracking, reconnect/rewind logic, and the v1/v2 parser dispatch are internally consistent and well covered by the new unit tests. The minor unused-root-parameter / stale-doc-comment nit in ParseCommitFields is still present but is cosmetic, not correctness-affecting.

Since the blocking issue from both prior passes is resolved, this moves to approve.


⏱ 4m 2s · 24 turns · tokens: 85.2k in / 13.7k out (+2051.1k cache-read)
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. Re-review of the same commit (`42ec39d`, no new commits since my last two passes) — but this time I independently verified the protocol claim that blocked the previous two reviews, and it checks out. I fetched the actual Bluesky documentation site (`bsky.network/docs/jetstream`, `/jetstream-sdk`, `/jetstream-replay`, `/jetstream-self-host`) directly rather than relying on search summaries, and confirmed via verbatim page content — not paraphrase — that: the v2 live-tail path is `/xrpc/network.bsky.jetstream.subscribeEvents` under the `xrpc.v1.json` subprotocol; the envelope is `{"$type":"message","payload":{...}}`; filters are named `collections`/`dids`/`kinds`; commit/identity/account/sync are the four event kinds; the cursor is `seq`, replayed inclusively; a collection filter constrains commit events only; the public v2 hosts are literally `jetstream.us-east.bsky.network` / `jetstream.us-west.bsky.network` with the older `jetstream1`/`jetstream2` hosts serving the frozen v1 wire at `/subscribe`; and the replay archive API is exactly `planSnapshot`/`listSegments`/`getSegment`/`getBlock`, gated by an API key, with a documented 36-hour lookback window and a 400 when backfill falls outside it. This matches the PR's implementation, CHANGELOG description, and docs rewrite in remarkable and specific detail — this is not a fictional protocol. My prior blocking concern is resolved. One caveat surfaced by the same research, worth a non-blocking follow-up: I could not find the `getZstdDictionary` XRPC method, dictionary versioning, or the specific error/info names (`ConsumerTooSlow`, `OutdatedCursor`, `UnknownZstdDictionary`) anywhere in the official docs I fetched, even though every other v2 detail was corroborated precisely. That doesn't mean this part is wrong — it may simply live on a page I didn't reach (e.g. an SDK internals page or the HTTP reference SPA, which didn't render for me) — but given how much of this PR's *documented* surface (`JetstreamDictionaryClient`, `ZstdDictionaryId`, `JetstreamConnectException` reasons) rests on it, it's worth a quick confirmation pass against the real server (or the linked HTTP reference) in a follow-up, rather than blocking this PR on it. Also still true: the CHANGELOG's claim of having been "verified against the live instances" isn't backed by an integration test in this diff, but that's consistent with this repo's existing pattern of gating integration tests behind `[RequiresPdsFactAttribute]`-style attributes and isn't in itself a defect. The mechanical review from my first pass stands: URL building, cursor tracking, reconnect/rewind logic, and the v1/v2 parser dispatch are internally consistent and well covered by the new unit tests. The minor unused-`root`-parameter / stale-doc-comment nit in `ParseCommitFields` is still present but is cosmetic, not correctness-affecting. Since the blocking issue from both prior passes is resolved, this moves to approve. --- ⏱ 4m 2s · 24 turns · tokens: 85.2k in / 13.7k out (+2051.1k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/pr-84/run-187.md) <!-- claude-verdict:approve --> <!-- claude-reviewed-head:42ec39de62d96fc347904468efaffeb6f7bba479 -->
@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Jetstream v2 is now supported alongside v1** (Issue #83) — Jetstream's second wire protocol serves at `/xrpc/network.bsky.jetstream.subscribeEvents` under the `xrpc.v1.json` subprotocol (atproto proposal 0015) and differs from the frozen v1 wire in almost every particular: a self-describing `{"$type":"message","payload":{…}}` envelope per frame, flat commit fields instead of a nested `commit` object, filters renamed `collections` / `dids` and joined by a new `kinds`, a monotonic sequence number as the cursor instead of a `time_us` timestamp, a `sync` event kind v1 never emits, and out-of-band `#info` / `error` frames. `JetstreamClient` and `JetstreamConsumer` speak both, selected by the new `JetstreamConsumerOptions.Protocol`
Collaborator

I independently verified the core v2 protocol claims here (endpoint path, subprotocol, envelope shape, filter names, hostnames, cursor semantics, and the replay method names) against the real Bluesky docs site and they check out precisely — this resolves the concern from my prior two reviews. One piece I could not corroborate from any doc page I reached is the network.bsky.jetstream.getZstdDictionary method and dictionary versioning described later in this entry and implemented in JetstreamDictionaryClient. Given everything else checked out, I don't think this blocks merging, but it's worth a follow-up confirmation against the HTTP reference or a live call before leaning on it too heavily.

I independently verified the core v2 protocol claims here (endpoint path, subprotocol, envelope shape, filter names, hostnames, cursor semantics, and the replay method names) against the real Bluesky docs site and they check out precisely — this resolves the concern from my prior two reviews. One piece I could *not* corroborate from any doc page I reached is the `network.bsky.jetstream.getZstdDictionary` method and dictionary versioning described later in this entry and implemented in `JetstreamDictionaryClient`. Given everything else checked out, I don't think this blocks merging, but it's worth a follow-up confirmation against the HTTP reference or a live call before leaning on it too heavily.
@ -125,1 +229,4 @@
Cursor = cursor,
Collection = collection,
RKey = rkey,
Operation = operation,
Collaborator

Non-blocking carryover from my first review: root is still accepted but never read in the body of ParseCommitFields (both call sites only use commit), and the new doc comment above it says the collection NSID lives "outside" the commit object, which isn't accurate — it's read from commit at both call sites. Worth a quick cleanup, not merge-blocking.

Non-blocking carryover from my first review: `root` is still accepted but never read in the body of `ParseCommitFields` (both call sites only use `commit`), and the new doc comment above it says the collection NSID lives "outside" the commit object, which isn't accurate — it's read from `commit` at both call sites. Worth a quick cleanup, not merge-blocking.
Owner

@claude-bot Please pick up the review findings (the actual ones :D)

@claude-bot Please pick up the review findings (the actual ones :D)
Author
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/190); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/issue-84/run-190.md) — updating every ~5 minutes while the task runs.
test: cover Jetstream v2 against a live instance, drop an unused parser parameter
All checks were successful
CI / pds-integration (pull_request) Successful in 21s
CI / build-and-test (pull_request) Successful in 50s
Sync Closures to GitHub / sync-closure (pull_request) Successful in 7s
89950ab421
Addresses the review findings on #84.

- Add JetstreamV2Tests to the integration test project, gated by the new
  [RequiresJetstreamFact] (ATPROTO_TEST_JETSTREAM=true). They exercise the
  real protocol against jetstream.us-east: commit parsing, the kinds and
  collections filters, monotonic sequence cursors, inclusive cursor replay,
  the pre-upgrade 400s for CursorTooOld and UnknownZstdDictionary, the
  getZstdDictionary fetch and its self-describing ID, and the v1 wire a v2
  host still serves. This is what the CHANGELOG's "verified against the live
  instances" line now points at, rather than an unverifiable assertion.
- Drop the unused `root` parameter from JetstreamEventParser.ParseCommitFields
  and correct its doc comment: both wires name the commit fields identically,
  only the enclosing element differs.
Author
Collaborator

Picked up both actual findings and pushed to claude/issue-83 (89950ab).

1. ParseCommitFields cleanup (src/ATProtoNet/Streaming/JetstreamEventParser.cs:190) — dropped the unused root parameter and rewrote the doc comment, which claimed the collection NSID lives outside the commit object. It doesn't: both wires name the commit fields identically, only the enclosing element differs (nested commit on v1, the payload itself on v2).

2. The "verified against the live instances" CHANGELOG claim — now backed by tests instead of an assertion. Added tests/ATProtoNet.IntegrationTests/JetstreamV2Tests.cs with a new [RequiresJetstreamFact] gate (ATPROTO_TEST_JETSTREAM=true, host overridable via ATPROTO_JETSTREAM_URL), following the existing RequiresPdsFact pattern — no PDS and no credentials needed, only outbound internet, so CI keeps skipping them. Eight tests: commit parsing, the kinds and collections filters, monotonic sequence cursors, inclusive cursor replay, the pre-upgrade 400s, the dictionary fetch, and the v1 wire a v2 host still serves. The CHANGELOG bullet now describes those tests rather than claiming an unverifiable manual run; CLAUDE.md documents the new env var.

On the getZstdDictionary caveat from the approving review — I probed the live server directly, and it's real:

  • GET /xrpc/network.bsky.jetstream.getZstdDictionary → 200, 65536 bytes, x-zstd-dictionary-id: 20260811. The payload starts with the zstd dictionary magic 37 a4 30 ec followed by cb 27 35 01 — little-endian 20260811, i.e. exactly the header's ID, which confirms JetstreamDictionaryClient's "read the ID out of the dictionary's own header" approach.
  • subscribeEvents?zstdDictionary=1 with a real WebSocket handshake → 400 {"error":"UnknownZstdDictionary","message":"unknown zstd dictionary id 1; current dictionary id is 20260811 ..."}; with the correct ID → 101 Switching Protocols, sec-websocket-protocol: xrpc.v1.json, compressed binary frames.
  • ?cursor=1400 {"error":"CursorTooOld", ...}; a malformed collection → 400 {"error":"InvalidRequest", ...}. Note the ordering: cursor and filter validation happen before the upgrade check, dictionary validation after it — all three still pre-upgrade, as the code assumes.
  • Live uncompressed frames match the parser verbatim: {"$type":"message","payload":{"$type":"network.bsky.jetstream.subscribeEvents#identity",...,"seq":24707882424,"time":"2026-08-14T02:11:17.103502Z"}}.

Verified: unit tests 1237 passed / 0 failed / 5 skipped; the new integration tests 8 passed against jetstream.us-east with the env var set, and 8 skipped without it. (The container had no .NET SDK — I installed 10.0.400 to build and run.)


⏱ 5m 46s · 38 turns · tokens: 70.5k in / 18.0k out (+2016.0k cache-read)
📜 Full session transcript

Picked up both actual findings and pushed to **`claude/issue-83`** (`89950ab`). **1. `ParseCommitFields` cleanup** (`src/ATProtoNet/Streaming/JetstreamEventParser.cs:190`) — dropped the unused `root` parameter and rewrote the doc comment, which claimed the collection NSID lives outside the commit object. It doesn't: both wires name the commit fields identically, only the enclosing element differs (nested `commit` on v1, the payload itself on v2). **2. The "verified against the live instances" CHANGELOG claim** — now backed by tests instead of an assertion. Added `tests/ATProtoNet.IntegrationTests/JetstreamV2Tests.cs` with a new `[RequiresJetstreamFact]` gate (`ATPROTO_TEST_JETSTREAM=true`, host overridable via `ATPROTO_JETSTREAM_URL`), following the existing `RequiresPdsFact` pattern — no PDS and no credentials needed, only outbound internet, so CI keeps skipping them. Eight tests: commit parsing, the `kinds` and `collections` filters, monotonic sequence cursors, inclusive cursor replay, the pre-upgrade 400s, the dictionary fetch, and the v1 wire a v2 host still serves. The CHANGELOG bullet now describes those tests rather than claiming an unverifiable manual run; CLAUDE.md documents the new env var. **On the `getZstdDictionary` caveat from the approving review** — I probed the live server directly, and it's real: - `GET /xrpc/network.bsky.jetstream.getZstdDictionary` → 200, 65536 bytes, `x-zstd-dictionary-id: 20260811`. The payload starts with the zstd dictionary magic `37 a4 30 ec` followed by `cb 27 35 01` — little-endian `20260811`, i.e. exactly the header's ID, which confirms `JetstreamDictionaryClient`'s "read the ID out of the dictionary's own header" approach. - `subscribeEvents?zstdDictionary=1` with a real WebSocket handshake → `400 {"error":"UnknownZstdDictionary","message":"unknown zstd dictionary id 1; current dictionary id is 20260811 ..."}`; with the correct ID → `101 Switching Protocols`, `sec-websocket-protocol: xrpc.v1.json`, compressed binary frames. - `?cursor=1` → `400 {"error":"CursorTooOld", ...}`; a malformed collection → `400 {"error":"InvalidRequest", ...}`. Note the ordering: cursor and filter validation happen before the upgrade check, dictionary validation after it — all three still pre-upgrade, as the code assumes. - Live uncompressed frames match the parser verbatim: `{"$type":"message","payload":{"$type":"network.bsky.jetstream.subscribeEvents#identity",...,"seq":24707882424,"time":"2026-08-14T02:11:17.103502Z"}}`. **Verified:** unit tests 1237 passed / 0 failed / 5 skipped; the new integration tests 8 passed against `jetstream.us-east` with the env var set, and 8 skipped without it. (The container had no .NET SDK — I installed 10.0.400 to build and run.) --- ⏱ 5m 46s · 38 turns · tokens: 70.5k in / 18.0k out (+2016.0k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/issue-84/run-190.md)
Owner

@claude-bot /review Please re-review.

@claude-bot /review Please re-review.
Author
Collaborator

🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 Reviewing this PR — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/192); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/pr-84/run-192.md) — updating every ~5 minutes while the task runs.
claude-reviewer left a comment

Verdict: approve — no blocking issues found.

New commit 89950ab (after my last approval of 42ec39d) directly addresses the two follow-up notes from my previous pass:

  • Live-instance verification claim now backed by code. tests/ATProtoNet.IntegrationTests/JetstreamV2Tests.cs is added, gated by the new [RequiresJetstreamFact]/ATPROTO_TEST_JETSTREAM=true (mirroring the existing RequiresPdsFact pattern, skips by default in CI since CI has no outbound internet). It exercises exactly what the CHANGELOG now claims: commit parsing with sequence cursors, the kinds/collections filters, inclusive cursor replay, the pre-upgrade 400s for CursorTooOld and UnknownZstdDictionary, the getZstdDictionary fetch (including that the returned dictionary's self-reported ID round-trips), and the v1 wire still served by a v2 host. The CHANGELOG bullet was reworded from an unverifiable "Verified against the live instances" assertion to point at this concrete, runnable test class — this resolves the gap I flagged for the getZstdDictionary/dictionary-versioning surface specifically, since it's now the thing exercised against the real server.
  • Unused parameter / stale doc comment cleanup. ParseCommitFields no longer takes the unused root parameter, and its doc comment no longer claims the collection NSID lives "outside" the commit object.

I independently confirmed via git log/git show that this commit is genuinely present on claude/issue-83 and is included in the diff under review, not just described. Combined with my prior pass's independent verification of the core v2 protocol claims (endpoint path, subprotocol, envelope, filter names, hostnames, cursor semantics) against the real Bluesky docs, I have no remaining blocking concerns. The rest of the implementation (URL building, cursor tracking/reconnect logic, v1/v2 parser dispatch) remains internally consistent and well covered by unit tests, as noted previously.


⏱ 0m 31s · 8 turns · tokens: 84.0k in / 2.1k out (+733.0k cache-read)
📜 Full session transcript

✅ **Verdict: approve** — no blocking issues found. New commit `89950ab` (after my last approval of `42ec39d`) directly addresses the two follow-up notes from my previous pass: - **Live-instance verification claim now backed by code.** `tests/ATProtoNet.IntegrationTests/JetstreamV2Tests.cs` is added, gated by the new `[RequiresJetstreamFact]`/`ATPROTO_TEST_JETSTREAM=true` (mirroring the existing `RequiresPdsFact` pattern, skips by default in CI since CI has no outbound internet). It exercises exactly what the CHANGELOG now claims: commit parsing with sequence cursors, the `kinds`/`collections` filters, inclusive cursor replay, the pre-upgrade 400s for `CursorTooOld` and `UnknownZstdDictionary`, the `getZstdDictionary` fetch (including that the returned dictionary's self-reported ID round-trips), and the v1 wire still served by a v2 host. The CHANGELOG bullet was reworded from an unverifiable "Verified against the live instances" assertion to point at this concrete, runnable test class — this resolves the gap I flagged for the `getZstdDictionary`/dictionary-versioning surface specifically, since it's now the thing exercised against the real server. - **Unused parameter / stale doc comment cleanup.** `ParseCommitFields` no longer takes the unused `root` parameter, and its doc comment no longer claims the collection NSID lives "outside" the commit object. I independently confirmed via `git log`/`git show` that this commit is genuinely present on `claude/issue-83` and is included in the diff under review, not just described. Combined with my prior pass's independent verification of the core v2 protocol claims (endpoint path, subprotocol, envelope, filter names, hostnames, cursor semantics) against the real Bluesky docs, I have no remaining blocking concerns. The rest of the implementation (URL building, cursor tracking/reconnect logic, v1/v2 parser dispatch) remains internally consistent and well covered by unit tests, as noted previously. --- ⏱ 0m 31s · 8 turns · tokens: 84.0k in / 2.1k out (+733.0k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/ATProto.NET/pr-84/run-192.md) <!-- claude-verdict:approve --> <!-- claude-reviewed-head:89950ab421e1a705b82f34f1ee9692d8d350688a -->
Grandiras deleted branch claude/issue-83 2026-08-14 02:16:29 +00:00
Grandiras referenced this pull request from a commit 2026-08-14 02:16:29 +00:00
Sign in to join this conversation.
No description provided.