Commit Graph

12869 Commits

Author SHA1 Message Date
Claude 00194d44a4 ci(desktop): pre-fetch VLC + UPX with curl --retry to dodge videolan flakes
Replace the nick-fields/retry wrapper around the Gradle invocation with a
curl-based pre-fetch step. The vlc-setup plugin writes its downloads to
${gradleUserHomeDir}/vlcSetup/ with overwrite(false), so dropping the
archives there ahead of time turns vlcDownload / upxDownload into no-ops.

curl --retry-all-errors --retry-max-time 900 tolerates a sustained
get.videolan.org outage far better than the plugin's de.undercouch
Download (retries(4) + 5min readTimeout, which still hit
SocketTimeoutException on Windows).

The actions/cache step at ~/.gradle/vlcSetup still captures the result
for subsequent runs; the prefetch only does real work on cache miss.
The in-build retries(4) in desktopApp/build.gradle.kts stays as a third
line of defense.

URLs and on-disk paths are read straight from the plugin source
(ir.mahozad.vlc-setup 0.1.0); macOS skips UPX because UPX cannot compress
.dylib files.
2026-05-06 19:16:48 +00:00
Claude cb428be28e ci(desktop): wrap desktop build in retry to survive vlcDownload flakes
The Windows leg of the Test/Build workflow has been intermittently failing
with SocketTimeoutException inside :desktopApp:vlcDownload, even after the
in-build retry budget (retries(4) + 5min readTimeout in
desktopApp/build.gradle.kts) and the actions/cache of ~/.gradle/vlcSetup.
When get.videolan.org is unreachable for long enough to exhaust the inner
retries, the whole job dies on a cache miss.

Wrap the Test+Build step in nick-fields/retry@v4 (max_attempts: 2,
timeout_minutes: 45) so one outer retry can ride out a sustained
videolan.org outage. This mirrors the proven pattern already used in
create-release.yml for the release artifact build.
2026-05-06 18:57:01 +00:00
Vitor Pamplona ddef13cabc Merge pull request #2747 from davotoula/fix-nip05-underscore
Correct '_@' display in profile header + centralise wildcard rule
2026-05-06 13:10:42 -04:00
David Kaspar 90dedcb64a Merge pull request #2745 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-06 18:35:48 +02:00
davotoula fe2cb7da66 Centralise the NIP-05 wildcard check (name != "_") on the data class itself. 2026-05-06 18:32:11 +02:00
davotoula d2e04a6726 Code review:
exhaustive when on Nip05State in Nip05OrPubkeyLine
memoize pubkey display in Nip05OrPubkeyLine
2026-05-06 18:32:11 +02:00
davotoula 54c48bc5b7 extract Nip05OrPubkeyLine into shared composable
Extract nip05 value calculation into helper
2026-05-06 18:32:11 +02:00
Claude a34a5e0af7 fix(profile): hide '_@' prefix on NIP-05 in profile header
Per NIP-05, when the local part is '_', the address should display as
just the domain. The profile header was calling Nip05Id.toValue() which
always returns 'name@domain', producing '_@houseofeff.com' on screen.
Match the pattern already used in NIP05VerificationDisplay,
AwardBadgeScreen, RelayManagementScreen, and NewCommunityScreen.
2026-05-06 18:32:11 +02:00
Vitor Pamplona ed85f3e5be Merge pull request #2746 from vitorpamplona/claude/fix-nostr-repeat-sub-test-Y5YK3
Make NostrClientRepeatSubTest more resilient to relay behavior
2026-05-06 12:00:20 -04:00
Claude 084fc7ddc2 test(quartz): make repeat-sub test tolerant of relay limit overshoot
nos.lol (strfry) sometimes returns one extra event past the requested
limit, pushing the second EOSE past the hard-coded slot 111 and failing
the run. Replace the fixed 112-message assertion with EOSE-driven
termination plus structural checks that allow the relay to return up to
limit + 1 events for each sub.
2026-05-06 15:55:55 +00:00
Vitor Pamplona 5bf542c18d Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-06 10:55:17 -04:00
Vitor Pamplona 85d27a80b0 Updates AGP 2026-05-06 10:53:14 -04:00
Crowdin Bot 9b865478de New Crowdin translations by GitHub Action 2026-05-06 14:52:41 +00:00
Vitor Pamplona 7c3fe7601c Merge pull request #2743 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-06 10:51:17 -04:00
Vitor Pamplona b9617590d5 Merge pull request #2744 from vitorpamplona/claude/update-libschnoor-1.0.5-lpNRu
Upgrade schnorr256k1Kmp dependency to 1.0.5
2026-05-06 10:50:56 -04:00
Claude 614e403037 chore(deps): bump schnorr256k1-kmp to 1.0.5 2026-05-06 14:31:28 +00:00
Crowdin Bot 010fc03292 New Crowdin translations by GitHub Action 2026-05-06 13:32:35 +00:00
David Kaspar d1c8c03907 Merge pull request #2740 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-06 15:30:48 +02:00
Vitor Pamplona 0874706b31 Merge pull request #2742 from vitorpamplona/claude/fix-eventstore-expiration-test-AbtSm
test(quartz): widen NIP-40 expiration buffer to outlive insert race
2026-05-05 20:54:39 -04:00
Claude d6be9e4574 test(quartz): drive nip40 expiration test through projection directly
Replace the wall-clock dance with a direct EventStoreProjection
construction wired to a controllable nowProvider. Both events are
inserted with expirations safely in the future so the SQL
reject_expired_events trigger never fires; the test then bumps the
fake clock and replays the StoreChange.DeleteExpired the observable
would emit after a sweep. Tests the projection's reaction to the
sweep signal — which is what this case was always about — without a
6s real-time delay or a flake window. Full SQL sweep behavior
remains covered by ExpirationTest.testDeletingExpiredEvents.
2026-05-06 00:50:30 +00:00
Claude 5f4d8ba747 test(quartz): widen NIP-40 expiration buffer to outlive insert race
The reject_expired_events trigger checks NEW.expiration <= unixepoch()
at insert time, so signing/insert latency on a slow runner could push
unixepoch() to time+1 before the row was committed, raising
SQLiteException at runBlocking entry. Bumping the short event to
time+5 with a matching delay(6000) mirrors
ExpirationTest.testDeletingExpiredEvents and gives the trigger room
to breathe without changing the behavior under test.
2026-05-06 00:42:14 +00:00
Vitor Pamplona 44f3776e86 Merge pull request #2741 from vitorpamplona/claude/investigate-build-size-458NE
Optimize CI builds and reduce APK size with Gradle caching and font subsetting
2026-05-05 20:39:07 -04:00
Claude 19588be84b build(perf): trim build outputs for hooks, CI, and the shipped font
Headline numbers from a fresh `./gradlew assemble`:
- :commons material_symbols_outlined.ttf 11M -> 409K (subset to the 210
  codepoints actually referenced from MaterialSymbols.kt)
- :amethyst per-ABI APKs no longer built in CI (-PdisableAbiSplits=true);
  ~600M of stripped_native_libs intermediates skipped per run

Changes:
- .git-hooks/pre-push runs only :amethyst:testPlayDebugUnitTest plus jvmTest
  on the KMP modules instead of `./gradlew test`, which was compiling all six
  amethyst variants (play/fdroid x debug/release/benchmark)
- amethyst/build.gradle adds three opt-in fast-build flags:
    -PdisableAbiSplits=true       skip per-ABI APK splits
    -PdisableUniversalApk=true    skip the universal APK output
    -Pamethyst.skipMapping=true   disable R8 (release+benchmark)
  Defaults are unchanged; release pipelines must not set skipMapping.
- .github/workflows/build.yml uses gradle/actions/setup-gradle@v4 with
  cache-read-only on PRs / cache-write on main, drops --no-daemon, collapses
  the Android job to a single Gradle invocation (lint + focused debug unit
  tests + assembleBenchmark with -PdisableAbiSplits=true), and globs both
  APK naming patterns for the upload step.
- tools/material-symbols-subset/{subset.sh,README.md} regenerates the font
  from upstream + MaterialSymbols.kt; run after adding/removing icons.

Verified: ./gradlew :amethyst:help with all three new -P flags parses cleanly,
and ./gradlew :commons:jvmJar --rerun-tasks succeeds with the subsetted font
(commons-jvm jar shrinks from ~13M to 2.9M).

https://claude.ai/code/session_01YSmkagXXN5AwGcY4upiUh1
2026-05-05 22:49:43 +00:00
Crowdin Bot f6b081fec3 New Crowdin translations by GitHub Action 2026-05-05 19:11:39 +00:00
Vitor Pamplona 46a07cfe43 Merge pull request #2739 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-05 15:10:04 -04:00
Vitor Pamplona 84bd3560e5 Merge pull request #2736 from vitorpamplona/claude/disable-vlc-ci-checks-4BD0q
Cache vlc-setup downloads in desktop CI
2026-05-05 15:09:55 -04:00
Crowdin Bot 0887c65ce9 New Crowdin translations by GitHub Action 2026-05-05 18:18:54 +00:00
Vitor Pamplona 3915d98774 Merge pull request #2738 from vitorpamplona/claude/fix-session-swap-timeout-VB9NT
Fix race condition in ReconnectingNestsListenerTest frame delivery
2026-05-05 14:17:08 -04:00
Claude bd681a0a5f test(nests): close FRAME1 delivery race in subscribeSpeaker_survives_session_swap
The test emitted FRAME1 into first.frames and immediately called
first.fail(...), trusting that FRAME1 would propagate through the pump
to the wrapper's frames before the session swap. emit() is
non-suspending — it only enqueues FRAME1 into the pump's slot. On
slower hosts (observed on macOS CI) the orchestrator's reconnect path
can flip activeListener to the second session before the pump's
collect lambda runs, at which point collectLatest cancels
pump-iteration-1 mid-resume and FRAME1 is dropped.  The consumer's
take(2) then only ever sees FRAME2 and the async's withTimeout fires
after 5 s.

Add a consumerProgress StateFlow that the consumer's collector bumps
on each frame, and wait for it to reach 1 before failing the listener.
Same shape as the existing consumerSubscribed gate that closed the
"emit before consumer subscribes" race.
2026-05-05 18:06:56 +00:00
Claude 0c8e1ef58e chore(desktop): drop skipVlcSetup escape hatch
The -PskipVlcSetup / AMETHYST_SKIP_VLC opt-out is no longer used: CI
relies on the actions/cache step in build.yml to avoid hitting
get.videolan.org, and the in-build retry budget covers cache misses.
Remove the dead opt-out from desktopApp/build.gradle.kts and the env
export from the session-start hook.
2026-05-05 16:35:28 +00:00
Vitor Pamplona c0262abe59 Merge pull request #2735 from vitorpamplona/claude/fix-keyboard-padding-messages-nIBGm
Add IME padding to MessagesTwoPane Scaffold
2026-05-05 12:33:59 -04:00
Claude 697a749243 ci(desktop): cache vlc-setup downloads, drop skipVlcSetup bypass
Replace the -PskipVlcSetup=true CI bypass with a per-OS GitHub Actions
cache of ~/.gradle/vlcSetup. Cache key is hashFiles('desktopApp/build.gradle.kts')
so a VLC version bump invalidates cleanly; restore-keys lets unrelated
edits to that file still warm-start.

Cache hit: vlcDownload / upxDownload are up-to-date and we never touch
get.videolan.org. Cache miss (first run after version bump, or a new
runner): we fall back to the network, where the existing 5-attempt
retry budget in build.gradle.kts already handles flakes.

The skipVlcSetup property + AMETHYST_SKIP_VLC env hooks stay in
desktopApp/build.gradle.kts as a local-dev / CCW escape hatch — the
CCW egress can't reach get.videolan.org at all and has no actions/cache.
2026-05-05 16:28:41 +00:00
Claude f922cfd5bc fix(desktop): also disable vlc/upx Extract tasks under skipVlcSetup
Disabling only vlcDownload/upxDownload/vlcSetup left the chained
*Extract tasks in the graph with @InputFile properties pointing at
archives that were never downloaded — Gradle 9 then fails the build
during input validation:

  property 'upxArchiveFile' specifies file
  '/root/.gradle/vlcSetup/upx-4.2.4.tar.xz' which doesn't exist.

Disable the full vlc-setup plugin task set
(vlcDownload, vlcExtract, vlcFilterPlugins, vlcCompressPlugins,
upxDownload, upxExtract, vlcSetup) so packaging skips VLC bundling
cleanly.

Verified locally: :desktopApp:packageDeb now proceeds past those tasks
straight to jpackage with -PskipVlcSetup=true.
2026-05-05 16:11:55 +00:00
Vitor Pamplona 3364890a01 Merge pull request #2737 from vitorpamplona/claude/fix-group-message-notifications-UNKZq
Add notifications for Marmot group messages (kind:445)
2026-05-05 12:10:54 -04:00
Claude 30d8d1bb6f refactor(notifications): type notifyGroupMessage as ChatEvent
The previous signature took Event and runtime-checked kind == 9 inside
the notifier. That left the contract implicit: any caller could pass a
reaction or control message and the call would silently no-op.

Type the parameter as ChatEvent so the kind:9 restriction is structural,
and move the narrowing (`is ChatEvent`) to the GroupEventHandler call
site where the inner event is parsed. Reactions, deletions, and other
inner kinds now can't reach the notifier in the first place.

Drops the runtime kind check and the now-stale comment about reactions.
2026-05-05 15:57:36 +00:00
Claude ab68f827ce ci(desktop): add skipVlcSetup opt-out to bypass flaky VLC downloads
get.videolan.org regularly times out from GitHub-hosted runners and
Claude Code web sandboxes, taking the desktop CI build down with it
even though the failure is unrelated to the change under review.

Wire a -PskipVlcSetup=true (env: AMETHYST_SKIP_VLC=true) opt-out in
desktopApp/build.gradle.kts that disables the vlcDownload, upxDownload,
and vlcSetup tasks. Pass it from build.yml so PR CI no longer gates on
VLC reachability, and export the env from the CCW session-start hook.

Release packaging (create-release.yml) intentionally does not set the
flag, so shipped artifacts still bundle VLC.
2026-05-05 15:41:01 +00:00
Claude cfde4a5bf7 fix(notifications): popup for Marmot group messages (kind:445)
Welcomes (kind:444) already had a direct-dispatch path because they have
no `p` tag and the cache-observer route can't match them to an account.
Group messages (kind:445) have the same problem — recipients are routed
by the `h` tag carrying the nostr_group_id — but were silently missed,
so the user only saw a popup when first added to a group, never for any
chat that followed.

Mirror the notifyWelcome path: after MarmotInboundProcessor decrypts
and verifies an ApplicationMessage and we've persisted the inner event
for the first time, fire notifyGroupMessage on NotificationDispatcher.
The notifier filters to ChatEvent (kind:9) so reactions, deletions and
control messages stay silent (matching how NIP-17 only notifies on
kind:14), applies the same 15-min freshness and self-author gates as
the other DM paths, and uses the marmot:<groupHex>?account=<npub>
deep-link scheme so taps land in the right chatroom.

Only fires on first-time decryption (isNew) so a relay re-broadcast or
on-disk persist replay can't double-notify.
2026-05-05 15:36:35 +00:00
Claude d7c3c22522 fix(messages): apply imePadding to two-pane Scaffold so edit field clears keyboard
The single-pane chat path wraps content in DisappearingScaffold, which applies
Modifier.imePadding() at the root surface. The dual-pane MessagesTwoPane uses a
plain Scaffold with no IME inset handling, so when the keyboard opens the
PrivateMessageEditFieldRow stayed hidden behind it.

https://claude.ai/code/session_012sPZ9KbbkTzdPL2KTn29ak
2026-05-05 15:23:01 +00:00
Vitor Pamplona f2c8e154cf Merge pull request #2734 from vitorpamplona/claude/fix-moq-protocol-exception-q1gTL
Fix header protection sampling for short QUIC payloads
2026-05-05 10:56:49 -04:00
Claude 410123e281 fix(nests): reset CreateNest sheet state after successful publish
CreateNestViewModel is keyed by user pubkey via `viewModel(key = …)`,
so the same instance survives across sheet open/close cycles. After a
successful publish the form was left untouched: room name, summary,
image URL stayed populated and — most visibly — `isPublishing` was
never cleared, so on the second open the Submit button rendered as a
spinning progress indicator forever.

Reset the form to fresh defaults (re-seeded from the user's saved
kind-10112 list) at the end of `publishAndBuildLaunchInfo()`, right
before returning the launch info. The sheet is about to dismiss
anyway, so the user never sees the in-place reset; the next open
behaves like a first open.
2026-05-05 14:17:05 +00:00
Claude edd6eb5c10 fix(quic): pad short plaintext payloads for HP sample (RFC 9001 §5.4.2)
ShortHeaderPacket.build / LongHeaderPacket.build crashed with
`IllegalArgumentException: packet too short for HP sample` whenever the
plaintext payload was small enough that pnLen + payload < 4 — most
visibly on the 1-RTT path when buildApplicationPacket emitted a single
1-byte PING (PTO probe with no ACKs queued and no streams to drain) and
the packet number still fit in 1 byte. The crash tore down the writer
loop, which surfaced upstream as moq-lite "subscribe stream FIN before
reply" because in-flight bidi streams got FIN'd by the peer.

RFC 9001 §5.4.2 mandates the sender pad the plaintext so the encrypted
output (plaintext + 16-byte AEAD tag) has at least 20 bytes after the
packet-number offset for the 16-byte HP sample. The fix pads the
plaintext with trailing 0x00 bytes — those decode as PADDING frames per
RFC 9000 §19.1 and decodeFrames already absorbs them. For long-header
packets the padded size feeds back into the Length varint.
2026-05-05 13:37:41 +00:00
Vitor Pamplona 8f2bbabb32 Merge pull request #2733 from vitorpamplona/claude/fix-nests-audio-dropout-EGlUZ
Eliminate audio gaps during JWT refresh and improve network resilience
2026-05-05 09:16:49 -04:00
Claude 003cf42564 fix(nests): self-audit pass — two real bugs + robustness + tests
Audit of the four prior commits found two genuine regressions and two
robustness gaps in the new code paths.

Bug A: NestForegroundService.networkCallback dropped the publish for
the most important scenario it was added to handle. The earlier guard
`if (previous != null && previous != network)` suppressed both the
registration-time first onAvailable AND the legitimate WiFi-loss-
then-cellular-available path. On a WiFi → cellular handover the
sequence is `onLost(wifi)` (clears currentDefaultNetwork to null)
followed by `onAvailable(cellular)` — which then looks identical to
the registration callback to the guard, so no publish fires and the
QUIC session sits on the dead socket until PTO. Replaced the implicit
"previous == null" suppression with an explicit `seenInitialNetwork`
flag that's set true on the first onAvailable and never cleared, so
post-onLost reconnects publish correctly.

Bug B: requestAudioFocus result handling was too permissive. The
shape `if (result == AUDIOFOCUS_REQUEST_FAILED) TransientLoss else
Granted` falls through to Granted on the runCatching exception path
(`result == null`) and on AUDIOFOCUS_REQUEST_DELAYED (= 2) — meaning
audio plays over an active call when the OS hasn't actually released
focus. Switched to a strict `if (result == AUDIOFOCUS_REQUEST_GRANTED)`
check; everything else (FAILED, DELAYED, exception) starts the VM
muted.

Robustness: NestViewModel.openSubscription's onError callback used to
swallow every AudioException, which fit the per-packet decoder-error
case but turned PlaybackFailed and DeviceUnavailable from a deferred
beginPlayback into a permanent "Connecting…" spinner on the speaker
tile. Now we discriminate by AudioException.Kind: decoder/encoder
errors stay swallowed (Opus PLC papers them over), but PlaybackFailed
and DeviceUnavailable roll the slot back so a future reconcile can
retry.

Pre-roll ordering swap + tests: NestPlayer.play used to call
beginPlayback BEFORE flushing the pre-roll buffer, leaving a
microsecond window where the AudioTrack hardware was playing against
an empty buffer. AudioTrack MODE_STREAM explicitly supports write()
before play(), so flush-then-beginPlayback is the textbook pattern
and what the fix now does. Three regression tests cover:
  - pre-roll defers beginPlayback until threshold is met (and the
    flush-then-begin ordering is observable)
  - partial pre-roll flushes when the upstream flow ends early
  - empty flow doesn't begin playback at all
The FakeAudioPlayer grows beginPlaybackCount + queuedAtBeginPlayback
fields so the tests can assert ordering directly.
2026-05-05 13:04:18 +00:00
Claude 6237c02c6f fix(nests): platform-side audio robustness — focus, AEC, route obs, network handover
Four follow-up fixes from the post-audit review (#4 / #5 / #6 / #7).

#6 AcousticEchoCanceler / NoiseSuppressor / AGC on the AudioRecord
   session. The VOICE_COMMUNICATION input source engages the platform
   echo canceller automatically on most modern Android devices, but a
   small set of older / OEM-customised devices only attach AEC under
   MODE_IN_COMMUNICATION — which an audio-room app deliberately
   avoids. Attaching the standalone audiofx effects to the
   AudioRecord's session id covers those devices without rerouting
   through the call audio path. All three are best-effort and a no-op
   on devices where the source already engages them.

#4 Real audio focus handling. The previous OnAudioFocusChangeListener
   was a no-op based on the assumption that the OS would auto-duck
   us; it doesn't (CONTENT_TYPE_SPEECH streams aren't auto-ducked).
   Inbound phone calls were mixing on top of room audio.
   - New `NestAudioFocusBus` (commons) — process-wide enum signal,
     decoupled from android.media so commons stays platform-free.
   - NestForegroundService translates AUDIOFOCUS_GAIN / LOSS_TRANSIENT*
     / LOSS into the bus enum.
   - NestViewModel observes the bus and silences both the listener
     playback (effective listen-mute = user OR focus) and the
     broadcast mic (effective mic-mute = user OR focus). User-visible
     mute states stay the user's choice so a focus regain restores
     them automatically.
   - The pipeline keeps running while focus is lost (decoder, capture,
     network) so unmute is sample-accurate when the call ends.

#5 AudioDeviceCallback observability. Registers a callback in
   NestForegroundService that logs Bluetooth / wired / USB headset
   attach + detach with device type + name. Doesn't drive playback
   decisions — Android's auto-routing handles route swaps — but
   makes "audio cut out when I plugged in headphones" reports
   correlatable with a concrete event for the first time.

#7 Network-change → fast reconnect. Without this, a Wi-Fi → cellular
   handover left the QUIC connection sitting on a now-dead socket
   until its PTO fired (~30 s) before the wrapper noticed. Now:
   - New `NestNetworkChangeBus` (commons) — collapses bursts of
     onLost/onAvailable into a single recycle event.
   - NestsListener + NestsSpeaker grow `recycleSession()` (default
     no-op); the reconnecting wrappers override to close the inner
     session so their orchestrator opens a fresh one.
   - NestForegroundService registers a default-network callback;
     suppresses the first onAvailable (registration callback)
     and only publishes on actual default-network changes.
   - NestViewModel observes the bus and calls recycleSession on
     both wrappers. The SubscribeHandle re-issuance pump (listener)
     and the hot-swap publisher pump (speaker) cut existing
     subscriptions / broadcasts onto the new session as soon as
     it lands — same paths the JWT-refresh recycle uses.
   - Manifest gains ACCESS_NETWORK_STATE for
     registerDefaultNetworkCallback.
2026-05-05 12:42:47 +00:00
Vitor Pamplona 4377a50295 Merge pull request #2732 from vitorpamplona/claude/modernize-settings-ui-QMMGn
Refactor settings screen UI with card-based layout and improved styling
2026-05-05 08:32:48 -04:00
Claude e4e55d1df6 fix(nests): three more dropout sources from the post-audit review
1. Split AudioPlayer.start() into allocate + beginPlayback to restore
   the synchronous DeviceUnavailable error path that the previous
   pre-roll fix collapsed.
   - AudioPlayer gains `beginPlayback()` (default no-op) so test
     fakes / desktop sinks aren't forced to grow a method they
     don't need.
   - AudioTrackPlayer.start() now allocates the AudioTrack + audio-
     priority writer thread but does NOT call AudioTrack.play();
     beginPlayback() flips the device into the playing state.
     AudioTrack in MODE_STREAM explicitly supports write() before
     play() per the platform docs, which is exactly the contract
     pre-roll wants.
   - NestPlayer.play() now calls player.start() synchronously
     (caller catches DeviceUnavailable + rolls back the slot like
     before) and defers beginPlayback() until pre-roll fills.

2. MediaCodecOpusDecoder: drain output + retry input dequeue before
   dropping a frame. The prior 10 ms input-buffer dequeue followed
   by an unconditional `return ShortArray(0)` turned every transient
   stall (thermal throttling, GC pause, output not yet pulled) into
   a 20 ms audio gap. Now we drain whatever output is ready —
   freeing input slots — then retry input dequeue with a 5 ms
   timeout. Only after both misses do we drop the packet, and even
   then we return any output samples that the drain produced so
   the player doesn't underrun on the back of one tight cycle. The
   decoder's drain logic is extracted into a `drainAvailableOutput`
   helper so both the pressure-relief path and the post-queue main
   drain share it.

3. ReconnectingNestsListener: exponential backoff for the opener-
   throws retry path. Replaces the flat 1 000 ms `SUBSCRIBE_RETRY_BACKOFF_MS`
   with 250 → 500 → 1 000 ms, reset on first successful subscribe.
   The 250 ms floor is well under moq-rs's typical announce-
   propagation latency (< 200 ms), so a subscribe-before-announce
   miss usually recovers fast enough that the wrapper SharedFlow's
   ~1.3 s buffer hides the gap entirely.
2026-05-05 12:17:59 +00:00
Claude 076b301d84 fix(nests): close 4 audio-dropout sources across listener + speaker
1. Listener-side pre-roll + bigger playback buffer + audio-priority thread.
   - NestPlayer buffers 5 decoded frames (~100 ms) before starting
     the AudioPlayer, masking the first-frame underrun that fires
     whenever Compose / GC briefly stalls Main.
   - AudioTrackPlayer sizes the AudioTrack at max(minBuffer*16, 250 ms)
     instead of minBuffer*4 (~80 ms) and writes via a per-instance
     audio-priority single-thread executor (Process.THREAD_PRIORITY_AUDIO)
     instead of Dispatchers.IO, so WRITE_BLOCKING never contends with
     unrelated IO work.

2. MoqLiteSession.subscribe: hoist response typeCode out of the
   collect lambda. readVarint advances pos permanently while
   readSizePrefixed only rolls back its own length-varint, so a
   chunk-split between type and body would re-read the body's size
   prefix as the type code on the next chunk and misframe the
   response. Mirrors the same fix already in handleInboundBidi.

3. MoqLiteSession.subscribe: register the subscription in the map
   BEFORE writing the SUBSCRIBE bytes on the wire. The relay can
   open the first group's uni stream before our continuation
   re-enters [state] to register; if so, drainOneGroup looked the
   id up against an empty map and silently dropped the frame
   (~1 frame / 20 ms gap on first attach). Wrap the post-register
   writes so a transport-failure unwinds the orphan registration.

4. Hot-swap moq-lite publisher across JWT-refresh boundaries.
   - NestMoqLiteBroadcaster: publisher is now @Volatile + supports
     swapPublisher(); capture loop snapshots the reference per frame
     and resets framesInCurrentGroup on swap.
   - MoqLiteNestsSpeaker implements a new internal
     HotSwappablePublisherSource interface that exposes
     openPublisherForHotSwap(track) without spinning up a broadcaster.
   - ReissuingBroadcastHandle keeps a single long-lived broadcaster
     across session recycles when the speaker supports hot swap;
     legacy IETF / fake speakers fall back to close-then-restart.
   - connectReconnectingNestsSpeaker.orchestrator hoists the old-
     session close onto a sibling launch so the wrapper can swap
     the publisher into the broadcaster on the new session before
     the old session's WebTransport drops. Eliminates the previously-
     accepted 50–150 ms audible silence at every JWT refresh.
2026-05-05 03:06:40 +00:00
Claude cc35a9b29a feat(amethyst): modernize Settings screen UI
Group rows into Material 3 cards by section, add tonal icon
containers and trailing chevrons, upgrade section header
typography, and visually mark the danger zone with error colors.
2026-05-05 02:28:26 +00:00
Vitor Pamplona 1acd54eada Merge pull request #2731 from vitorpamplona/claude/fix-nest-audio-display-3chAG
debug(nests): wire NestRx/NestTx logs across listener and speaker paths
2026-05-04 22:07:56 -04:00
Claude fba0a5c952 feat(quic): bestEffort streams + park CC plan indefinitely
After drafting the congestion-control plan we concluded the audio-rooms
workload doesn't actually need CC — speakers push ~8 KB/sec, which
never fills any modern link's capacity. The one real concern that
surfaced — STREAM retransmit wasting bandwidth on stale Opus frames
on lossy uplinks — is much cheaper to fix directly than to bound via
a 14-test CC subsystem.

SendBuffer gains a `bestEffort: Boolean = false` constructor flag.
When true, markLost drops the lost ranges instead of moving them to
the retransmit queue and lets the underlying byte storage compact as
if the bytes had been ACK'd. The FIN flag (if covered) also stays
sent — best-effort skips FIN re-emission too. The peer may end up
with a truncated stream; moq-lite's per-stream timeouts handle that.

Plumbed through QuicStream → QuicConnection.openUniStream(bestEffort)
→ QuicWebTransportSessionState.openUniStream(bestEffort) →
WebTransportSession.openUniStream(bestEffort). Default is false
everywhere, so reliable streams (HTTP/3 control, moq-lite SUBSCRIBE
bidi, etc.) keep RFC 9000 §3.5 semantics.

MoqLiteSession.openGroupStream now passes `bestEffort = true` —
group streams carry a single Opus packet, are real-time, and don't
benefit from retransmit.

Internal cleanup: `removeOverlap`'s `ackedNotLost: Boolean` parameter
became `OverlapAction { ACK, RETRANSMIT, DROP }` so the third best-
effort disposition has a name. Same code paths, same tests, just
clearer at the call site.

CC plan (quic/plans/2026-05-05-congestion-control.md) is updated to
"parked indefinitely" with a note that this commit is the lighter-
weight alternative that addresses the only practical concern. The
plan is preserved as a reference if a future workload justifies CC.

New tests: SendBufferBestEffortTest (6 cases — reliable baseline,
best-effort drops, FIN drop in best-effort mode, partial overlap,
idempotent stale loss, ACK path still works).

https://claude.ai/code/session_01PYYez8a6sjiakyjAxsfCEQ
2026-05-05 02:06:08 +00:00