Render `EndpointConfig.iconURL` in drawer + conversation list rows with
per-conversation override via `Conversation.iconURL`. Extract shared
`EndpointIcon` composable to `:core:ui`. Eager `/api/config` fetch via
`EndpointConfigFetchSessionTask` so cold-start has icons. Clear config
cache on logout to prevent cross-user leak.
Closes#68.
Pre-#67 builds wrote Conversation.endpoint as the Kotlin enum `.name`
(e.g. "OPENAI"). #67 added a read-side shim in ConversationMapper that
converted those legacy rows back to the wire-format SerialName on read.
Replace the shim with a one-time data migration so the conversion runs
once per user on db upgrade and the read path is plain.
Bump database to v4 and register MIGRATION_3_4 on both Android and iOS
builders. The migration runs nine UPDATE statements per affected column
(conversations.endpoint, conversations.endpointType, presets.endpoint).
The presets table is migrated defensively — PresetDao currently has no
production callers, so the column is empty in the field, but symmetry
is enforced if a future change starts persisting presets.
Schema v4.json is structurally identical to v3.json; identityHash is
unchanged (b4117797915eafc13b5bd956677f83bf). Only the version field
differs.
Pre-#67 ChatPayloadBuilder coerced any unknown endpoint name to
EModelEndpoint.AGENTS, so a legacy "AGENTS" row may have originated
from any custom endpoint. The migration rewrites "AGENTS" -> "agents",
preserving the same lossy outcome the read-side shim already produced.
A dedicated test pins this contract so a future "smart" migration can't
silently change cache/fresh consistency.
Verified via :core:data:connectedDebugAndroidTest (5 tests, all green:
existing v1->v3 helper test, v1->v4 Room API test, v3->v4 endpoint
normalization, v3->v4 lossy AGENTS, v3->v4 preset normalization).
ChatPayloadBuilder caught EModelEndpoint.valueOf for custom endpoint
names (OpenRouter, Deepseek, etc.) and silently sent endpoint=agents,
producing 403 "Forbidden: Insufficient permissions" on the server.
Type endpoint identity as String end-to-end, add EndpointClassifier to
derive endpointType / key / modelDisplayLabel from /api/config, and
propagate the dispatch fields to all five chat-send sites in
ChatViewModel (doSendMessage, editUserMessage, editAiMessage,
regenerateMessageNow, continueGenerationNow). A normalization shim in
ConversationMapper converts legacy enum-name rows in the Room cache
back to wire format on read.
Verified end-to-end against a docker LibreChat v0.8.5 instance with
OpenRouter user_provided keys; built-in .env Anthropic regression also
passes.
AgentRepositoryImpl.cachedAgents and PresetRepositoryImpl.cachedPresets
are read-checked, fetched across a suspension point, then written. With
@Volatile alone, two coroutines can both observe null, both issue the
network request, and both write the cache — a redundant API call per
cold-cache race. Wrap read-check-write in a Mutex so at most one fetch
runs per cold cache; invalidateCache() becomes suspend to match.
Wraps `CommonTokenDataStore.clearTokens()` in the existing `refreshMutex`,
serializing logout against any in-flight `refreshAccessToken()` call
(which already holds the same mutex for its full HTTP duration). Without
this, a refresh completing successfully after `clearTokens()` would
silently resurrect the just-killed session by writing a fresh token to
disk. A private `clearTokensLocked()` helper holds the inner clear logic
and is called from inside `refreshAccessToken()` to avoid self-deadlock
on the non-reentrant mutex.
- New D3: two mandatory independent review passes after the verifier signs
off — `reviewer` (hasn't seen verifier output) then `auditor` (after any
fixes land). Two independent passes catch different things; a sync PR is
too large to ship after a single review. Implementer fixes; reviewers
do not.
- New D5: dispatch per-section test-runner teammates after presenting the
report. Section split scales with sync size — small (per-platform) vs.
medium/large (per-feature-area × platform).
- Existing D3 (Present results) renumbered to D4.
Captures the v0.8.5 sync experience: a single-pass verifier + monolithic
device-test plan are insufficient for syncs that touch dozens of files
across SSE/auth/UI surfaces.
Upstream LibreChat ships breaking API and SSE-shape changes inside patch
releases (e.g. 0.8.4 → 0.8.5 changed the SSE payload contract), so the
mobile compat surface must compare patch as well as major.minor.
- isCompatible: exact (major, minor, patch) equality
- isCompatibleOrNewer: lex compare over (major, minor, patch); fixes a
silent bug where 0.8.5-only feature gates returned true on 0.8.4
- VERSION_GATES.md updated to reflect patch-aware semantics
- New BackendVersionTest covers parse, isCompatible, isCompatibleOrNewer,
and extractVersionFromFooter
Targets upstream tag v0.8.5 (commit 9ccc8d9be). Bumps the supported
backend version constant, advances the upstream submodule pin, and
adds wire-protocol parity for new server features.
Wire protocol
- Add ContentType.SUMMARY enum value and flat sibling fields on
MessageContentPart (content as JsonElement to absorb array/string/
legacy-text variants per BaseClient.getSummaryText).
- Add Anthropic effort.xhigh dropdown value, ThinkingDisplay (Opus 4.7
reasoning visibility), and Message.contextMeta pass-through.
- Add allowAccountDeletion to StartupConfig (defaults to true to
preserve older-server behavior).
- Drop unused instanceProjectId from StartupConfig and from
ConfigRepositoryImpl.isValidLibreChatConfig.
New endpoints / features
- Favorites: UserFavorite DTO + FavoritesApi + FavoritesRepository,
Settings -> Favorites sub-screen (list/unpin), pin icons in chat
model selector. Repository owns the canonical list as a StateFlow
guarded by Mutex.withLock so chat-side toggles and settings-side
bulk-unpin can't drift apart and concurrent writes can't lose each
other.
- Prompt-group usage telemetry (POST /api/prompts/groups/:id/use,
fire-and-forget on prompt insertion).
- Summarize SSE events (on_summarize_complete -> StreamEvent.ContextSummary)
and a collapsible "Summarized earlier messages" card rendered from
the persisted SUMMARY content part. Forward-compat else-arm preserved
on both LangGraph and legacy SSE mappers.
Backward compatibility
- BackendVersion.isCompatibleOrNewer helper added; documented fail-open
contract for callers that have already null-checked
detectedBackendVersion.
- New VERSION_GATES.md catalog at repo root tracks every code path that
branches on server version. Initial entries:
- isCollaborative agent toggle (hidden on v0.8.5+; full-ACL UX deferred)
- xhigh effort dropdown value (filtered out on <v0.8.5 to avoid
server-side enum rejection)
Defensive hardening
- ChatApi.startChat asserts Content-Type: application/json before
decoding ChatStartResponse and catches NoTransformationFoundException
as a backstop, translating both to a friendly ApiException. Observed
during testing on v0.8.5-rc1 with summarization enabled, where the
agents chat-start endpoint returned 200 OK with no Content-Type and
Ktor leaked a NoTransformationFoundException stack trace into the
chat surface. Belt-and-suspenders against future backend regressions
or unsupported-server pointing.
Documentation
- DISCOVERY.md updated for the v0.8.5 surface: /api/endpoints now
requires JWT, /api/config pre-auth/post-auth split, allowAccountDeletion,
instanceProjectId removal, favorites endpoints (with all three XOR
variants: agentId | model+endpoint | spec), prompt-usage endpoint,
/api/admin/** marked out-of-scope. Plus a new MessageContentPart +
SUMMARY wire-shape section.
iOS
- Swift SharedFrameworkTest.swift updated for the new
StreamEvent.ContextSummary variant and Message.contextMeta
constructor argument.
Out of scope (by design)
- /api/admin/** route family - web-only feature.
- Full ACL migration for agent sharing - replaced by version-gating
the legacy isCollaborative toggle.
- Spec-pinning UI on mobile - UserFavorite.spec round-trips for
fidelity with web-authored pins, but mobile does not yet surface
a spec picker.
- Replace hardcoded simulator framework path with SDK-conditional paths
so Xcode automatically resolves iosSimulatorArm64 or iosArm64 based
on the selected build target
- Fix Compose Resources build script to use \$PLATFORM_NAME instead of
\$CONFIGURATION for platform detection
- Add CODE_SIGN_IDENTITY override for iphoneos SDK to enable device signing
- Document simulator and physical device build workflows in README
SettingsViewModel.logout() and deleteAccount() each called
authRepository.logout() directly before flipping the state flag that
drives the screen's LaunchedEffect -> onLogout callback ->
NavHostViewModel.logout(), which invokes authRepository.logout() again.
Each redundant call fires a POST /api/auth/logout that 401s (tokens
already gone) and re-runs session cleanup; with the planned full-wipe
change that duplication becomes actively harmful.
Collapse SettingsViewModel.logout() to a single state update, and drop
the direct authRepository.logout() call from the deleteAccount success
branch. NavHostViewModel.logout() remains the single source of truth
for the cleanup side of the flow.
Five Android producers wrote temp files directly into cacheDir root,
bypassing CommonSessionCacheCleaner which only sweeps entries listed in
CACHE_SUBDIRECTORIES. Moved each producer to its own named subdir and
registered the new names so logout actually clears them.
reset() now clears pagination cursor, hasMore, isLoadingMore and
isRefreshing state so the next user session does not inherit stuck
spinners or stale cursors from the previous session. Also cancel and
restart the search debounce collector so an in-flight debounced
emission from the previous session cannot land after reset() and
clobber the cleared grouped conversations list.
The Android camera-capture flow in ChatInput writes temp photos to
cacheDir/camera_photos/ but this subdir was missing from
CACHE_SUBDIRECTORIES, so captured photos leaked across user sessions.
Add it to the sweep list so CommonSessionCacheCleaner clears it on
logout.