feat(lotus): expected-device check before joining (cinny #121) #62

Merged
jared merged 1 commits from feat/expected-devices into lotus 2026-10-24 19:47:39 -04:00
Owner

Fork half of cinny #121. Owner decisions 2026-10-25: ask before joining, and "usual" = the device you last picked.

Problem: EC remembers the picked mic and output by id. When that device is missing it silently uses the first one available, so "always joins with a headset" ends up on the laptop mic with no warning.

Change (src/lotus/lotusDevices.ts), active only with lotusDeviceCheck=1:

  • Remember by name: the last-picked device's name is stored next to its id (lotus-audio-{input,output}-name), because ids can change when a device is re-plugged on Windows. Choosing the system default clears it.
  • Before the automatic join, both kinds are checked against the current device list:
    • device still there: join;
    • back under a new id, same name: re-selected silently, then join;
    • gone: report io.lotus.device_missing { missing: [{ kind, expected, fallback }] } and wait for io.lotus.device_decision { join } from the host. fallback is what EC would use instead.
  • In a call: losing the usual device sends io.lotus.device_lost { kind, expected, fallback }, for a toast.
  • Fails open: if there are no device names (no permission yet; waits 3 s), no host, or nothing remembered, it joins as before.
  • Without the flag the join stays synchronous, as upstream (CallView tests unchanged).
  • The gate runs once per call: CallView's effect re-runs while its inputs settle, and restarting the 3 s check each time held the join indefinitely. The cinny run caught this.

Tests

  • 10 new unit tests: the pure check; the gate's join, ask, re-select and no-names paths; the name memory; the disconnect report.
  • Full suite: 1,013 passed. tsc, oxlint, knip and oxfmt are clean.
  • End to end in cinny (local Synapse + LiveKit, fake devices, a usual mic "Arctis 7" seeded):
    • the prompt shows "Arctis 7 isn't connected. Join with Fake Default Audio Input instead?";
    • nothing joins while asking;
    • Cancel removes the call, with no membership;
    • Join anyway joins, and the next start in the same session doesn't ask;
    • re-plug (known name, stale id): no prompt, joins, and the stored id now maps to that device.
  • The mid-call toast is covered by unit tests only, because fake devices can't be unplugged.

Embedded version 0.28.0-lotus.2.

🤖 Generated with Claude Code

https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5

Fork half of cinny #121. Owner decisions 2026-10-25: **ask before joining**, and **"usual" = the device you last picked**. **Problem:** EC remembers the picked mic and output by id. When that device is missing it silently uses the first one available, so "always joins with a headset" ends up on the laptop mic with no warning. **Change (`src/lotus/lotusDevices.ts`), active only with `lotusDeviceCheck=1`:** - **Remember by name:** the last-picked device's **name** is stored next to its id (`lotus-audio-{input,output}-name`), because ids can change when a device is re-plugged on Windows. Choosing the system default clears it. - **Before the automatic join**, both kinds are checked against the current device list: - device still there: join; - **back under a new id, same name:** re-selected silently, then join; - **gone:** report `io.lotus.device_missing { missing: [{ kind, expected, fallback }] }` and **wait** for `io.lotus.device_decision { join }` from the host. `fallback` is what EC would use instead. - **In a call:** losing the usual device sends `io.lotus.device_lost { kind, expected, fallback }`, for a toast. - **Fails open:** if there are no device names (no permission yet; waits 3 s), no host, or nothing remembered, it joins as before. - **Without the flag** the join stays synchronous, as upstream (`CallView` tests unchanged). - **The gate runs once per call:** `CallView`'s effect re-runs while its inputs settle, and restarting the 3 s check each time held the join indefinitely. The cinny run caught this. **Tests** - 10 new unit tests: the pure check; the gate's join, ask, re-select and no-names paths; the name memory; the disconnect report. - Full suite: 1,013 passed. tsc, oxlint, knip and oxfmt are clean. - **End to end in cinny** (local Synapse + LiveKit, fake devices, a usual mic "Arctis 7" seeded): - the prompt shows "Arctis 7 isn't connected. Join with Fake Default Audio Input instead?"; - nothing joins while asking; - **Cancel** removes the call, with no membership; - **Join anyway** joins, and the next start in the same session doesn't ask; - **re-plug** (known name, stale id): no prompt, joins, and the stored id now maps to that device. - The mid-call toast is covered by unit tests only, because fake devices can't be unplugged. Embedded version **0.28.0-lotus.2**. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
jared added 1 commit 2026-10-24 19:42:45 -04:00
feat(lotus): expected-device check before joining (cinny #121)
CI / Build embedded bundle (push) Skipped
CI / Build embedded bundle (pull_request) Successful in 4m21s
CI / Publish to Gitea npm registry (pull_request) Skipped
a366243836
EC remembers the picked mic / output by id and silently falls back to the
first device when it is missing, so a user who always joins with a headset
lands on the laptop mic. With lotusDeviceCheck=1:
- the last-picked device's NAME is remembered next to its id
  (lotus-audio-{input,output}-name), since ids can change on re-plug;
- before the automatic join, a usual device that is gone is reported to the
  host (io.lotus.device_missing) and EC waits for io.lotus.device_decision
  { join }; one that came back under a new id is just re-selected;
- in a call, losing it is reported (io.lotus.device_lost) for a toast.
Fails open (no names / no host / nothing remembered). Without the flag the
join stays synchronous, as upstream. The gate runs once per call: CallView's
effect re-runs while its inputs settle and must not restart it.

Embedded version 0.28.0-lotus.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
jared merged commit 4c84c3a4f3 into lotus 2026-10-24 19:47:39 -04:00
Sign in to join this conversation.