Commit Graph

12692 Commits

Author SHA1 Message Date
davotoula 16c30da4d6 chore: gitignore .claude/scheduled_tasks.lock 2026-05-02 15:37:25 +02:00
David Kaspar 49ce010a96 Merge pull request #2702 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-02 14:44:23 +02:00
Crowdin Bot d622f2464e New Crowdin translations by GitHub Action 2026-05-02 11:58:02 +00:00
Vitor Pamplona efe8cc7545 Merge pull request #2697 from davotoula/fix/pow-string-index-out-of-bounds
Off-by-one error in PoW evaluator
2026-05-02 07:56:30 -04:00
David Kaspar cd8bf9fdd7 Merge pull request #2699 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-02 09:29:19 +02:00
davotoula ca640df2c2 cover PoWRankEvaluator partial-rank branches explicitly
bound PoWRankEvaluator hex loop to id.indices
2026-05-02 09:27:39 +02:00
Crowdin Bot d4808d585a New Crowdin translations by GitHub Action 2026-05-02 07:26:22 +00:00
David Kaspar 412ba1afe2 Merge pull request #2698 from davotoula/investigate/ci-context-cast-to-activity
fix: use LocalActivity to resolve NestActivity (lint ContextCastToActivity)
2026-05-02 09:24:51 +02:00
davotoula 59275f6638 fix: use LocalActivity to resolve NestActivity (lint ContextCastToActivity)
Casting LocalContext.current to NestActivity tripped the Jetpack
Activity Compose lint rule ContextCastToActivity, failing
:amethyst:lintPlayBenchmark in CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 08:40:34 +02:00
Vitor Pamplona 656614c423 Merge pull request #2696 from vitorpamplona/claude/moq-speaker-animation-okK2f
feat: animate local speaker ring from MoQ frame transmission
2026-05-01 22:03:18 -04:00
Claude 5d955eff99 feat: animate local speaker ring from MoQ frame transmission
Wire the local user's avatar into the existing speaking-ring animation so
the host/speaker sees the same green ring + amplitude glow on their own
cell that remote speakers already get.

Ground truth is the broadcaster's `publisher.send(opus)` success: the new
`onLevel` callback fires only after a frame actually leaves on the wire,
computed from the raw PCM peak (shared `peakAmplitude` util used by both
the player decode loop and the broadcaster capture loop). This means the
animation reflects what the relay sees, not any UI-side mute / role
state that could be stale or buggy — if frames are flowing, the ring
plays.

Plumbing: `NestsSpeaker.startBroadcasting` gains an optional `onLevel`
that the moq-lite + IETF broadcasters both honour, the reconnecting
wrapper replays it on every session reissue (alongside the existing
mute-intent replay), and `NestViewModel.startBroadcast` hands it a
lambda that calls the existing `onSpeakerActivity` / `onAudioLevel`
plumbing keyed on the local pubkey. The 250 ms speaking-timeout fades
the ring naturally when the user mutes or stops broadcasting.
2026-05-02 01:17:40 +00:00
Vitor Pamplona 0c4def6491 Merge pull request #2695 from vitorpamplona/claude/fix-duplicate-mic-buttons-AKgfT
fix: drop redundant StopBroadcastButton from nest action bar
2026-05-01 18:20:19 -04:00
Claude 770af8faf9 fix: tear down nest session on PIP close + Just Leave
Three more leave paths now fully release the speaker session and
listener (and with them AudioRecord + the system mic indicator)
before the Activity destruction queue eats viewModelScope:

  - PIP overlay's Leave action — pipReceiver invokes a cleanup
    callback set by NestActivityBody, then finish().
  - PIP swipe-dismiss — NestActivity.onStop() invokes the same
    callback when in PIP and isFinishing. Non-PIP onStop stays a
    no-op so backgrounding doesn't tear down.
  - Host's Just leave — calls viewModel.leave() before onLeave().
    Unlike Close the Room, this still leaves the kind-30312
    meeting space open for other users.

The cleanup callback is registered via DisposableEffect in
NestActivityBody so it's auto-cleared when the composable leaves.
2026-05-01 22:05:23 +00:00
Claude 36152243ab fix: tear down nest session on host Close the Room
The host-leave confirmation dialog's Close the Room button only called
finish(), relying on VM.onCleared() to release the AudioRecord. That
runs late in the destroy lifecycle, so the system mic-in-use indicator
stayed lit while the activity was queued for destruction.

Adds NestViewModel.leave(), which mirrors onCleared() (sets closed,
runs both teardowns with finalCleanup=true so closes route through
cleanupScope/GlobalScope and survive Activity destruction). Wires it
into the Close the Room callback only — the Just Leave and non-host
Leave paths are unchanged per request.
2026-05-01 21:47:33 +00:00
Vitor Pamplona 27250fa6dd Merge pull request #2694 from vitorpamplona/claude/redesign-nests-cards-AHCcK
Add section headers and compact view for Nests feed
2026-05-01 17:41:40 -04:00
Claude 6610683a20 fix: hide StageControlsBar on local intent flag for responsive tap
After tapping Leave the Stage, the bar lingered because it was gated
solely on isOnStageMe — a value derived from the presence aggregator.
That aggregator only updates after a presence event signs, broadcasts
to relays, and loops back through LocalCache. With the recent
broadcast / mute traffic contending on the signer plus the 500 ms
mute debounce in NestPresencePublisher, the round-trip ran long
enough that the user perceived Leave the Stage as not firing.

AND the visibility gate with ui.onStageNow (the local intent flag,
which flips synchronously on setOnStage(false)) so the tap hides the
bar on the next frame. Host demotion still hides it via the existing
isOnStageMe path.
2026-05-01 21:30:05 +00:00
Claude 7328874c09 fix(nests): wire kebab to NoteDropDownMenu instead of QuickAction
The kebab on a NoteCompose card opens the comprehensive
NoteDropDownMenu (Follow/Unfollow, Copy text/pubkey/note id, Share,
Edit, Broadcast, Timestamp, Pin, Bookmark, Delete/Report) — not the
smaller LongPressToQuickAction popup. The previous nests commit
wired the kebab to the wrong menu.

Replace the manual ClickableBox + VerticalDotsIcon + showQuickAction
plumbing with the existing MoreOptionsButton composable, which
already wraps the icon and triggers NoteDropDownMenu for both card
variants. Long-press still opens NoteQuickActionMenu via the outer
LongPressToQuickAction wrapper, matching NoteCompose's two-tier
gesture model exactly.

Drops the onMore parameter threading on ObserveAndRenderSpace,
RenderLiveSpacesThumb, and SpaceHostAndReactions — MoreOptionsButton
is self-contained, so the trailing optional parameter is no longer
needed.
2026-05-01 21:27:27 +00:00
Claude 00de615733 feat(nests): add visible kebab menu to room cards
Long-press on the cards already opened the standard
NoteQuickActionMenu, but the affordance was hidden — especially on
the compact ENDED card which has no visible interactive elements.

Add an explicit MoreVert (VerticalDotsIcon) tap target that opens
the same menu:

- ENDED compact card: 40dp icon on the trailing edge of the row.
- LIVE/SCHEDULED full card: 35dp icon in the existing
  SpaceHostAndReactions row, after Like and Zap.

Both wire through the showQuickAction lambda from
LongPressToQuickAction, so the icon and the long-press gesture
trigger the exact same menu (share, copy, broadcast, report,
block, delete) — no second popup-state plumbing.

ObserveAndRenderSpace, RenderLiveSpacesThumb, and
SpaceHostAndReactions gain an optional `onMore: (() -> Unit)?`
parameter so other callers (e.g. existing live-stream surfaces
that reuse SpaceHostAndReactions) keep their current rendering
unchanged when they don't pass one.
2026-05-01 21:20:44 +00:00
Claude c981058a9f fix: render StageControlsBar inside the Stage card surface
The previous commit placed the controls strip as a sibling row right
after StageGrid. Visually it read as a separate band, not as part of
the stage card.

Adds a bottomBar slot to StageGrid that renders inside the same
Surface, below the grid, sharing the card's tonal background and
horizontal padding. StageControlsBar drops its own horizontal padding
since the card supplies it, and keeps a top inset so it has breathing
room from the avatars.
2026-05-01 21:16:38 +00:00
Claude 1ce0e3c179 feat(nests): bucket feed into Live / Scheduled / Recently ended sections
Replace the flat NestsScreen list with three explicit sections so the
audio-room surface separates actionable rooms from historic ones:

- LIVE: ordered by follows-participating DESC, total-participants DESC,
  createdAt DESC. PRIVATE rooms continue to live in the LIVE bucket
  since they're audible once a join token is granted.
- SCHEDULED: ordered by `starts` ASC (soonest first). PLANNED rooms
  now have their own bucket instead of collapsing into ENDED.
- ENDED: ordered by createdAt DESC (proxy for "ended at"), capped to
  the last 7 d, and rendered with a compact one-line card variant
  (square thumb + name + host + "Ended Xh ago"). Tapping still
  routes through the read-only lobby so recordings remain reachable.

The DAL exposes `NestsFeedFilter.bucketOf()` so the screen can walk
the pre-sorted feed once and inject sticky-header sections without
re-sorting on each recomposition.

Cards now wrap in `LongPressToQuickAction`, giving share / copy /
delete / report / block / broadcast on long press — matching the
NoteCompose interaction model and giving hosts a path to delete
(kind:5) ended rooms without entering the room first.
2026-05-01 21:09:01 +00:00
Claude 4af15a301f Merge remote-tracking branch 'origin/main' into claude/fix-duplicate-mic-buttons-AKgfT 2026-05-01 21:01:50 +00:00
Claude 9c26c51a1b fix: move on-stage controls out of action bar onto stage card
NestActionBar held the speaker controls (Talk / MicMute / Leave the
Stage) alongside the audience controls (hand-raise / react / leave
room), making the bar uncomfortably wide on phones once a user was
promoted.

Splits responsibilities:
  - NestActionBar now only handles connection state in the start
    cluster (Connect / Connecting chip / Reconnecting chip / empty)
    plus the existing end cluster.
  - StageControlsBar is a new strip rendered under the Stage card,
    gated only on isOnStage, holding the full broadcast state machine.

isOnStage is the single visibility signal — a stable derived value
from the kind-30312 role + presence onstage flag — so connection
blips, broadcast state churn, mute toggles, and permission flows
never make the new bar appear or disappear. It only flips on a real
promote/demote or the user tapping Leave the Stage.
2026-05-01 20:57:07 +00:00
Vitor Pamplona a69e4696a8 Merge pull request #2693 from vitorpamplona/claude/fix-nests-audio-issue-a8sFv
Switch audio playback to USAGE_MEDIA for hands-free rooms
2026-05-01 16:53:38 -04:00
Claude 523e8a92ec fix(nests): play audio-room output through STREAM_MUSIC, not STREAM_VOICE_CALL
`AudioTrackPlayer` was constructing its `AudioTrack` with
`USAGE_VOICE_COMMUNICATION` + `CONTENT_TYPE_SPEECH` so the room would
behave like a phone call (volume rocker controls call volume, ducks
notifications). The `AudioTrack` then renders on `STREAM_VOICE_CALL`,
which Android only services audibly while the device is in
`MODE_IN_COMMUNICATION`. Nests never drives `AudioManager.mode` (only
NIP-100's `CallAudioManager` does), so on most devices the playback
either dropped to the earpiece at near-zero volume or produced no audio
at all — making rooms appear silent on both phones.

Fix: route through `USAGE_MEDIA` + `CONTENT_TYPE_SPEECH` (=
`STREAM_MUSIC`) so audio-room playback comes out of the loudspeaker by
default and the volume rocker controls it any time. Same approach
Twitter/X Spaces and Clubhouse take for hands-free audio rooms. Echo
cancellation still works on the capture side via
`MediaRecorder.AudioSource.VOICE_COMMUNICATION` regardless of the
playback usage.

Also update `NestForegroundService.requestAudioFocus` to request focus
under the matching `USAGE_MEDIA` attributes so the focus claim and the
playback attributes line up.
2026-05-01 20:50:12 +00:00
Claude e7cb6dae5f fix: drop redundant StopBroadcastButton from nest action bar
MicMuteToggle already covers 'stop sending audio while staying on stage'
with sample-accurate resume, and LeaveStageButton covers full teardown.
StopBroadcastButton drew a Mic icon next to MicMuteToggle's Mic icon,
reading visually as two adjacent mic buttons.
2026-05-01 20:39:11 +00:00
Vitor Pamplona e6d755a7ab Merge pull request #2692 from vitorpamplona/claude/fix-encryption-deprecation-Hu4T2
Fix flaky test by waiting for mute state instead of start count
2026-05-01 16:31:20 -04:00
Claude 7c1af48ae7 fix(nestsClient): wait for mute replay in setMuted_intent_replays test
ScriptedSpeaker publishes startCount > 0 from inside startBroadcasting,
so polling startCount alone races the wrapper pump's subsequent
`if (desiredMuted) handle.setMuted(true)` step. Under load (full suite
run on CI) the assertion fired before the pump replayed mute intent,
producing setMutedCalls=0. Wait for the post-condition the assertion
checks instead.
2026-05-01 20:26:18 +00:00
Vitor Pamplona 66985020e0 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-01 16:16:58 -04:00
Vitor Pamplona 4a54ac6bee minimizes chance of failing tests 2026-05-01 16:16:14 -04:00
Vitor Pamplona 653ad9992b unecessary tag 2026-05-01 16:16:00 -04:00
Vitor Pamplona a6fef4c813 Merge pull request #2691 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-01 16:03:42 -04:00
Crowdin Bot d3057b77e1 New Crowdin translations by GitHub Action 2026-05-01 20:01:13 +00:00
Vitor Pamplona bce034185e Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2026-05-01 15:59:30 -04:00
Vitor Pamplona 7be405847f Merge pull request #2690 from vitorpamplona/claude/audit-nest-interface-WCwBR
Improve Nest room UX: loading states, error handling, and performance
2026-05-01 15:59:14 -04:00
Claude ed89e2d542 fix(nests): UI polish — Stop button, unjoinable state, hand queue, DST
Audit-driven UI fixes that don't fit the perf bucket:

* NestActionBar — wire StopBroadcastButton between MicMuteToggle and
  LeaveStageButton while broadcasting. The doc table at the top of
  the file already advertised [Mute] [Stop] [Leave the Stage] but the
  button was defined and never used; speakers had to leave the stage
  to stop sending audio.

* NestActivityContent — render an explicit loading spinner while the
  kind-30312 resolves and a clear "this room can't be opened" page
  with a Back button when the event is missing service/endpoint.
  Replaces the previous black-screen dead end.

* HandRaiseQueueSection — wrap filter+sort in remember so a heartbeat
  doesn't rebuild the queue on every recompose, swap to LazyColumn
  so a long queue doesn't render every row every frame, and show
  UsernameDisplay instead of the raw 8-char hex prefix.

* CreateNestSheet schedule picker — compute the timezone offset at
  the picked instant rather than at Instant.now(); the previous
  variant was off by one hour for rooms scheduled across a DST
  transition.

* NestViewModelFactory — pass OkHttpNestsClient's httpClient via
  named parameter so the (String) -> OkHttpClient function reference
  doesn't get bound to the Long callTimeoutMs slot. Fixes the
  pre-existing :amethyst:compilePlayDebugKotlin failure on the
  branch base.

Adds three strings (loading, unjoinable title/body, back).

https://claude.ai/code/session_01DMeCvWyBYVVVPez2hwqCs4
2026-05-01 19:25:36 +00:00
Claude fc79faa468 fix(nests): clear NestBridge on logout and account switch
NestBridge holds a process-singleton AccountViewModel reference so
NestActivity (separately tasked) can read the active account without
serialising a NostrSigner through an Intent extra. The bridge's doc
promised it'd be cleared on logout / account switch but no call site
existed — so the audio-room activity could pick up a stale
AccountViewModel from the previous session and sign with the old
key after an account swap.

Wire NestBridge.clear() into both transition points in
AccountSessionManager: switchUserSync (always) and logOff (only when
the current account is being torn down).

https://claude.ai/code/session_01DMeCvWyBYVVVPez2hwqCs4
2026-05-01 19:25:14 +00:00
Claude 714159f816 perf(nests): dedicated kinds=[10312] limit=1 feed liveness probe
Feed thumbnails were reusing NestRoomFilterAssemblerSubscription to
flip the LIVE/ENDED badge based on the freshest kind-10312 presence.
That assembler subscribes to chat + presence + reactions (kinds 1311,
10312, 7) — so a feed of N rooms paid each popular room's full
chat-history pull per outbox relay just to render a status pill.

Add NestRoomLivenessAssembler, scoped to a single kinds=[10312],
#a=[room], limit=1 filter per relay, and switch the feed thumbnail
to it. The full chat / reaction / admin command REQ stays gated
behind the join flow.

https://claude.ai/code/session_01DMeCvWyBYVVVPez2hwqCs4
2026-05-01 19:25:03 +00:00
Claude 3acd84bf76 fix(nests): reactions dedup, drift-fade animation, 10s window
Audit-driven fixes for the audio-room reactions overlay:

1. RoomReactionsAggregator now dedups by event id. The previous code
   appended every event to a flat list, but LocalCache.observeEvents
   re-emits the full matching list on every cache mutation — so an
   N-reaction window grew quadratically and the overlay rendered the
   same emoji once per replay. Keyed by event id collapses re-emits.

2. RoomPresenceAggregator gains applyOrNull that returns null when an
   incoming heartbeat is older-or-equal to the cached presence; the VM
   skips the StateFlow write in that case. In a 200-peer room, every
   replay used to copy a 200-entry map plus run an O(N) StateFlow
   equality check 200 times per emission. Now it's one-and-skip.

3. ReactionsEvictionTicker only ticks while the aggregator is non-empty.
   Once the last reaction expires the loop self-cancels until the next
   reaction lands — a quiet room costs no scheduled work.

4. REACTION_WINDOW_SEC dropped 30s -> 10s. Reactions are about what
   the speaker is saying right now; a 10s window keeps the overlay
   timely instead of bleeding into the next paragraph.

5. SpeakerReactionOverlay drives a per-chip lifecycle animation: each
   chip drifts upward ~16dp and fades over the 10s window. A fresh
   reaction (same emoji) restarts the chip's animation. The
   AnimatedVisibility outer entry/exit still smooths first-arrival and
   final disappearance.

Tests: added a re-emit dedup case to RoomReactionsStateTest plus an
end-to-end replay assertion in NestViewModelTest. Existing tests
updated to use unique event ids.

https://claude.ai/code/session_01DMeCvWyBYVVVPez2hwqCs4
2026-05-01 19:24:48 +00:00
Vitor Pamplona 11d8eb0629 Fixes Mock class signature 2026-05-01 15:13:56 -04:00
Vitor Pamplona c2fd8727e0 Merge pull request #2689 from vitorpamplona/claude/review-nests-implementation-5kO48
Fix race conditions and improve resilience in MOQ-Lite subscribe/broadcast
2026-05-01 14:45:54 -04:00
Claude 8b7785a7e8 fix(nestsClient): token-parse hardening, mint timeout, IPv6 [], broadcaster-bail signal
- NestsTokenResponse.parse catch list now includes IllegalStateException
  so a malformed escape from a misbehaving auth server surfaces as
  NestsException instead of crashing the listener.
- OkHttpNestsClient gains a configurable callTimeoutMs (default 90 s)
  enforcing an upper bound on the entire mintToken round-trip including
  retries. Without it a stalled server suspends the reconnect
  orchestrator indefinitely (the orchestrator's openOnce step parks on
  mintToken). 90 s leaves headroom over the worst-case 63 s 429
  retry chain documented in MAX_RATE_LIMIT_RETRIES.
- parseEndpoint tightens IPv6 bracket check from `closeBracket > 0` to
  `> 1`, rejecting the empty `[]` literal.
- NestBroadcaster + NestMoqLiteBroadcaster gain an `onTerminalFailure`
  callback that fires once when the consecutive-send-error guard bails.
  MoqLiteNestsSpeaker wires this to flip the speaker state to Failed,
  giving ReconnectingNestsSpeaker the signal it needs to recycle the
  session — without this hook the broadcaster bailed silently and the
  outward speaker state stayed on Broadcasting forever.

Adds regression test:
  - onTerminalFailure_fires_once_after_consecutive_send_failures

225 tests pass, 0 failures. Android target compiles clean.
2026-05-01 18:42:19 +00:00
Claude 702885f4cb fix(nestsClient): fix race + leaks + Android encoder/decoder bugs from second audit
A fresh audit caught a real race in the round-1 subscribe() rewrite plus
several distinct bugs in the Android MediaCodec layer.

**MoqLiteSession.subscribe() race regression** — the launched collector
that reads the SubscribeResponse and watches for peer FIN ran cleanup
(remove from subscriptionsBySubscribeId, close frames) before subscribe()
itself reached the post-await registration. If the publisher FIN'd
immediately after sending Ok, the collector exited first against an
empty map; subscribe() then registered the subscription, leaving frames
in the map with no live collector to ever close it on transport drop.
Consumer hung forever — exactly the failure mode round-1 fix #3 was
supposed to prevent. Fix: pre-register the subscription BEFORE launching
the collector. Drop unused `ok` field from ListenerSubscription.

**MoqLiteNestsSpeaker.startBroadcasting publisher leak** — if
session.publish() succeeded but broadcaster.start() then threw (mic
permission denied, AudioRecord allocation failure), the publisher was
never closed and stayed registered as session.activePublisher,
permanently blocking subsequent startBroadcasting calls.

**runCatching{suspend close} swallowing CancellationException** —
MoqLiteNestsSpeaker.close, MoqLiteBroadcastHandle.close,
MoqLiteNestsListener.close all wrapped suspending closes in
runCatching, breaking structured cancellation when the parent scope
cancelled teardown. Replaced with explicit cancel-rethrowing try/catch.

**MediaCodecOpusDecoder ArrayList<Short> boxing on every audio frame**
— at 50 fps × 960 samples × N speakers ≈ 48 000 boxed Short
allocations/sec/speaker on the audio hot path. Rewritten to write
directly into a pre-sized ShortArray via ShortBuffer.get(dst, off, len).

**MediaCodecOpusEncoder bugs**
- KEY_AAC_PROFILE = AACObjectLC was being set on an audio/opus
  encoder. Meaningless for Opus; stricter Codec2 stacks on Android
  13+ reject the configure() call with IllegalArgumentException and
  surface as DeviceUnavailable. Removed.
- The drain loop's INFO_OUTPUT_FORMAT_CHANGED branch had no progress
  guard. A buggy encoder re-emitting FORMAT_CHANGED without producing
  output would busy-spin against the 10 ms dequeue timeout. Now
  absorbed at most once per encode call. Same guard added to the
  decoder.

Adds regression test:
  - frames_flow_completes_when_peer_FINs_immediately_after_Ok

224 tests pass, 0 failures. Android target compiles clean.
2026-05-01 18:42:19 +00:00
Claude 9729f3a20c fix(nestsClient): perf + robustness — jitter, frame buffer, defensive bounds, send-error guard
- NestsReconnectPolicy gains a `jitter` parameter (default 0.3, AWS-
  style equal jitter). Without it, every client reconnecting after
  a relay restart retries on the identical 1s/2s/4s/… schedule and
  thunders the relay during recovery. delayForAttempt also accepts
  an explicit Random source for deterministic tests.
- MoqLiteFrameBuffer decouples capacity from size so power-of-two
  growth actually amortises (the old shape allocated a fresh
  ByteArray then truncated capacity back to `needed` per chunk,
  defeating doubling). Adds a guard against varint reads past the
  live region into uninitialised slack capacity.
- prefixWithType inlines the size-prefix wrap so a publisher
  SubscribeOk/Drop reply doesn't allocate three ByteArrays.
- MoqLiteSubscribeOk gains the same `init { require(priority in 0..255) }`
  bounds check its sibling MoqLiteSubscribe has — a buggy caller
  building a malformed Ok reply now fails loudly instead of writing
  a truncated byte on the wire.
- NestBroadcaster + NestMoqLiteBroadcaster track consecutive
  publisher.send exception count and bail after 250 (~5 s at 50 fps)
  instead of holding the mic open forever on a permanently dead
  transport. publisher.send returning `false` (no inbound
  subscriber) is NOT counted — empty rooms are a normal state.

Adds 8 regression tests:
  - 5 in MoqLiteFrameBufferTest (multi-chunk reads, back-to-back
    payloads, growth amortisation, compact, varint past-live guard)
  - 3 in NestsReconnectPolicyTest (jitter spread band, jitter=0
    determinism, jitter=1 collapses to 0..base)

223 tests pass, 0 failures.
2026-05-01 18:42:19 +00:00
Claude f3a942dbd0 fix(nestsClient): more bugs from review — IETF parser, leaks, IPv6, URL encoding, structured concurrency
- Unknown IETF control message types now skip just the unknown frame
  instead of dropping the entire merge buffer (a peer sending a
  draft-17 message we don't enumerate — FETCH, GOAWAY, MAX_SUBSCRIBE_ID
  — would otherwise wedge the pump). Adds MoqUnknownTypeException
  carrying bytesConsumed so runControlPump can advance past it.
- pumpUniStreams + pumpInboundBidis wrap their inner collect in
  coroutineScope so per-stream drains are children of the pump's job
  (was: launched on the outer scope as siblings, leaking past
  cancelAndJoin until the transport's own flow errored out).
- parseEndpoint handles IPv6 authorities ([::1], [2001:db8::1]:4443).
  Naive lastIndexOf(':') used to find a colon inside the address.
- buildRelayConnectTarget percent-encodes the namespace path so a
  malicious / careless `d` tag containing `?`, `#`, `&`, ` ` etc.
  can't truncate the URL and shove the JWT into the wrong slot.
- Reconnect orchestrators (listener + speaker) replace
  `runCatching { openOnce / close / startBroadcasting }` with explicit
  try/catch that rethrows CancellationException, so cooperative
  cancellation from the parent scope dies promptly instead of running
  one more iteration after the cancel.

Adds three regression tests:
  - parseEndpoint_handles_IPv6_authorities
  - buildRelayConnectTarget_percent_encodes_path_unsafe_chars
  - unknown_message_type_throws_typed_exception_with_full_frame_size

215 tests pass, 0 failures.
2026-05-01 18:42:19 +00:00
Claude eb20ac2c2a fix(nestsClient): bug + perf fixes from Nests implementation review
- send() now nulls currentGroup and returns false on uni-stream write
  failure (was: silently returned true while reusing the dead stream)
- Subscribe bidi gets a single long-running collector that closes the
  frames channel on peer FIN / transport tear-down (was: consumer
  could hang forever on a black-holed UDP path with no Announce(Ended))
- AudioException.Kind gains EncoderError; mic-side encode failures no
  longer mis-route through DecoderError handlers
- MoqCodec.decode bounds the payload-length varint before .toInt() so
  a peer-driven absurd length can't wedge the parser forever
- Publish hot-path framing collapses to a single ByteArray allocation
  (was: Varint.encode + plus operator, two allocs per Opus frame at
  50 fps)
- Drops the now-unused readSubscribeResponseFromBidi /
  readSizePrefixedFromBidiInto / EarlyExit helpers

Adds a regression test that asserts the frames flow completes when
the peer FINs the subscribe bidi.

213 tests pass, 0 failures.
2026-05-01 18:42:19 +00:00
Vitor Pamplona 1a912e5d7c Merge pull request #2688 from vitorpamplona/claude/audio-transmission-tests-zKzlB
Claude/audio transmission test learnings
2026-05-01 14:35:46 -04:00
Claude 897287be5f nestsClient: expose moq-lite max_latency on subscribeSpeaker + correct plan framing
The moq-lite Lite-03 spec puts subscriber-side group-staleness control in
max_latency on the SUBSCRIBE frame: 0 = unlimited (relay falls back to its
MAX_GROUP_AGE = 30 s default); a positive value tells the relay to evict
groups older than that in favour of newer ones during transient backpressure.

Plumb it through:

  NestsListener.subscribeSpeaker(pubkey, maxLatencyMs = 0L)
    └─ MoqLiteNestsListener → MoqLiteSession.subscribe(.., maxLatencyMillis)
    └─ DefaultNestsListener (IETF) ignores — no equivalent on that wire
    └─ ReconnectingHandle re-issues across reconnects

Default stays at 0L for back-compat with the JS reference watcher; callers
opt in for low-latency live audio.

Also rewrites nestsClient/plans/2026-05-01-quic-stream-cliff-investigation.md
with corrected framing: of the four "upstream issues" the prior draft listed,
only our own missing MAX_STREAMS_UNI emission was a real bug (RFC 9000 §4.6
violation, fixed in d391ae1d). The relay's per-subscriber queue policy,
unbounded FuturesUnordered, no-timeout open_uni().await, and lack of
lagging-consumer detection are spec-permitted architectural decisions in a
gap moq-lite explicitly leaves to implementations — the spec puts staleness
control on the subscriber via max_latency, which we were sending as 0 the
whole time. Reframed as "feature request worth filing upstream", not "bugs".
2026-05-01 18:30:37 +00:00
Claude 9841e3ca2c plan: cite the actual moq-rs 0.10.25 source for the upstream stream-stop architectural gaps
Previous status note speculated about a per-subscriber forwarding ceiling.
This pins it down with file:line citations from the production version of
kixelated/moq so the upstream issue we file (or the next person to debug
this) has the exact code paths in hand:

  1. Unbounded per-track group queue (moq-lite/src/model/track.rs:69-90),
     evicted only by 30 s age (track.rs:29).
  2. serve_group's session.open_uni().await has no timeout
     (moq-lite/src/lite/publisher.rs:317-379) — blocks forever when the
     subscriber's CWND collapses.
  3. Publisher's FuturesUnordered task pool is unbounded
     (publisher.rs:325, 346) — keeps spawning blocked tasks with no
     backpressure to upstream.
  4. max_concurrent_uni_streams = 10000 (moq-native/src/quinn.rs:30-33)
     so the symptom is NOT the QUIC stream-id cap; it's the cascade
     above.
  5. No lagging-consumer RESET/STOP_SENDING anywhere; the relay never
     gives up on a stuck subscriber.

The framesPerGroup=5 mitigation we shipped sidesteps the cascade by
keeping upstream rate (10 streams/sec) below the threshold any of those
gaps would accumulate at. It doesn't fix the underlying issues — those
are upstream to address.
2026-05-01 15:53:43 +00:00
Vitor Pamplona 8cdeb13654 Merge pull request #2685 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-01 11:50:14 -04:00
Crowdin Bot 77318f14ce New Crowdin translations by GitHub Action 2026-05-01 15:47:46 +00:00