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.
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
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 lotus2026-10-24 19:47:39 -04:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 withlotusDeviceCheck=1:lotus-audio-{input,output}-name), because ids can change when a device is re-plugged on Windows. Choosing the system default clears it.io.lotus.device_missing { missing: [{ kind, expected, fallback }] }and wait forio.lotus.device_decision { join }from the host.fallbackis what EC would use instead.io.lotus.device_lost { kind, expected, fallback }, for a toast.CallViewtests unchanged).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
Embedded version 0.28.0-lotus.2.
🤖 Generated with Claude Code
https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
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