Skip to content

feat(repo)!: v11 - #2947

Draft
xsahil03x wants to merge 45 commits into
masterfrom
v11
Draft

xsahil03x wants to merge 45 commits into
masterfrom
v11

Conversation

@xsahil03x

@xsahil03x xsahil03x commented Sep 7, 2026 •

Copy link
Copy Markdown
Member

Tracking PR for the v11 major, It stays open while v11 accumulates, so the diff against master is always the answer to "what does v11 change?", and merges when v11 ships.

What is in v11 so far

  • feat(llc): add the OpenAPI generated client and its generation tooling (feat(llc): add the OpenAPI generated client and its generation tooling #2931) — the generated v2 client under lib/open_api/, plus melos run gen:openapi. Nothing calls it yet.
  • melos.yaml pins stream_core and stream_core_flutter to git refs rather than the published versions, so v11 builds against unreleased core.

#2931)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@xsahil03x
xsahil03x marked this pull request as draft September 7, 2026 22:24
xsahil03x and others added 2 commits September 8, 2026 13:09
Conflicts, both in the dependency setup and the changelog:

- `melos.yaml` — v11 pinned `stream_core` and `stream_core_flutter` to git
  refs to build against unreleased core. Both are now published (`stream_core`
  0.5.0 and `stream_core_flutter` 0.5.1 each contain the pinned ref), so v11
  takes master's published constraints and drops the pins, along with the
  `dependency_overrides` blocks and the `invalid_dependency` ignore that only
  existed to work around them.
- `packages/stream_chat/CHANGELOG.md` — v11's entry moves to a new
  `Upcoming Beta` section so it stays separate from master's `Upcoming`.
xsahil03x and others added 8 commits September 15, 2026 15:22
* docs(repo): plan the migration of stream_chat onto stream_core

Adds `core-migration/`, a ten-phase plan for moving the low-level client's
foundation — HTTP, errors, token/auth, WebSocket, uploads, query DSL, logger,
platform, utils — onto `stream_core`, the way `stream_feeds` already is. Chat
currently carries a second implementation of six of those, several as
near-verbatim forks that have drifted.

Three findings shape the order. Nothing is blocked: `stream_core` is a hosted
`^0.5.0` and 0.5.0 ships both the sealed error layer and the upload task API.
The WebSocket is not coupled to v2, because core supports `onAuthenticate:
null` for a connection that sends nothing after open. And the error-code
question is settled — the backend registry has no code 23 or 24, so
`StreamErrorCode` needs no additions.

Corrects the two docs that said otherwise, and notes in `openapi-migration/`
that its group 01 no longer owns the error layer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ci(repo): re-enable the pana jobs for stream_chat_flutter and localizations

Both were disabled while `stream_core` was a git dependency: pana resolves each
package standalone, without the overrides melos generates, so a package pulling
both a git `stream_core` and a hosted one failed version solving before pana
could score it.

`stream_core` is hosted `^0.5.0` now, so that solve cannot fail. Verified by
resolving each package standalone with no overrides: both succeed and pull
`stream_core 0.5.0` from pub.dev.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): map each phase to the PR that lands it

The phase numbering and the stack order do not line up. Phase 01 splits across
two PRs, phase 03 across two more, and one PR carries phases 04, 05 and 06 — so
a reader opening a phase doc sees a status describing the finished stack rather
than the rung they are on. At the first rung, phase 01 reads as partly landed
while both of its files are still present.

Reconciling every doc to every rung is not possible when docs and code
interleave across a stack. Saying which PR a phase lands in is, so this adds
that table and states plainly what the status column means.

Appended rather than edited into the existing prose: eleven commits rewrite
this file, and an in-place edit at the bottom of the stack conflicts with every
one of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): correct stale claims in the core-migration plan

- The `CurrentPlatform.name` shim cannot be an extension: `name` is a static.
- Show the interceptor pipeline as it is assembled, with tags, an unconditional
  `ConnectionIdInterceptor` and `LoggingInterceptor` after `ApiErrorInterceptor`.
- Warn that core's health-check ping writes `client_id` where chat writes
  `connection_id`.
- `stream_chat_dio_error.dart` outlives phase 01; its consumer is phase 05.
- A log tag namespaces records, it does not own `StreamLogger`'s configuration.
- `pana.yml` runs five jobs, none of them gated off.
- A resolved `stream_core` is not the same as a fixed `client.tpl`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): point the plans at the phase that actually owns the work

`01-utilities.md` derived the deletion of `stream_chat_dio_error.dart` to
phase 05, then told the reader it lands in 03 — which never mentions the file.

`openapi-migration/01-foundation.md` said at the top that the error layer and
`Result` moved to `core-migration/03-errors.md`, then scoped, decided, risked
and defined-done the same work below. The `stream_core` release it treated as a
blocker is hosted `^0.5.0` today, and the pana jobs it wanted re-enabled already
run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): make 01's pointer to the error phase true instead of moving it

`01-utilities.md` deferred `stream_chat_dio_error.dart` to 03 and said so twice,
but `03-errors.md` never named the file, so "tracked there, not here" pointed at
nothing. Name it in 03's definition of done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): plan the migration of stream_chat onto stream_core

Adds `core-migration/`, a ten-phase plan for moving the low-level client's
foundation — HTTP, errors, token/auth, WebSocket, uploads, query DSL, logger,
platform, utils — onto `stream_core`, the way `stream_feeds` already is. Chat
currently carries a second implementation of six of those, several as
near-verbatim forks that have drifted.

Three findings shape the order. Nothing is blocked: `stream_core` is a hosted
`^0.5.0` and 0.5.0 ships both the sealed error layer and the upload task API.
The WebSocket is not coupled to v2, because core supports `onAuthenticate:
null` for a connection that sends nothing after open. And the error-code
question is settled — the backend registry has no code 23 or 24, so
`StreamErrorCode` needs no additions.

Corrects the two docs that said otherwise, and notes in `openapi-migration/`
that its group 01 no longer owns the error layer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ci(repo): re-enable the pana jobs for stream_chat_flutter and localizations

Both were disabled while `stream_core` was a git dependency: pana resolves each
package standalone, without the overrides melos generates, so a package pulling
both a git `stream_core` and a hosted one failed version solving before pana
could score it.

`stream_core` is hosted `^0.5.0` now, so that solve cannot fail. Verified by
resolving each package standalone with no overrides: both succeed and pull
`stream_core 0.5.0` from pub.dev.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): map each phase to the PR that lands it

The phase numbering and the stack order do not line up. Phase 01 splits across
two PRs, phase 03 across two more, and one PR carries phases 04, 05 and 06 — so
a reader opening a phase doc sees a status describing the finished stack rather
than the rung they are on. At the first rung, phase 01 reads as partly landed
while both of its files are still present.

Reconciling every doc to every rung is not possible when docs and code
interleave across a stack. Saying which PR a phase lands in is, so this adds
that table and states plainly what the status column means.

Appended rather than edited into the existing prose: eleven commits rewrite
this file, and an in-place edit at the bottom of the stack conflicts with every
one of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): correct stale claims in the core-migration plan

- The `CurrentPlatform.name` shim cannot be an extension: `name` is a static.
- Show the interceptor pipeline as it is assembled, with tags, an unconditional
  `ConnectionIdInterceptor` and `LoggingInterceptor` after `ApiErrorInterceptor`.
- Warn that core's health-check ping writes `client_id` where chat writes
  `connection_id`.
- `stream_chat_dio_error.dart` outlives phase 01; its consumer is phase 05.
- A log tag namespaces records, it does not own `StreamLogger`'s configuration.
- `pana.yml` runs five jobs, none of them gated off.
- A resolved `stream_core` is not the same as a fixed `client.tpl`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): point the plans at the phase that actually owns the work

`01-utilities.md` derived the deletion of `stream_chat_dio_error.dart` to
phase 05, then told the reader it lands in 03 — which never mentions the file.

`openapi-migration/01-foundation.md` said at the top that the error layer and
`Result` moved to `core-migration/03-errors.md`, then scoped, decided, risked
and defined-done the same work below. The `stream_core` release it treated as a
blocker is hosted `^0.5.0` today, and the pana jobs it wanted re-enabled already
run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): make 01's pointer to the error phase true instead of moving it

`01-utilities.md` deferred `stream_chat_dio_error.dart` to 03 and said so twice,
but `03-errors.md` never named the file, so "tracked there, not here" pointed at
nothing. Name it in 03's definition of done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(llc): adopt stream_core's InFlightCache

Ours was a fork with the same `run` signature, the same `Future.sync(work)` and
the same `whenComplete(...).ignore()` cleanup, down to the doc prose — and
core's test file covers the same six cases, so deleting ours loses no coverage.

`client.dart` imports it with a `show` clause rather than core's barrel, which
would collide with `User`, `Filter`, `TokenManager` and `SystemEnvironment` in
that file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc): adopt stream_core's SystemEnvironment and its manager

Both were forks. `SystemEnvironment` was identical — same constructor, same
eight fields, same nullability — so it is re-exported from `stream_chat.dart`
through a `show` allowlist and existing usage keeps compiling.
`SystemEnvironmentManager` shared the class name, the extension name and a
byte-identical `xStreamClientHeader` body, and was never exported.

Core's manager is the better one: it takes the SDK baseline as a constructor
argument and snapshots the SDK-owned fields rather than holding a reference,
because `SystemEnvironment` is not final and a subtype could drift the values an
update is meant to lock. It also rejects an unrecognized `sdkIdentifier` rather
than only comparing precedence.

Chat's manager tests are deleted rather than ported: core covers the manager
with 13 cases and the header extension with 9, a superset of ours. What this
package still owns is the baseline, so one test pins that.

`_systemEnvironmentManager` stays static. Making it per-instance also breaks the
public `defaultUserAgent` static that reads it, so that moves to the phase
retiring `additionalHeaders` alongside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc)!: rename UploadState's variant classes

`UploadState`'s freezed variants were named `Preparing`, `InProgress`,
`Success` and `Failed` — four strikingly generic names in the public surface.
`Success` was contended three ways: this union, `PagedValue`'s success variant
in `stream_chat_flutter_core`, and `Result`'s in `stream_core`. There are
already 13 `hide Success` import clauses in this repo working around the first
two, and exporting `Result` needs the name free.

They become `UploadStatePreparing`, `UploadStateInProgress`,
`UploadStateSuccess` and `UploadStateFailed`.

Deliberately not `UploadPreparing` / `UploadSuccess` / …, tempting as the
symmetry is: those are `stream_core`'s `AttachmentUploadState` variant names,
and adopting the upload task API puts that union in scope alongside this one,
which stays on `Attachment` for persistence. Matching core's names now would
just move the collision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc, ui)!: adopt stream_core's error layer

A failed request now reports one of `stream_core`'s four sealed
`StreamException` kinds instead of a `StreamChatNetworkError`, so a Stream
error is handled the same way here as in our other SDKs.
`typedef StreamChatException = StreamException` gives the family a
chat-shaped name, mirroring `StreamFeedsException`; the kinds keep theirs,
since those are what a `switch` matches on.

`StreamHttpClient._parseError` delegates to core's mapper, so all seven verb
wrappers throw a `StreamChatException`. `ApiErrorInterceptor` is installed
ahead of the logging interceptor, which is also what the generated client
needs: it goes straight to `_dio.fetch` and never reaches those wrappers.

`ChatErrorCode` is deleted. The backend registry has no code 23 or 24, so
`requestTimeout` (23) never matched a real response — the wire code is 48,
which core already has — and `maximumHeaderSizeExceeded` (24) and
`undefinedToken` (1000) were never wire codes at all.

Retry classification stays ours, since retryability is policy the caller owns,
but it now follows core's table: a request that never reached the server, a
5xx, a 429 and a 408 retry; another 4xx, a cancelled request, broken
credentials and anything marked unrecoverable do not. Previously only failures
without a parseable error body retried, so a 500 or a 429 did not — failed
messages will now retry where they used to give up.

`PagedValue.error` and every `errorBuilder` carry the new type, so the break
lands once rather than leaving the old error as the UI currency. The list
controllers' catch-and-rewrap collapses: what they catch is already the right
type, and anything else becomes a `StreamClientException` keeping the original
as its cause. Two "no internet vs slow connection" switches now key off the
failure kind instead of a dio transport enum, which also fixes them covering
only three of the timeout types.

`StreamChatNetworkError` is deprecated rather than deleted, because endpoints
the OpenAPI migration has not moved still flow through the hand-written verb
facade. Nothing raises it, so an `on StreamChatNetworkError catch` clause
still compiles and quietly stops matching — the migration guide says so in
those words, since a deprecation warning does not convey it.

`StreamChatError` stays and is not deprecated: it is still the base for the
SDK's own precondition throws and for the UI's attachment-validation results.
`core-migration/03-errors.md` records what has to happen before the file that
declares it can go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc)!: adopt stream_core's token layer

`Token` becomes `UserToken`, `TokenProvider` becomes an interface rather than
a `Future<String> Function(String)` typedef, and our `AuthInterceptor` gives
way to core's. The client holds a `TokenManager.unconfigured()` and configures
it on connect, since core's primary constructor needs a user id that chat does
not have until then.

`_connectUser` now takes a single `required TokenProvider`, so all four entry
points configure the manager the same way. It previously took `Token?` *and*
`TokenProvider?` and asserted at runtime that exactly one was set.

Core's interceptor fixes three bugs ours had: no guard against a refresh loop,
no guard against refreshing across a user switch, and no `FormData` re-clone —
so a retried file upload replayed streams the first attempt had consumed. It
also skips refreshing a static provider, where a re-fetch returns the same
token, and `UserToken` parses `exp`, which is what lets a refresh happen before
the server refuses rather than only after.

Anonymous connections now identify as `!anon` instead of a client-generated
random id. This is forced rather than chosen: `StaticTokenProvider` validates
that a token's id matches the id it is registered under, and
`UserToken.anonymous()` is always `!anon`. It is also right — the backend
defines `AnonUserID = "!anon"` and pins it so a client cannot claim another
identity, and its subscription store already counts each `!anon` client
separately, so sharing the id is expected. Still worth one live anonymous
connect before release; `core-migration/04-token-and-auth.md` says why source
alone cannot settle it.

`StreamChatClient.devToken` is removed rather than retyped. It minted a
`devtoken`-signed JWT, which only an app with development tokens enabled
accepts, so shipping a minter in the SDK invites it into production. Tests
build their own with `testUserToken`, the way `stream_feeds_test` keeps
`generateTestUserToken`.

Our token tests are deleted rather than ported: core covers `AuthInterceptor`,
`TokenManager`, `UserToken` and `TokenProvider` with 83 cases against our ~30.
`TokenManager` joins the barrel allowlist — it appears in the exported
signatures of `StreamHttpClient` and `StreamChatApi` but was never nameable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc, persistence)!: log through stream_core, and adopt its interceptors

`package:logging` leaves the repo. The SDK writes through `stream_core`'s
`StreamLogger`, so `logLevel` and `logHandlerFunction` collapse into one
`logConfig`, `client.logger` is a `StreamLogger`, and `detachedLogger`,
`defaultLogHandler` and `LogHandlerFunction` are gone along with the
`package:logging` re-export and dependency.

An earlier attempt bridged instead — a handler forwarding core's records into
chat's `Logger` — and that was the wrong call: it left two logging systems
alive, which is the problem this was meant to solve, and it was code whose only
purpose was to be deleted later. v11 already breaks the error and token layers,
so this costs a consumer one more entry in a guide they are reading anyway.

It also could not wait. An unconfigured `StreamLogger` is silent, so every
record from the HTTP client and the token manager was going nowhere, and
adopting core's WebSocket would have taken chat's connection logging with it.

The default is unchanged: `const StreamLogConfig()` is warnings and errors to
the console, which is what this package already documented.

Tags follow core's shape — a class takes `{String tag = '...'}` and builds its
own logger, rather than being handed one. The per-channel identity that used to
live in a logger's *name* is now in the tag, appended rather than nested
(`SCh:RetryQueue:messaging:general`), so filtering by subsystem still works.
`stream_chat_persistence` moved too; it had its own detached logger, default
handler and emoji map.

Alongside it, three forked interceptors become one. Core owns the user-agent
header and the connection id, and its `LoggingInterceptor` replaces our
344-line copy; what stays is the part core has no concept of, applying
`StreamChatClient.additionalHeaders`. The pipeline reads as a list of
null-aware elements over `let`, so an absent dependency drops its interceptor
rather than guarding it, and `ApiErrorInterceptor` sits ahead of logging so
what gets logged is the mapped failure.

`api_key` stays a query parameter rather than moving to core's header-setting
`ApiKeyInterceptor`: the server appears to accept both, and changing a working
wire contract buys nothing.

Every call site in the 13 hand-written `*_api.dart` files is untouched, which
is what keeps the rest of this migration independent of the OpenAPI one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): track the core migration's deferred and upstream work

Two indexes cutting across the phase files, because decisions were surviving
only as sentences buried in them.

`DEFERRED.md` lists everything consciously postponed, grouped by what would
unblock it: waiting on a `stream_core` release, needing a live check that
source cannot settle, or waiting on a later phase. It also carries the shape of
the `StreamChatConfig` that absorbs several of the rows, sequenced into the
cleanup phase rather than done piecemeal.

`UPSTREAM.md` covers the other direction — what chat has that every product
needs. The retry table is the clearest case: `ERROR_LAYER.md` specifies it,
chat now implements it, and feeds independently re-derived a partial version.
It also records that core ships `HeadersInterceptor` and
`ConnectionIdInterceptor` with no tests, which is why chat's kept theirs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): correct what the filter operators need from core

The plan called `$ne`, `$nin` and `$nor` a hard upstream block in four places.
Checked against the other SDKs, only one of them is.

Android deprecates `ne` — "inefficient and causes performance issues, it will
not be supported in the future" — and every `nin` overload, and the JS SDK
never declared either in its `QueryFilter` type. So `stream_core` is right not
to have them, and chat should follow Android and deprecate rather than push
them upstream.

`$nor` is a different case: declared in JS beside `$and` and `$or`, present and
undeprecated in Swift and Android. It is a logical operator, so it belongs next
to core's `AndOperator` and `OrOperator`, and it is the only real ask.

Records the evidence so nobody re-derives it, and notes that Swift still
exposes the deprecated pair — which reads as an oversight there rather than a
signal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): restore jni in the example's generated plugin lists

`generated_plugins.cmake` is written by `flutter pub get`, and both the linux
and windows copies were committed without `jni` in `FLUTTER_FFI_PLUGIN_LIST`.
CI regenerates them during bootstrap, so the working tree came out dirty and
the formatting check — which fails on any modified tracked file — went red.

Restores what v11 has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): keep SDK failures loud when a send fails

The silence predicate was retyped from `StreamChatNetworkError` to
`StreamChatException`, which is the sealed root — so it also suppressed
`StreamClientException`, the kind that means the SDK itself failed. A genuine
bug during send stopped reaching `FlutterError.reportError`.

Narrows it to the two kinds the comment was describing: a refusal the server
sent, and a connection that failed. Both leave the message in a failed state
for the retry queue, so both are expected in release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): warn that catching a subtype narrows what you catch

The guide's two `catch` examples both read only `.message`, which lives on the
sealed root. A real v10 clause reads `.code` or `.statusCode`, and the Symbol
Map maps those onto `StreamApiException` — a subtype. Nothing showed the
bridge, so the obvious edit is `on StreamChatNetworkError catch` →
`on StreamApiException catch`, which compiles and silently stops handling
timeouts, cancellation, auth failures and undecodable responses.

That is the same silent break the guide's own warning box claims deleting the
old types prevents, so it needed saying. Adds a worked before/after, and notes
why the sealed root needs no default arm while `Result.fold` does —
`Failure.error` is typed `Object`. Both compile-checked, and the exhaustiveness
claim verified by removing a case and watching the analyzer reject it.

Also gives `TokenProvider.dynamic` its loader signature, and records that
`UserToken` throws on a malformed JWT and on a user-id mismatch. Neither is a
compile error; both present as "cannot log in".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(llc, ui): assert the exception a failing request produces

The API-error tests asserted inside a `catch` block, so a request that
returned instead of throwing passed without checking anything. The reaction
controller's generic-exception test checked the message but not the type.

Also documents `ChannelDeliveryReporter`'s `tag` parameter, which replaced
the `logger` the constructor doc still described.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc): keep the retry paths reachable after the error swap

`_parseError` reports a `StreamException` now, so five `is
StreamChatNetworkError` guards stopped matching and their branches
became unreachable.

`sendMessage`, `updateMessage`, `partialUpdateMessage` and
`_deleteMessage` no longer queued a failed write, so a transient
failure waited for the next connection-recovered sweep instead of
retrying with backoff. `sync` no longer flushed persistence on the 400
the server answers for a stale `lastSyncAt` or an oversized state,
leaving that `lastSyncAt` in place for every later sync to fail on.

The sync branch had a test, but it threw a hand-built
`StreamChatNetworkError.raw` no pipeline can produce any more, so it
stayed green over dead code. It now throws what a rejected request
reports.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(llc): pin that the Stream client header is still installed

`X-Stream-Client` moved from chat's own interceptor to `stream_core`'s
`HeadersInterceptor` in this branch, and it was the one interceptor
whose installation nothing asserted. A miswired `systemEnvironmentManager`
would drop the header silently and cost SDK attribution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style(llc): keep the logger field with the other fields

`_logger` had landed between a setter and a getter in `WebSocket`, and
between two methods in `ChannelDeliveryReporter`.

`ErrorResponse`'s dartdoc also still pointed at `StreamChatNetworkError`,
which nothing raises any more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): correct what the changelogs and phase docs claim

The default channel error state is `stream_chat_flutter`'s, so its entry
moves out of `stream_chat_flutter_core`'s changelog.

`🚀 Changed` was an invented heading sitting beside `🔄 Changed` in the
same release; both fold into one.

Two phase-doc claims were wrong: `AppSettingsManager` was not detached
and did reach `Logger.root`, and the controllers' error wrap was
rewritten rather than deleted — it keeps the original as `cause`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc)!: stop logging the user token with the connect URI

The WebSocket connect URI carries the token twice — as `authorization`,
and as `user_token` inside the `json` payload — and both `[connect]` and
`[reconnect]` logged it whole at `info`.

Raising the priority to `info` is what the SDK documents for development,
so anyone debugging a connection wrote a usable token to the console and
to whatever handler the app had installed, which for an app forwarding
records to a crash reporter means off the device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc, persistence): log lifecycle at info, everything else below it

101 of the 127 log sites in these two packages were `info`, including a
health check every 20 seconds, a reconnect check every 10, a line per
WebSocket frame, one per keystroke, and — in persistence — the name of
every call it makes.

`info` now carries only what a developer wants to see once: the client
created and disposed, the user set and disconnected, the connection
opening, established and closing. Per-operation records are `debug` and
per-event ones are `verbose`, so raising the priority to chase a problem
no longer buries it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core): say what failed instead of stringifying the error

A throwable the SDK does not recognise was wrapped as
`StreamClientException(message: error.toString())`, which put the
stringified error where a message belongs and left the same text in
`cause` a second time.

The message now names the load — `Failed to load more channels` — and
the throwable survives untouched as `cause`, which is the field that
exists to hold it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): say what failed in the photo gallery too

`StreamPhotoGalleryController` is the tenth `PagedValueNotifier` and the
one outside `stream_chat_flutter_core`, so it was missed when the other
nine stopped stringifying the throwable into the message.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(llc): name these tests after what they check

Eight tests claimed to add a message to the retry queue; none looked at
the queue, and four of them threw a 403, which is not retriable at all.
They assert the message reaches a failed state, and the four that use a
408 or a 500 also assert the verdict — so that is what they now say.

`StreamChatException` aliasing `StreamException` was pinned by
`hasLength(4)` on a four-element literal, which holds whatever the alias
does. It now compares the two types.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): keep this rung's plan docs to what this rung changes

`08-query-dsl.md` was rewritten here, but its subject is Sort and Filter,
which land two and three rungs up — and both rewrite the file wholesale,
so nothing this rung wrote survives to the tip. It goes back to what the
plan said; #2958 records the same decision with a better citation.

The status table said 03, 04 and 08 were unfinished, while the section
below it says the column describes the finished stack rather than the PR
you are reading it in. The table now says what the tip says.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): correct what the README claims this rung leaves behind

The collision list still named seven types this branch deletes —
`TokenManager`, `AuthType`, `AuthInterceptor`, `ConnectionIdInterceptor`,
`LoggingInterceptor`, `InterceptStep` and `LogPrint`. Chat declares none
of them any more, so a wholesale export no longer collides on any of
them, and the paragraph below that says the list shortens each phase
should have said so.

Two lines pointed past this rung: the hard-block sentence carried phase
08's narrowed conclusion while the phase doc no longer does, and the
parked note promised findings that `07-websocket.md` does not hold yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): match the wrap this phase actually ships

The paragraph still showed `StreamClientException(message:
error.toString())`, which is what it was until the message started naming
the load instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc)!: stop exporting the logging interceptor's types

`LoggingInterceptor`, `InterceptStep` and `LogPrint` were public because the
barrel exported the whole interceptor file. Nothing needs them now: the
interceptor is installed by default and writes through the configured
`StreamLogHandler`, so routing its output is a `logConfig` concern.

Also drop a comment in `stream_http_client.dart` that described the
implementation it replaced, and stop naming `stream_core` in the persistence
changelog, which its consumers never depend on directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc)!: delete StreamChatNetworkError rather than deprecate it

The plan deferred this to `openapi-migration` group 12, reasoning that
endpoints that group had not moved still flowed through the hand-written verb
facade. That reasoning does not survive this phase: the facade throws a
`StreamChatException` now, so nothing raises the type whichever endpoint you
call.

Keeping it declared bought source compatibility for a type that no longer
works — `on StreamChatNetworkError catch` would still compile and silently
match nothing, which this plan's own risk section calls the one break a
consumer can ship without noticing. Deleting it makes that a compile error.

`StreamChatNetworkErrorType` goes with it. `ErrorResponse` stays: it is still
`StreamWebSocketError`'s payload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): keep one account of why stream_chat_dio_error waits

The fuller section added here left the original in place, so the file argued
the same point twice, a few dozen lines apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(llc): describe logConfig's reach, not the package behind it

`logConfig` is public, and which package implements the logger is not something
a caller can act on. What they need is that the setting is process-wide and the
last client constructed wins, which the entry now leads with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc): log the channel event handler through StreamLogger

v11 landed `ChannelEventHandler` after this rung replaced `package:logging`,
so its contained-error report still called `Logger.warning`. Route it through
`StreamLogger` like the rest of the client.

`MockStreamChatClient` carries the real logger rather than a stub, because a
`StreamLogger` writes to a global handler: a test that wants the records
installs one, as `retry_queue_test` already does, and the three suites that
merely reach the reporting path need no setup at all.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(llc)!: adopt stream_core's Sort for every query

Replaces `SortOption` / `SortOrder` and the `ComparableField` machinery with
`stream_core`'s `Sort<T>` and `SortField<T>`, giving each model its own sort
type and field registry, as in `stream_feeds`.

A field now carries the value it sorts on, so one declaration drives both the
wire query and a client-side sort, and a field from the wrong model no longer
compiles — `client.search(sort:)` previously took an untyped `SortOrder`, so a
channel field passed the compiler and the server silently ignored it. The
message sort is named `MessageSearchSort` because searching is the only
message query the API sorts.

Every registry was verified field by field against the backend's
`mq.TableConfig`, which found `MessageReminderSortField.messageId` and
`MessageSearchSortField.relevance` missing, and `PollVoteSortField.answerText`
and a custom sort field on drafts rejected by the server.

Default sorts move from `stream_chat_flutter_core`'s `defaultXListSort`
constants onto the sort that owns them, so the ordering the API applies is
reachable without the Flutter layer.

Verified with `melos run analyze`, `melos run format`, and the stream_chat
(1646), stream_chat_flutter_core (362) and stream_chat_persistence (305)
suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): record what the filter investigation settled

The upstream list and the filter half of phase 08 were written before the
evidence and recommended five things that turned out to be wrong. Correcting
them rather than deleting, so the same asks are not re-raised.

Dropped from UPSTREAM, each with the reason: `$nor` (breaks a parity Swift and
Android core keep), `_ResultCallAdapter` (core does not depend on retrofit),
`Sort` value equality (cannot be written correctly — it compared the field's
wire name and ignored the comparator, so two sorts ordered a list in opposite
directions while comparing equal), and `SortDirection.fromJson` (json_serializable
decodes enums through the generated value map, never a `fromJson` static).
`normalizeStringForSort` landed, but as a utility rather than the
`ComparableField` change proposed — `Filter` compares through the same code, so
folding there would have made `$eq` case-insensitive locally.

Phase 08's filter half is rewritten around three findings: the backend withdrew
`$ne` and `$nin` from the published spec in GetStream/chat#15657, which is a
better citation than Android's deprecation and carries per-field exceptions the
plan did not have; a predefined filter is authored server-side and can contain
operators core does not model, so its decode needs a raw fallback leaf the way
sort fields have `custom`; and no chat SDK evaluates filters locally, so
`matches()` is not the payoff the plan claimed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): record the verified filter fields for phase 08

111 filterable fields extracted from each resource's `mq.TableConfig.Columns`
with named operator sets resolved, so the registry and its per-field
"Supported operators" lines are written from the backend rather than from the
JS client or guesswork — the order that kept the sort half clean.

It also surfaces the one capability the operator removals cost. `$nin` on
`user.id` is the sanctioned "everyone except these people" query, index-safe
and deliberately optimised, and core models neither `$nin` nor `$nor`. After
the migration it is reachable only through `Filter.raw`, which is a decision to
take before `UserFilterField` is written, not after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(persistence): bump the schema version for the sort converter change

`channel_queries_metadata`'s `sort` column swapped `ChannelStateSortOrderConverter`
for `ChannelSortConverter`, so the JSON stored in it changes shape. A cache
written by an older build would be read back through the new converter without
a version change.

Moves to the `1100 +` band, which is where v11's schema numbering starts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc): keep nulls-last thread ordering and normalized channel names

Two orderings regressed when sorting moved to `stream_core`'s typed fields,
because the old code keyed its special cases off the field *string* and the new
registries key off the model.

`ThreadSort` lost nulls-last on `last_message_at`. The old `SortOption.desc`
compared the raw field name, so a thread sort matched the same
`'last_message_at'` case a channel sort did; `Sort.desc` defaults to nullsFirst.
Since `ThreadSort.defaultSort` sorts that field descending and `lastMessageAt`
is nullable, a thread with no replies moved from the end of the list to the
front.

Channel names stopped folding. `ComparableField` normalized every string it
compared, and `getComparableField` resolved an unmodelled key through
`extraData`, so sorting channels by name folded diacritics and ligatures — the
behaviour #2601 added. With no `ChannelSortField.name`, the only route was
`custom('name')`, which compares raw. Declares the field, matching what
`MemberSortField.name` and `UserSortField.name` already do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc): make the shared default sort lists unmodifiable

Each `defaultSort` is a `static final` growable list handed straight to a
controller — `ChannelSort.defaultSort` reaches `_resolvedChannelStateSort`, and
the rest reach their list controllers the same way. A caller that sorted or
added to one changed the default for every controller in the process, which the
`const` lists they replaced could not do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): record that sort lists can no longer be const

The Symbol Map never said so, and every v10 example wrote
`const [SortOption.desc(...)]`, so a consumer meets this on the first line they
touch. Core's `SortField` holds a value closure, which is not a constant
expression, so the registry members are `static final` and the list cannot be
`const`.

It went unwritten because the phase doc claimed the opposite. Its
`StreamSortField` section described a chat-side base class with a const
constructor that was never built — `git grep` finds no such class — and while
the plan recorded const-ness as preserved, nobody wrote the consumer note.
Replaced with what shipped, including the correction that an explicit
`nullOrdering:` wins over the per-field default rather than being ignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(llc): correct the sort fields the changelog claims

Three entries named fields that do not exist. `userId`, `attachments`,
`attachmentsType` and `mentionedUsersId` are `MessageSearchFilterField`s, not
sortable; `MemberSortField` has no `lastActive`; `UserSortField` has no `teams`.

Corrected here rather than four PRs later, where it was landing before: a
changelog is the highest-trust document a reader has, and these read as
authoritative for the three PRs in between.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(llc): cover the thread and reminder sort field getters

`ThreadSortField` and `MessageReminderSortField` were the only two registries
with no ordering assertions, and they are the two codecov flags worst — 41% and
47%. That is not a coincidence: a `SortField` is a remote name plus a closure
that reads a value off the model, and the existing tests pinned only the name.
A getter reading the wrong property sorts plausibly while ordering by something
else.

It is also why the nulls-last regression on `ThreadSortField.lastMessageAt`
reached review — nothing exercised it. The last test here pins that: a thread
with no replies trails the list in both directions, and it fails if the
`_orderingFor` default is removed. Verified by removing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc): read the raw name for local name sorting

`User.name` answers the id when a user has no name, so `UserSortField.name`
and `MemberSortField.name` ordered unnamed users among the named ones while
the API sorts them together by an empty `name` column.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc)!: drop the custom sort field where the API rejects one

`queryPolls` and `queryThreads` validate a sort against whole combinations of
declared fields (`Poll.AllowedSortCombinations`,
`ThreadWithLastReadAt.AllowedSortCombinations`), so a custom field fails the
request. `PollSortField.custom` and `ThreadSortField.custom` are gone; the
four registries whose resources leave sort validation open keep theirs.

Also adds `PollSort.defaultSort`, matching the server's `created_at` ascending,
and corrects the documented direction on the poll-vote and reaction defaults.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc): restore the poll-vote default sort direction

`PollVote.DefaultSort()` is `created_at` ascending, and v10's
`defaultPollVoteListSort` matched it. Adopting core's `Sort` flipped it to
descending, so a poll-vote list left unsorted came back reversed.

Pins every default sort against the server's own in `sort_test.dart`, which
is what would have caught it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): say what the poll-vote default and answer_text actually do

The changelog still announced a poll-vote default of newest-first, which the
previous commit reverted — the default is `created_at` ascending, exactly what
v10 shipped, so there is no change to announce.

The migration guide explained the `answerText` removal through the backend's
own `AllowedCustomSortColumns`, an identifier no SDK consumer can see. The
contract is that the API rejects the sort.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core)!: stop typing a controller sort that is never null

Every list controller with a default resolves `sort ?? XSort.defaultSort` in
its constructor, so the property could not be null — but it was typed as if it
could, leaving callers to null-check a field that never is.

The setter kept the old meaning: assigning null sent no sort at all, while
passing null to the constructor selected the default. It now selects the
default too, which is what the changelog already claimed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(llc): document the sort contract, not the design behind it

`XSortField.custom`'s doc described the other registries and its own cost
rather than what the caller gets. `ThreadSort.defaultSort` said it sorted
"minus the unread term" while the list it documents carries that term, and the
nulls-last comment beside it claimed a parity with the channel case that does
not hold — the API's thread date is never absent, so placing a dateless thread
last is a local-sort choice.

`_defaultSortFor` also returned an unmodifiable list on one branch and a fresh
growable one on the other.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* revert(llc): drop the unmodifiable wrapper on the default sorts

`List.unmodifiable` guarded a mutation nobody performs, and it costs a copy and
a line of ceremony on every registry. Reverts 395351d.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(llc): add an empty sort for querying without one

A controller's `sort` is non-nullable where the model declares a default, so
there was no way left to say "let the API order this" — the meaning v10's
`sort: null` carried. `ChannelSort.empty` and its siblings say it explicitly.

An empty sort also has to skip local sorting rather than run a no-op
comparator: an empty list compares every pair as equal, and `List.sort` is not
stable, so the page the server ordered would be permuted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(llc): drop the changelog line the empty sort replaced

The bullet described a controller's null handling from the low-level client's
changelog, and the setter no longer takes null. What a controller does with an
unset sort is already stated in `stream_chat_flutter_core`'s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(persistence): order a cached channel query given no sort

An empty or absent sort left the cached page in whatever order the cid lookup
returned, so a controller that names no sort got the API's ordering online and
an arbitrary one offline. A cached read has no server to defer to, so it
applies `ChannelSort.defaultSort` — the ordering the query would have had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(core): size the empty-sort pages past the insertion-sort threshold

At five items the test passed with the guard removed: `List.sort` is a stable
insertion sort at or below 32 items, so an all-equal comparator preserves the
order there whether the sort runs or not. Fifty items reaches the unstable
quicksort, which is the case the guard exists for — both tests now fail without
it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core): sort a loaded page with a stable sort

`sorted` is `List.sort`, which is a dual-pivot quicksort above 32 elements and
reorders elements the comparator calls equal. A sort with ties — members
batch-added share a `created_at`, and the server breaks that tie on `user_id`
where the local comparator cannot — therefore scrambled its tied rows on every
page append, once the list grew past a page or two.

`sortedByCompare` is `mergeSortBy`, which is stable. At these sizes the cost is
microseconds: 4µs for 100 items, 57µs for 1000.

Trims the sort docs and comments to what a caller cannot read off the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(core): drop the empty-sort short-circuit

It existed because `sorted` permuted a page an empty sort compared as all
equal. The sort is `sortedByCompare` now, which is stable, so an empty sort
already leaves the page as it arrived — the branch restated what the sort does.

`an empty sort leaves a page in the order it arrived in` still fails if the
sort goes back to an unstable one, which is what the branch was really
guarding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core)!: order a stateless channel by the sort's null ordering

The local sort intercepted a channel whose state was disposed and pinned it
last, overriding the ordering every other null in the same sort obeys. A
channel with state but a null value in the sorted field already went through
`nullOrdering`; only a channel with no state at all was special-cased.

It now passes straight to the comparator, so both nulls are placed the same
way. Under `ChannelSort.defaultSort` — descending, nulls first — a disposed
channel sorts to the top rather than the bottom.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(core): sort a loaded page with stream_core's `sortedWith`

Every controller reached a stable sort through `sortedByCompare((it) => it,
…)`, an identity key standing in for a method core did not have. It has one
now, so each list sorts by the comparator alone — including the channel list,
whose key extractor existed only to feed a null triage that is gone.

`stream_chat_flutter_core` names `stream_core` directly rather than reaching it
through `stream_chat`'s barrel: that barrel already exports this package's own
`SortedListX`, which declares `sortedInsert`, `sortedUpsert` and
`sortedUpsertAt` too, so re-exporting core's would leave every caller of those
three with an ambiguous extension until the list extensions move over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(core): keep a disposed channel at the end of the list

Reverts the ordering change in 7221c6b. `last_updated` is never null on the
server — Postgres computes `GREATEST(last_message_at, created_at)`, which
ignores nulls, and `ChannelModel.lastUpdatedAt` mirrors that — so a null key
here means only one thing: the channel was disposed. There is no API ordering
to defer to, and a row about to disappear surfacing at the top of a descending
list is worse than it sinking.

The rule now lives in a named comparator rather than inline, so the sort reads
as one line and the reason is stated where it applies.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style(core): wrap the channel list sort call

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style(persistence): drop a stray blank line in the client test

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): say that this phase is what pins core to an unreleased commit

The plan's prerequisites still read "nothing in phase 08 waits on a core
release" — true until this phase started depending on `sortedWith`, which no
published `stream_core` carries.

Records the unstable-sort fix in the changelog too: rows a sort called equal
were reordered on every page append, which is a behaviour change a reader
upgrading would want to see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(llc): split the sort tests, and correct two more release-blocker claims

`sort_test.dart` had grown to 818 lines across six groups doing the job files
should do. It splits along the seam the groups already drew: what the
registries declare and how a sort is built, against how a sort orders values.
One group was still named for `StreamSortField`, a class that was never built.

The skill doc and the upstream index still described `stream_core` as a hosted
dependency with nothing blocking a release, which stopped being true when this
phase pinned it for `sortedWith`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): trim two changelog entries and name two groups by precondition

`stream_chat`'s entry listed six `default*ListSort` constants that live in
`stream_chat_flutter_core` and are already listed in its changelog, which is
the per-method enumeration the changelog policy warns off.

`single field` and `Composite Sorting` named the shape of a sort rather than
the precondition the tests under them share.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(llc): describe what a caller sees, not how the server stores it

Two sort docs reached past the contract: one named the column a query sorts
unnamed users by, the other named the two things a search can be backed by.
Neither is something a caller can observe or act on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): correct what the preset sort fallback actually mismatches on

The recorded gap does not happen by the route the plan described. When the
server flags a query as filtering on `last_message_at` it rewrites a sent
`last_updated` to `last_message_at`, so sending `ChannelSort.defaultSort` and
resolving `last_message_at` agree.

The real gap is that `_touchesField` is looser than the condition it mirrors:
the server needs every `last_message_at` node reachable through `$and` alone
and one narrowing by value, while ours asks only whether the field appears.
Filed in DEFERRED.md rather than fixed here — closing it inverts a test.

Also: `_defaultSortFor` names `lastUpdated` itself instead of borrowing
`ChannelSort.defaultSort`, which it matches only by coincidence; nothing sorts
a thread list locally, so the plan should not say `ThreadSort.defaultSort`
exists to; and `User.name`'s promotion out of `extraData` is recorded against
the `User` shape decision it belongs to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): name the function that outlives this rung, not its helper

The private helper is renamed by the filter phase; `_defaultSortFor` is what
carries the behaviour through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc): add the user sort field the plan said shipped, and correct two defaults it got wrong

`08-query-dsl.md` records `teams` as one of the two fields added to the user
registry, but it was never declared. The backend accepts it: it is in the user
table's `Columns` with a `TargetName`, and `user` declares no
`AllowedSortCombinations` to reject it. A team list has no ordering, so a local
re-sort leaves the page as it arrived.

The same doc's backend column was wrong in two rows. `PollVote.DefaultSort()`
is `created_at` asc and `MessageReminder.DefaultSort()` is `remind_at` asc,
where the table claimed neither had a default. The poll-vote row went further
and proposed flipping ours to descending for consistency with reactions, on
that false premise; reactions really are descending, poll votes really are
ascending, and the code already matched the API.

Also: the scope table claimed `ComparableField` folded into a `StreamSortField`
the same file later says was never built, behind a link to a heading that does
not exist; and the migration guide's custom-sort example used `Sort.desc`,
which yields a `Sort<ChannelState>` that no migrated signature accepts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc): drop ThreadSort.defaultSort, which the client cannot honour

It named the ordering a thread query already has when given no sort — unread,
then last message, then parent message id — so sending it bought nothing but a
wider code path on the server.

Sorting by it locally is worse: a `Thread` carries no per-user unread state, so
`ThreadSortField.hasUnread` reads nothing and the first term silently drops.
The list comes out ordered by last message date, which is not what the server
returns. Declaring the constant only invited a caller to reach for an ordering
the client cannot deliver.

`ThreadSortField.hasUnread` stays — the server can sort by it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(llc): drop the note explaining an absent member

Why `ThreadSort` has no `defaultSort` belongs in the CHANGELOG and the phase
doc, not as a comment on the empty space where it would have been.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(llc): log the sort fields and defaults this phase actually added

`UserSortField.teams` and `PollSort.defaultSort` shipped without an entry. The
note that `ThreadSort` has no default now qualifies the bullet claiming every
sort owns one, instead of standing alone under Changed where nothing changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(llc): drop the changelog note about a default that never existed

`ThreadSort` is new in v11, so no consumer had a `ThreadSort.defaultSort` to
lose. Why it has none belongs in the phase doc, not in a release note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* build(repo): pin stream_core to the ref v11 now needs, overriding its one source

v11 picked up `StreamMessageAnnotation.separator`, which the
`chat-integration/v11` ref this rung pinned predates, so the pin moves to
core's `main`.

That ref cannot carry the commit the old one did — `stream_core_flutter`
depending on `stream_core` by path — because a path dependency would block
publishing it. Its hosted constraint is a second source for a package this
workspace takes from git, and pub allows only one, so each package seeing
both overrides `stream_core` to the same ref.

The overrides are scaffolding for the same window as the pins and come out
when core publishes a release carrying these APIs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore(repo): resolve stream_core from the sibling checkout

Phase 08b needs `Filter.raw`, which is unreleased. Melos'
`dependencyOverridePaths` points resolution at ../stream-core-flutter while
both repos change together, leaving the declared `^0.5.0` constraint honest —
reverting is deleting three lines rather than editing a version back.

A direct path dependency does not work here: hosted stream_core_flutter 0.5.1
depends on hosted stream_core, so pub cannot reconcile the two. Both packages
are overridden, named explicitly because globbing `packages/**` picks up the
plugin symlinks under stream_thumbnail/example.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc)!: adopt stream_core's Filter for every query

Chat's own `Filter` addressed fields by string, so nothing tied a key to
the model it filtered or to the operators the API accepts there. It is
replaced by `stream_core`'s sealed `Filter<T>`, with one alias and one
field registry per query — the shape `Sort` already took.

Each registry is pinned to `x-stream-filter-fields` in the protocol repo
by a test that requires every published field to be either declared or
listed as omitted with a reason, so a gap has to be written down rather
than gone unnoticed.

`$ne`, `$nin` and `$nor` are gone: they are deprecated server-side and
being withdrawn. `Filter.empty()` is gone too — every filter argument is
nullable, which `queryThreads` distinguishes from an empty one. The
persistence layer follows: its resolved-filter column is nullable, so a
predefined-filter query with no filter stores none.

Message flags are the one endpoint left unmodelled; chat has no API for
them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(llc): show a query on each filter and sort

Every alias now points at its field registry and carries a worked
example, so the fields a query accepts are reachable from the type a
caller already has. Filtering lost its only example when chat's own
`Filter` was deleted; this restores one per query rather than the single
channel example it had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(llc): drop the filter field accounting test

It read as a check against the API spec, but the spec was not consulted:
the published set was transcribed into the test by hand. A field added
upstream left it green, which is the one case its name promised to
catch, and it cost a second list to keep in step with every registry.

The reasoning it carried — why a field is declared or left out — moves
to the phase doc, which was still claiming `hidden`, `muted`, `blocked`,
`members` and `member.user.name` cannot have getters. All five are
declared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(repo): resolve stream_core from a pinned commit

Replaces the sibling-checkout `dependencyOverridePaths` block, which only ever
worked on a machine with stream-core-flutter checked out next door. On CI the
glob matched nothing, melos resolved the published 0.5.0 instead, and every
package using an unreleased core API failed to analyze.

Pinned to a SHA rather than a branch on purpose. `stream_core_flutter` reaches
`stream_core` through the same clone, and pub compares git descriptors rather
than resolved commits — a branch ref on one side and a resolved SHA on the
other count as two sources of one package, which fails to solve. Both entries
name the same commit so the descriptors match.

`invalid_dependency` is silenced because a git dependency makes these packages
temporarily unpublishable, which is true and intended. It goes when the git
dependency does; publishability is still enforced by `lint:pub` and `pana` on
master-targeted PRs.

Restore hosted constraints before releasing v11, and delete the
`chat-integration/v11` branch in stream-core-flutter — its path dependency
cannot be published.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(persistence): bump the schema version for the nullable filter column

`channel_queries_metadata`'s `filter` column becomes nullable, so a row written
by an older build carries a non-null placeholder where the new code expects
absence to mean "no filter".

Separate from the bump in the sort change below it: each is its own stored-format
change, and the entity check compares a PR against its base rather than against
the whole stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc): always send filter_conditions where the API requires it

`queryUsers` and `queryBannedUsers` declare `filter_conditions` as a required
field (`validate:"required"` in the backend's request payloads). go-playground
reads that as non-nil, so `{}` passes and an omitted key is rejected.

Making `filter` nullable turned every `Filter.empty()` caller into a `null`,
which dropped the key entirely — a 400 on mention autocomplete with
`mentionAllAppUsers`, and on the initial directory load of the sample app's
three user-picker screens. `queryMembers` already sent `filter ?? {}`;
`queryChannels` genuinely has no such constraint and stays conditional.

Also rewords the changelog line about the withdrawn negative operators, which
read as though the sample app filters client-side. It does not — it shows
everyone the query returns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): stop three docs claiming there is no release blocker

This PR is where `stream_core` stops being a hosted constraint, and three
documents kept asserting it is one.

The `openapi-codegen` skill is the dangerous one: it is auto-loaded, and its
own trigger is "changing the `stream_core` constraint", so an agent doing
exactly this work was handed a file saying the dependency is hosted with no
overrides, and that publishing is unblocked. It now describes the SHA pin and
says a git dep under `dependencies` makes the package unpublishable until it is
restored.

Also fixes this phase's definition of done, whose first item was an unticked
box reading "`Filter.notEqual`, `notIn` and `nor` are `@Deprecated`" — the
inverse of what the PR does. An unticked box reads as assigned work, so anyone
closing out the phase would have re-added three operators the API is
withdrawing. And `UPSTREAM.md` still named `$nor` as the top thing to push
upstream, which this PR deletes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc): read replacement extraData flags safely in `copyWith`

`ChannelModel.copyWith` cast `muted`, `blocked`, `disabled` and `hidden` out
of a replacement extra-data map with `as bool?`, so a consumer-authored
non-boolean threw where the matching getter answers null.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc): declare the filter fields the API accepts, and read them safely

`UserFilterField` had no `email` and `MemberFilterField` no
`notifications_muted`, though both are declared columns — `mq/user/user.go:61`
maps `email` to `custom->>'email'` with `$eq`/`$in`, and
`mq/member/member.go:197` takes `notifications_muted` with `$eq`. The member
side already declared the user's email as `user.email`, so the user side
lacking it read as an oversight rather than a decision.

Every typed field backed by extra data now goes through `safeCast` instead of
handing the raw `Object?` to the comparison. The sort registry already did
this (`user.dart:428`); the filter registry did not, so a server value of an
unexpected type would have compared as itself rather than as absent.

`ChannelFilterField.custom` and `ThreadFilterField.custom` name the fields the
server computes per request — `joined`, `has_unread`, `invite`, `distinct`,
`app_banned` — which are reachable only that way. They were listed in the
phase doc and nowhere a caller would look.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(llc): pin the filter registries' wire names

The sort registry pins its remote names and says why: a field the server
rejects fails the request at runtime, and no other test catches it. The filter
registry had the same test until it was dropped as duplicated bookkeeping.

Filters are the worse of the two cases. `mq/parser.go:270-277` treats an
unrecognised key as custom data wherever the resource declares a custom
container — channel, user, member, poll and thread all do — so a typo does not
fail the request. It compiles to `custom->>'typo'`, matches no row, and returns
an empty page that reads like a legitimately empty result.

The objection that killed the old test still stands: a hand-written list stays
green when the API grows a field. It only ever covered the other direction,
which is the one that ships a silent bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): stop the docs describing a branch state that no longer holds

The migration guide told callers to pattern match on `EqualOperator` and
friends. The barrel exports neither it nor any other subclass, so that does not
compile — `stream_chat.dart` shows `Filter`, `FilterField`, `FilterOperator`
and two abstract bases, and the bases are there only because `searchQueryLength`
needs them. `stream_feeds` re-exports core wholesale, so every operator class is
public there, and still nothing in it pattern matches: introspection is
`toJson()` in all 36 places it reads a filter. Both docs now say `toJson()`, and
the allowlist says why the two exceptions exist.

`DEFERRED.md` is named as the authority by this branch's `UPSTREAM.md` and by
the codegen skill, and it carried three rows this branch falsifies: adopting
core's `Filter`/`Sort` "blocked on core gaining `$nor`", which this branch does
by dropping `$nor`; deciding the fate of `Filter.custom`/`raw`/`empty`, decided;
and the platform detector blocked on a release carrying
`debugCurrentPlatformOverride`, which the pinned SHA already has. The row every
other one folds into — restoring hosted constraints — was missing entirely,
which is what `UPSTREAM.md` deleted its own list in favour of.

The codegen skill's "there is no release blocker" now leads with the thing that
is true for the reader it is written for: the pin makes three packages
unpublishable. The generated client needing nothing past 0.5.0 is a separate
sentence, not a headline.

Phase 08 gains what its own field table cannot carry: the spec is a field
oracle, not an operator oracle, since `mq/config.go:189` strips only internal
operators and `$ne`/`$nin`; and core's evaluation operators require a `String`,
so the array getters it credits work for `$eq`/`$in` but not for the
`$autocomplete` on `member.user.name`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): tick what this phase finished, and file the platform row where it waits

Two leftovers from the previous commit. The `CurrentPlatform` row sat under
"Blocked on something outside the repo" reading "nothing any more", which is the
same shape of staleness that pass was meant to remove; it waits on the restore
row, so it belongs under "Waiting on a decision or a later phase".

Phase 08's definition of done still had unticked boxes for work this branch
completed — deleting `filter.dart` / `sort_order.dart` / `comparable_field.dart`
behind the allowlist, and the registries themselves. An unticked box reads as
assigned work, which is the failure `93abff8f4` called out for the `@Deprecated`
item. `location_coordinates.dart` is genuinely still exported, so it splits into
its own row instead of being ticked with the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): file the two follow-ups this phase found but cannot fix here

Core's `QueryOperator` and `AutoCompleteOperator` bail on `fieldValue is! String`,
so a getter returning an `Iterable` never matches. That is a core fix, not a chat
one, and phase 08 noting it in prose is not where upstream asks are tracked —
`UPSTREAM.md` is. `ChannelFilterField.memberUserName` is the field it bites, and
`stream_feeds` calling `matches()` in 36 event handlers is where it surfaces.

`UserFilterField.name` and `.custom` document fewer operators than the spec or
`mq/user/user.go` allows, on the strength of 400s nobody recorded. Source
contradicting a doc comment is how a correct narrowing gets widened back, so it
goes in the live-check section with the other things mocks cannot settle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): write the changelog for people who never add stream_core

An integrator sees `StreamApiException` and `ChannelFilter` in their own code
but never puts `stream_core` in their pubspec, so naming it in a release note
explains the change in terms of a package they cannot see. Each entry now says
what moved without naming where it moved from.

Also: phase 02's "read the resolved package" note pointed at the hosted copy,
which is not what resolves while `melos.yaml` pins a git ref — the exact
mistake the note exists to prevent. And whether `ChannelModel` should promote
its `extraData`-backed flags to real fields is recorded against the group that
owns that model.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc): drop ChannelFilterField.filterTags from the typed registry

`filter_tags` is deprecated as customer guidance — new apps are pointed at
custom-data filtering or predefined filters — but nothing in the API says so.
It is a live column with a GIN index, it is in the spec both ways, and Swift
declares a hand-written `FilterKey` for it, so every source this registry was
derived from argues for keeping it. Declaring it as a typed field reads as a
recommendation for a path we tell customers not to take.

`ChannelFilterField.custom('filter_tags')` still reaches it, and
`ChannelModel.filterTags` stays: reading a value the server sends is not the
same as recommending a query.

Recorded in the phase doc, because a reader deriving the registry from the
column list will otherwise add it straight back — this phase did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…er and list extensions (#2959)

* refactor(llc)!: adopt stream_core's LocationCoordinate

Chat's `LocationCoordinates` was two doubles and a `copyWith` nothing
called. Core's `LocationCoordinate` carries the same pair and adds
`distanceTo`, so the duplicate goes.

Equality changes with it: core compares to a 1e-7 epsilon rather than
demanding identical doubles, so a coordinate that has round-tripped
through the API still matches the one it came from.

This closes phase 08.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): bring the query DSL phase doc up to what shipped

Its decisions were still open questions, its upstream section still said
none was needed, and its definition of done still called for deprecating
`$ne`, `$nin` and `$nor` — all three were removed instead, since the API
is withdrawing them.

The one box left open is the serialisation diff: the api tests assert
request payloads and every documented example was executed, but no
systematic before/after comparison was run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): verify the uploads phase against the code

Records Option A as decided, with the check that could have sunk it:
`CdnClient` takes core's `AttachmentFile`, a different type of the same
name from the one chat's models use, and the conversion is total only
because core ships `fromData` for the bytes-only case chat's nullable
path exists to serve.

Corrects three things the code disagreed with. The provider is wired at
`client.dart:94`, `UploadState`'s variants were renamed by phase 03, and
phase 05 has not landed, so nothing deprecates the provider typedef yet.

States the persistence argument accurately: `file` and `upload_state`
ride inside a `text()` column as JSON, so retyping them changes a
payload rather than a schema, and a `schemaVersion` bump already drops
the rows. Keeping the models rests on the web case and on surviving an
app restart, not on a migration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): bring the cleanup phase doc up to the current tree

Three of its dependency rows were already settled: `logging` is gone
with 06, `equatable` stays because 28 files use it, and `diacritic` is
actionable now — chat still keeps a copy of `normalizeStringForSort`
that differs from core's only in its doc comment.

The barrel listing showed a `logging` export that no longer exists and
called `async` wholesale when 03 narrowed it, leaving two wholesale
re-exports rather than three.

The rename table now says which transforms can actually be written. A
`dart fix` rule needs the old name gone and the new one in place, and
four of the eleven renames have not happened yet — one of them in a
phase that is parked.

Records that the kit-before-08 question was answered by events: 08
landed without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc): use stream_core's normalizeStringForSort

Chat kept its own copy after the function went upstream, differing from
core's only in its doc comment. The three sort fields that fold a name
now use core's, and `diacritic` is no longer a direct dependency of
`stream_chat`.

Nothing public moves: chat's copy was `@internal` and never exported,
and core's tests carry the same 24 assertions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): record why a name filter matches only approximately

`user.name` and `channel.name` are stored normalized and the API
normalizes the filter value before comparing, so it compares normalized
against normalized. We control only the field side, so folding the
getter would not reproduce that — it would move which inputs disagree,
and break the natural call to fix the unusual one. Sorting is the
opposite case, and that is why the two registries differ.

Also corrects the neighbouring paragraph: `matches()` on a raw leaf
throws now rather than returning true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): correct the deferred and upstream indexes

Three deferred rows had landed. The query DSL one was blocked on a
premise that turned out false — core gaining `$nor` — when no core SDK
models it and chat removed all three withdrawn operators instead.

Upstream still described `matches` on a raw leaf as returning true; it
throws. Element-wise array matching, the third change in core#181, was
missing entirely, and chat has already deleted its copy of
`normalizeStringForSort` rather than waiting for a release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc)!: adopt stream_core's list extensions

Deletes `core/util/list_extensions.dart`. Every helper it held now lives in
`stream_core`: `sortedUpsertAt` and `mergeSorted` (as `sortedMerge`) were
added there, `updateIf` became `updateWhere`, and `mergeFrom` is expressed as
`merge` over a projected list. `SortedListExtensions` is re-exported from the
barrel in its place, so `SortedListX`, `IterableMergeX` and `ListX` are gone.

Behaviour is unchanged on the paths this package uses: 40k randomized merges
across tie-heavy inputs produce output identical to the version removed, and
the two `sortedMerge` call sites in `ChannelClientState` measure 0.97-1.01x
against it, 0.92-0.95x where the incoming batch shares no id.

The one visible difference is that a repeated key now collapses instead of
being carried through. That tolerance was a workaround for duplicates the old
merge manufactured itself, through a concat fast path that skipped dedup on
inputs whose `createdAt` disagreed between memory and the Drift cache. Both
causes are fixed — #2660 removed the fast path, 7f0804d made persistence
store ISO-8601 text — so the workaround has nothing left to guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): record that persistence already tiebreaks on `(createdAt, id)`

`message_dao.dart` orders `getMessagesByCid` — the query that feeds channel
state — by the tuple, while the in-memory `_sortByCreatedAt` compares
`createdAt` alone. So the two disagree on messages sharing a timestamp, and a
channel can render in one order from disk and another after a merge. That
makes adding the tiebreaker a matter of closing an internal gap rather than
picking a convention, which is worth knowing before the cross-SDK check.

Notes `getThreadMessages` separately: it orders by `createdAt` alone where
both of its siblings use the tuple.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Revert "docs(repo): record that persistence already tiebreaks on `(createdAt, id)`"

This reverts commit cb1eb8f.

The note misread why `message_dao` orders by the tuple. It is there for keyset
pagination — the cursor predicates compare the same `(createdAt, id)` key, so
rows sharing a `createdAt` fall on the correct side of a page boundary — not
because the SDK had settled on `id` as a display tiebreak. Recommending the
comparator follow it was built on that mistake.

`Message.id` is a v4 uuid, so ordering ties by it is deterministic and
meaningless: it says nothing about when a message was sent or arrived, where
a tie almost always means two messages sent at nearly the same moment and
arrival order is what the reader expects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): write up the message ordering investigation

Records why messages sharing a `createdAt` change places, and — more useful —
which of the obvious fixes are wrong, with the measurements behind each.

A tiebreak on `id` is deterministic and meaningless: `Message.id` is a v4
uuid, and `message_dao`'s `(createdAt, id)` is keyset-pagination machinery
rather than a chosen display order. Stamping `localCreatedAt` at construction
does not give a total order either — 500 of 500 rounds building five messages
produced duplicate timestamps, because `DateTime.now()` cannot resolve that
fast. And `client.dart:646` only looks like the same defect; it sorts `/sync`
events, whose replay sequence is meaningful.

What is left is tie-stability in `sortedMerge`, measured at 1.08-1.39x, which
is why `stream_core` ships without it. The `DEFERRED.md` row now says that
rather than pointing at the comparator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): map the list extension renames in the v11 guide

`aaba3734b` removed `SortedListX`, `IterableMergeX` and `ListX`, all of them
publicly exported, and shipped only a CHANGELOG entry. Anyone calling
`mergeSorted`, `updateIf` or `mergeFrom` had nothing to look up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): close the utilities phase

`list_extensions` was the last open row. It moved to core once `sortedMerge`
and `sortedUpsertAt` measured at parity with ours, so the phase doc no longer
describes it as deferred pending a benchmark.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc): sort pinned messages before merging into them

`updateChannelState` assigned `pinnedMessages` straight from the payload while
`messages` went through `sortedMerge`. Every later write reaches
`_mergePinnedMessagesIntoExisting`, which merges by `createdAt` — and the API
returns pinned messages by `pinned_at` descending, so the receiver was in the
wrong order.

Chat's own helpers reordered silently. `stream_core`'s assert that a receiver
is sorted, so the next message event in a channel with two or more pins threw
`AssertionError` in any debug build, including the SDK's test suite. The
existing pinned tests never exercise more than one pin, so nothing caught it.

The merge already produced `createdAt` order, so sorting on the way in makes
the invariant hold from the first assignment rather than from the first merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(core): reach `sortedWith` through `stream_chat` again

The controllers named `stream_core` directly because this package's barrel
still exported its own `SortedListX`, which declares three of the same members.
That extension is gone as of this phase, so the barrel carries core's and the
direct dependency is redundant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(llc): point the split sort test at core's normalizeStringForSort

The phase moved the normalizer and updated `sort_registry_test.dart` because
git tracked that file through its rename. `sort_comparison_test.dart` is new in
the same phase that split them, so it kept the chat-side import and stopped
resolving here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(persistence): sort cached channels stably before paging them

`List.sort` makes no promise about channels the comparator calls equal, so two
reads of the same cache could order a tie differently and the offset below
would skip or repeat a cid between pages. `sortedWith` is a merge sort.

Reachable here only from this phase on, which is where the barrel starts
exporting `SortedListExtensions`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(llc): keep stream_core out of these entries too

Same reason as the rest of the section: an integrator reads the new names in
their own code and never adds the package that declares them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): file the pinned-message ordering this phase exposed

Sorting the payload by `createdAt` on the way in is what makes the existing
merge sound; it is not the order a pinned list should show. Ordering by
`pinnedAt` is the real fix and needs a null-ordering decision, so it is
recorded rather than folded into this phase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* revert(llc): stop sorting pinned messages on the way in

Unrelated to adopting core's list extensions, so it belongs in its own change
rather than riding along here. The test that pinned it goes with it.

What this leaves behind is recorded in DEFERRED.md: core's `sortedUpsertAt`
asserts its receiver is sorted, so a pinned payload in `pinned_at` order now
trips that assert in debug. Release builds strip it and misplace the message,
which is what v10 already did. Both halves get fixed together by comparing on
`pinnedAt` instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): record core's coordinate equality as an upstream ask

`LocationCoordinate.==` compares with a `1e-7` epsilon while `hashCode` hashes
the raw doubles, so the type cannot be a `Set` element or `Map` key, and the
epsilon is non-transitive, so no hash can ever agree with it. It also makes
`distanceTo` report exactly 0m for anything under ~1.1cm, since that method
short-circuits on `==`.

Nothing here depends on the tolerance, and neither does feeds, so the fix is
free today — but feeds' `ActivityData` and `FeedData` already carry the field
through freezed, which generates both members over it.

Also: the closing advice said to read core from the hosted pub-cache, which is
not what resolves while `melos.yaml` pins a git ref — the same staleness the
platform phase carried.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc)!: pin the core commit that makes coordinate equality exact

`LocationCoordinate.==` compared within a `1e-7` epsilon while `hashCode`
hashed the raw doubles, so the type could not be a `Set` element or `Map` key,
and the epsilon was non-transitive, so no hash could have agreed with it. It
also made `distanceTo` report exactly 0m for anything under ~1.1cm, since that
method short-circuits on `==`.

Fixed in core#181 with `Equatable`, which is what sixteen other classes there
use, so the two members cannot drift apart again. Proximity is `distanceTo`:
`a.distanceTo(b) <= 1.meters`.

Drops the changelog line advertising the tolerance, which is no longer true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): add the coordinate fix to what a core release has to carry

Pinning the commit that makes `LocationCoordinate` equality exact adds a fifth
thing the branch needs from core before the hosted constraints can come back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…exception family (#2960)

* refactor(llc)!: adopt stream_core's platform detector

`CurrentPlatform` and `PlatformType` were a near-verbatim fork of core's.
Deleting ours drops the `js_interop` branch chat needed only because its own
stub threw; core's non-`io` fallback answers `web` directly.

BREAKING CHANGE: `CurrentPlatform.name` is now `CurrentPlatform.operatingSystem`
and reports the same string. Both types are re-exported from this package, so
an import of `stream_chat.dart` needs no change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc)!: adopt stream_core's sealed exception family

Replaces our error class tree with core's four `StreamException` kinds and
deletes `stream_chat_error.dart` outright. Two of its types were scheduled to
outlive this phase; neither needed to. Nothing raised `StreamChatNetworkError`
once the verb facade threw a `StreamException`, and the socket path maps onto
core's kinds without waiting for the new transport.

Deleting beats deprecating here: nothing throws the old types, so a deprecated
`on StreamChatNetworkError catch` would keep compiling and silently match
nothing. Removing them turns that into a compile error.

Also fixes five `is` checks on the old type that had gone unreachable — four
`scheduleRetry` gates in `channel.dart` and the `sync` 400 recovery in
`client.dart`. A failed message was never queued for retry, and an app with a
stale `last_sync_at` could not heal. The tests covering them asserted message
state rather than the queue, so they passed against dead branches.

BREAKING CHANGE: `StreamChatError`, `StreamChatNetworkError`,
`StreamChatNetworkErrorType`, `StreamWebSocketError` and `ChatErrorCode` are
removed. Catch `StreamChatException` — an alias of core's sealed root — or one
of `StreamApiException`, `StreamNetworkException`,
`StreamAuthenticationException` or `StreamClientException`. `isRetriable` is
now an extension on the alias, and `stream_chat_flutter`'s three
attachment-validation errors moved to their own sealed
`AttachmentValidationError` family. See migrations/v11-migration.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): close the token and auth phase

A live anonymous connect against a real app key succeeds, which was the last
open item.

Records why that had to be tested rather than argued. Reading the backend
source end to end predicted the opposite: `handshake` validates before it
authenticates, `ConnectUserDetails.ID` carries `validate:"userID,required"`,
and that tag's regex does not match `!anon`. Every link holds on its own and
the conclusion was still wrong, on a path the backend has no test for either.
Why the request passes is left unexplained rather than guessed at.

What does survive is that the server discards the client's id on the anonymous
path — it rebuilds its own user — so the switch to `!anon` could not have
changed behaviour there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* style(llc): split the token provider call across lines

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(llc): pin the interceptor pipeline order

The existing interceptor tests assert each type is present, which a fully
reordered pipeline satisfies just as well, and `ApiErrorInterceptor` had no
test at all. Order is behaviour here: anything rejecting ahead of
`ApiErrorInterceptor` escapes as a raw `DioException`, and anything logging
ahead of it logs the transport error rather than the mapped one. Verified the
assertion fails on a swap rather than only passing as written.

Also corrects the phase doc, which recorded "six verbs, not eight" as done.
`fetch` and `request` are still on the facade with no caller in `lib/`; they go
when `StreamHttpClient` becomes `@internal` in phase 09, rather than breaking a
public class twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): re-point the uploads phase at what 05 actually did

Phase 09 assumed 05 would delete `StreamHttpClient`, which would have forced
`AttachmentFileUploaderProvider` to be retyped. 05 kept it instead — it is the
verb facade — so the typedef compiles unchanged and nothing forces the retype.
What was an obligation is now a choice, and the two phases pull opposite ways:
leaving the typedef alone keeps 05's `@internal` item open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): record that the branch needs an unreleased stream_core

`melos.yaml` declares `stream_core: ^0.5.0` but `dependencyOverridePaths`
resolves it to the sibling checkout, whose pubspec also says 0.5.0 while
carrying APIs the published version does not. Every local run has therefore
been green against code no consumer can get.

Chat already depends on five of them: `debugCurrentPlatformOverride`,
`sortedMerge`/`sortedUpsertAt`, `normalizeStringForSort`, `Filter.raw` and
element-wise array matching. `Filter.raw` is the sharpest — persistence's
filter converter and `PredefinedFilter` both need it.

Also drops the rows this migration has since closed, and fixes UPSTREAM's
closing advice, which still named `$nor` (dropped) and
`debugCurrentPlatformOverride` (landed) as what to push first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): remove the contradictions a cold reader would trip on

Three places said the opposite of what this stack does, each true when written
and stale a rung or two later.

The migration guide carried both halves of a reversed decision in one
blockquote: the paragraph saying `StreamChatNetworkError` is deprecated, telling
readers "the deprecation warning tells you where", directly above the paragraph
saying it is deleted. The first was mine — I appended the replacement without
removing what it replaced. A reader upgrading would have searched for warnings
that never appear, because the type is gone and the failure is a compile error.

Phase 02's doc still said "merging is gated on a core release" and marked the
platform detector blocked, in the PR that deletes it, with its own
definition-of-done fully ticked. `DEFERRED.md` described the resolution
mechanism as `dependencyOverridePaths` against a sibling checkout, which is what
it *was* before CI proved it only works on a machine that has one.

And the schema version reads `1102`, which is what the database declares.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(llc)!: classify each failure as ERROR_LAYER.md says to

Three corrections against the spec, all in the sealed family this branch
adopts.

A cancellation is a cancelled request. A send superseded by a later one,
and a pending upload dropped because its message was deleted, were
`StreamClientException` — the kind whose documented reaction is "report
to your crash tracker". They are now `StreamNetworkException` with
`isCancelled` set, which is how a cancelled attachment upload was already
reported two hundred lines away.

Misuse is not in the hierarchy at all. Connecting twice, opening a
connection that is already open, `queryChannels` without one, a
persistence client that is not set or belongs to another user, and
cancelling an upload that never started — all say fix the call rather
than handle the failure, so they raise `StateError`. `queryChannels`
without a connection is close to the spec's own example.

An upload failure said `Failed to upload one or more attachments` and
nothing else. It now names which ones and what each reported. A real
`cause` is out of reach until `UploadStateFailed` holds more than a
string, which is phase 09's model swap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): let the README say what is true at the tip

The rung that introduced these paragraphs states them as of itself, and
they flow up unchanged, so by here three had gone stale.

The collision list is down to `AttachmentFile` and `User`: the query DSL,
the platform detector, the token and interceptor types and the logger
types are all core's by now. Nothing upstream is blocking either — phase
08's changes shipped in stream-core-flutter#181, and the operators an
earlier pass called a hard block turned out not to be asks at all.

07's findings do reach its own file by this rung, so the parked note can
point at them again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(repo): correct the sorting section a reader would follow

The section still described `sort: null` as coercing to the default — the
setter no longer takes null, and `XSort.empty` is what sends none — and told
every poll-vote caller to undo a default flip that was reverted before it
shipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ui): drop the duplicate @OverRide left by removing props

`AttachmentBlockedError.toString` carried the annotation from the deleted
`props` getter as well as its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ui): say what addAttachment throws

The `value` setter beside it already documents its refusal; this one threw the
same family silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ui): give the attachment errors a summary paragraph, and say who throws them

Effective Dart wants a one-sentence summary in its own paragraph; the addition
in 0548bb5 ran the summary and the detail together.

The family doc also said these are "returned rather than thrown", which sent me
looking in the wrong place: `StreamAttachmentPickerController` throws them from
`addAttachment` and from its `value` setter. It now says so, and states the
consequence a caller needs — an `on StreamChatException` clause does not catch
one — rather than naming the package the type does not come from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…2963)

* fix(llc): match a channel's members exactly, the way the API does

`collectionEquality` defaults to `containsAll`, which is how every array
column answers `$eq` — except a channel's `members`, where `$eq` looks a
distinct channel up by its exact membership. The server builds that with
`string_agg(user_id, ',' ORDER BY user_id) = ?` over the candidate set and
a `HAVING count(*) = ?` rejecting a channel holding anyone else, so it is
exact for every channel query rather than only the distinct fast path.

Left at the default, a 1:1 query for `[a, b]` also matched a group holding
`[a, b, c]` once the filter was evaluated locally.

Every `FilterField` subclass forwards the parameter. Only `members` asks
for anything but the default today, and a test pins that so a new
collection field has to make the choice rather than inherit it.

Closes FLU-789.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* build(repo): take stream_core from core's main, overriding its one source

`collectionEquality` lands in `stream_core` on core's `main`, so the pin
moves there from the `chat-integration/v11` branch it sat on.

That branch carried one commit main cannot: `stream_core_flutter` depending
on `stream_core` by path. Main keeps the hosted constraint, because a path
dependency would block publishing it — but a hosted `stream_core` is a
second source for a package this workspace takes from git, and pub allows
only one. Each package seeing both overrides `stream_core` to the same ref,
which is how they all resolve to a single copy again.

The overrides are scaffolding for the same window as the pins themselves
and come out when core publishes a release carrying these APIs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(llc): keep collection equality out of the filter field API

`collectionEquality` describes how the API compares a field, not something a
caller picks, so a public parameter only invited a wrong answer. It moves to a
private constructor that `members` uses, and comes off the ten field types that
never needed it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

⚠️ Database Entity Files Modified

The following database entity files have been modified in this PR:

packages/stream_chat_persistence/lib/src/entity/channel_queries_metadata.dart

📝 Remember to:

  1. ✅ Database schema version bumped to 1103.
  2. Update entity schema tests if necessary.

Note: This comment is automatically generated by the CI workflow.

xsahil03x and others added 8 commits September 16, 2026 16:19
`Merge branch 'master' into v11` brought #2945 across unchanged, so v11 has
been red since: `sync_manager.dart` imports `../core/error/error.dart` and
`../core/models/filter.dart`, both deleted on this branch, and its tests use
`Filter.empty`, `SortOrder`, `ChatErrorCode` and `StreamChatNetworkError`.

Ports it across: `package:logging` -> `StreamLogger`, `StreamChatNetworkError`
-> `StreamApiException`, `ChatErrorCode` -> `StreamErrorCode`,
`Filter.in_('cid', ...)` -> `ChannelFilter.in_(ChannelFilterField.cid, ...)`,
`SortOrder<ChannelState>` -> `List<ChannelSort>`, and the two `StreamChatError`
stubs -> `StateError`, which is what the client raises for these now.

A filter compares by identity on v11, so the `when`/`verify` calls naming one
would have compiled but never matched — silently unstubbing the failing page in
`should attempt every page when one of them fails`. They go through the
existing `isSameFilterAs` matcher instead.

Verified: `melos run analyze` clean, and `stream_chat` (1789),
`stream_chat_persistence` (309) and `stream_chat_flutter_core` (369) all green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Constructs a `StreamLogger` from a `tag` rather than taking one, matching how
every other component names itself, so a catch-up's records select under
`SCh:Sync` instead of arriving under the client's tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…multiline ones

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… level

`RetryQueue` logged a line per channel on recovery — thirty-plus identical
lines in a modest app — and `SyncManager` logged the whole cid list, which runs
to a screenful. Both move to debug, with the sync keeping a count at info.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sages

Constructing a channel object is per-object bookkeeping, so it moves to verbose
and stops repeating the cid the tag already carries.

The sync messages named `lastSyncAt` and a "refused window" — implementation
terms a reader has no way to interpret — and now say what happened instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xsahil03x and others added 6 commits September 18, 2026 18:38
`pull_request_target` reads its workflow from the base branch, so the filter
naming `v11` has to be here for a PR into it to be checked at all. Every rung
of the v11 stack went unchecked, and the branch ruleset could not ask for the
check because nothing could report it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…Client (#2970)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…2973)

* refactor(llc)!: route searchRoles through the generated client

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

* test(ui): drop a stray blank line in the mention autocomplete test

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

* refactor(llc): give the generated client its own configured Dio

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

* test(docs): compile the roles autocomplete stub against the generated response

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

* refactor(llc): keep RoleType as the searchRoles filter type

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

* refactor(llc): trim the generated client's comments and drop an unused mock

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

* fix(llc): order the generated client's interceptors to match the hand-written chain

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

* test(llc): drop the generated Role and SearchRolesResponse model tests

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

* test(llc): assert RoleType string interop without a redundant constructor call

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

* docs(repo): let the plan generator reproduce a finished group's record

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#2977)

* refactor(llc)!: route searchRoles through the generated client

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

* test(ui): drop a stray blank line in the mention autocomplete test

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

* refactor(llc): give the generated client its own configured Dio

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

* test(docs): compile the roles autocomplete stub against the generated response

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

* refactor(llc): keep RoleType as the searchRoles filter type

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

* refactor(llc): trim the generated client's comments and drop an unused mock

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

* fix(llc): order the generated client's interceptors to match the hand-written chain

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

* test(llc): drop the generated Role and SearchRolesResponse model tests

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

* test(llc): assert RoleType string interop without a redundant constructor call

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

* docs(repo): let the plan generator reproduce a finished group's record

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

* refactor(llc)!: route device registration through the generated client

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

* refactor(llc)!: keep PushProvider as the addDevice provider type

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

* test(llc): cover device decoding through OwnUser

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

* docs(repo): split push preferences out of the devices group

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

* docs(llc): drop the push provider name comment that explains old code

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

* refactor(llc)!: answer nothing from addDevice and removeDevice, and drop the PushProvider alias

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
VelikovPetar and others added 20 commits September 22, 2026 15:23
…on (#2986)

* docs(repo): require the style and testing guides in every v11 migration

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

* docs(repo): vendor Effective Dart's documentation guide and require it in migrations

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lic API (#3004)

* refactor(llc)!: keep the generated device and role models off the public API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): plan the migration around domain models over the generated client

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc): name the domain mappers toModel and toRequest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): list the sanctioned breaks of the domain-model migration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): cover the domain mappers directly

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): unify the generated-client prefix as api and name tests by behavior

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc)!: answer nothing from addDevice and removeDevice

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc)!: make the device and role models freezed and keep them beside the other models

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): document the searchRoles cursor and align the role type tests with the testing guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc)!: make PushProvider an extension type and type Device.pushProvider with it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style(llc): format the regenerated device model

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc): drop the value of a duration-only write through ignoreResult

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc): rename ignoreResult to ignoreValue

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): prefer extension types over enums for server-defined values

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…2996)

* refactor(llc, ui)!: keep the generated OG response off the public API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): record the enrichUrl migration against domain models

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc): move OGAttachmentResponse into models/response

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): drop the unreachable enrichUrl error handler in the composer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc, ui): keep OGAttachmentResponse.ogScrapeUrl non-null

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): cover enrichUrl through the public client only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test(llc): name the device and role client tests by behavior

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc)!: keep the generated user group models off the public API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): record the user groups migration against domain models

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc): generate the user group storage codec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(persistence): store mentioned groups through the generated codec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): allow @DataSerializable as the temporary storage codec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): teach the migration skill when to use @DataSerializable

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): record fromData and toData as the offline storage format

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): drop the annotation check from the migration plan tool

* docs(llc): move the v1 group decoding comment onto its function

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): cover reading mentioned groups from v1 and v2 payloads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): restore the user group method contracts on StreamChatClient

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): say which cached rows the user group codec stays compatible with

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc): move the response envelopes into models/response

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): describe the user group cursors as pairs and split the member docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): cover the user group failure path on the client and drop duplicate repository tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): fold the user group changelog entries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): name the v11 write path the user group codec matches

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): name the remaining user group client tests by what they check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): decode mentioned groups with epoch-nanosecond dates through Message.fromJson

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): keep the user group changelog clear that model equality is unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): name the device and role client tests by what they check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): drop the stale helpers reference from the user groups plan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): state the user group equality change once in the changelog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): drop the epoch-nanosecond group test that Message.fromJson already covers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…nt (#2992)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…3018)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
)

* test(llc): cover the migrated endpoints through the public client

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): restore the empty-member, provider-name and query-only checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): test mapping through the public client in the migration plan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(llc)!: route app settings through the generated client

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): record the app settings migration against domain models

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc): move the response models into models/response

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): cover app settings through the client and the manager

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): name the app settings client test after its outcome

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc)!: rename GetAppSettingsResponse to AppSettingsResponse

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): use an before AppSettingsResponse in the mapper docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…tructure (#3012)

* test(llc): name the device and role client tests by behavior

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc)!: keep the generated user group models off the public API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): record the user groups migration against domain models

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc): generate the user group storage codec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(persistence): store mentioned groups through the generated codec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): allow @DataSerializable as the temporary storage codec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): teach the migration skill when to use @DataSerializable

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): record fromData and toData as the offline storage format

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): drop the annotation check from the migration plan tool

* docs(llc): move the v1 group decoding comment onto its function

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): cover reading mentioned groups from v1 and v2 payloads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): restore the user group method contracts on StreamChatClient

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): say which cached rows the user group codec stays compatible with

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc): move the response envelopes into models/response

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): teach the migration skill the domain-model layout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): sanction the envelope and nullability breaks in the migration plan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): note the shared poll responses in the polls plan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): describe the user group cursors as pairs and split the member docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): cover the user group failure path on the client and drop duplicate repository tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): fold the user group changelog entries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): name the v11 write path the user group codec matches

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): name the remaining user group client tests by what they check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): decode mentioned groups with epoch-nanosecond dates through Message.fromJson

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): keep the user group changelog clear that model equality is unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(llc): name the device and role client tests by what they check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): drop the stale helpers reference from the user groups plan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(llc): state the user group equality change once in the changelog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(repo): address review remarks on the openapi-migration skill

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Carries #3008's two app-settings fixes into v11's generated-client
structure: loadAppSettings no longer overwrites a refresh that finished
first, and the mapper maps an unset size limit (0) to
UploadConfig.defaultSizeLimit, since the merged validator reads
sizeLimit as given.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… refuses a guest (#3032)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…3019)

* refactor(llc)!: route connectGuestUser through the generated client

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc): apply review remarks on connectGuestUser and the user mappers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(llc): rename the internal guest envelope to CreateGuestUserResponse

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/stream_chat_flutter_core/CHANGELOG.md

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants