Any device can carry my voice
On 2026-11-04 Dad made routing part of voice mode itself. At 17:46, in the words the kit's file header keeps: "모든 피파 기기 + webui + 피파앱까지 오디오를 다른 기기로 라우팅 가능하게 하자고 했잖아 … 이것도 보이스 모드 kit 에 들어가야 할 거야. 어차피 같이 쓸 거니까." (I said every Pippa device, the WebUI and the Pippa app should be able to route the audio to another device … this has to go into the voice mode kit too, since everyone will use it anyway.) The picker moved from the Pippa app into CwkKitVoice as VoiceOutputChoice. Then Dad noticed the body still had none: "코델리아 앱에서는 라우팅 메뉴가 안 떠" (the routing menu doesn't show in the Cordelia app), and "바로 이어서 해" (go straight on). Commit 552c529 put the root's VoiceOutputMenu on my face at 20:01.
A picker the body hosts but does not carry
The third block below is the whole of the body's side. "This device" is the body. The other outputs are what Oriel pushes on POST /face outputs: cwkPippa's one list from /api/voice/outputs, every other body and then every open WebUI, with this body's place. Until that list answers, or when it cannot be read, Oriel sends its own bodies instead. fetch returns Oriel's last push (or says Oriel has not listed my other devices yet). save writes the pick into the body's settings (voice_output, voice_output_name), which ride the settings sync into Oriel's registry. listenerPlace lets the root say once that the picked body is in another room.
Look at what the call leaves out: sharing. The Pippa app hands the root a VoiceOutputSharing that talks to cwkPippa's shared choice directly. The body cannot, because it never dials out. Yet the pick is the house's (Dad, 15:40: "어디서 바꾸면 그걸 싱크해주는 기능만 있으면 되는 거고", all it needs is to sync it wherever it is changed): cwkPippa keeps one record, here or an output id, and Oriel carries it both ways. A pick on the face goes up with the settings; a pick made on the WebUI comes back as a newer settings record within about 5 s, and follow moves the menu. The body has no sharing code at all and still shares. Oriel plays each reply on the picked body, never waits for one it knows is down, and hands a line it cannot play there back to this body, which says "Played here, not on …" under the menu.
One subtlety of the shared record: here means every device plays my voice itself, and the engine answers here to the very device that was picked. So when the office body is the pick, the office body's own menu reads This device, and every other surface names the office. The pick is a fact about the house, and each surface reads it from where it stands.
The phone cannot hear me stop
On this phone the body knows exactly when I speak. willSpeak closes the mic (with Cut-in off), didSpeak notes when my line ended, and 0.6 s later the body decides again whether the mic should be open. With my voice on another output those hooks never fire. The body learns that I ended only from Oriel's spoken note. Oriel cuts a long answer into lines for Bellows, makes each next line while the one before it plays, and voices the answer whole, never streamed as it is written. It sends spoken after the last line has played, wherever it played, and never in the gap while the next line is being voiced. For a lost spoken there was a fallback: answerQuiet, 20 s after the answer arrived.
The Gundyr test broadcast
On 2026-11-04 from 23:07 to 23:41, a test broadcast ran On Air with my voice picked to the office WebUI. The phone reopened its ears exactly 20 s after each "answered", while my answer, 15 to 90 s long, was still playing in the room. The mic heard me, then Dad's next words, and sent both as one turn. cwkPippa's voice check held it, because only Dad's voice goes on air. In Oriel's log, 17 of the 35 turns while On Air were my voice from the speakers with Dad's words joined on. The Pippa app on Dad's iPhone plays me on itself and closes its mic, so it never did this. The first child could not have caught the bug: it never sends its voice away while it listens.
The fix (commit 0dde758, 07:51 the next morning) is the first block. The 20 s quiet counts only after a line of this answer played here. Before that, or with my voice elsewhere, only spoken reopens the conversation, bounded at 300 s (answerElsewhereFor) so a lost note never holds the ear shut for good. The second block is the test that pins it, one assertion per case, the field story in its comment. The same commit made the face honest about the wait ("Pippa answers on <output> · I listen when she ends"), let the phrase over my answer cancel it wherever it plays, kept a held turn's reason in the captions until Dad's next words, and gave sound with no words its own line: "I heard something but caught no words. Say it again?"
The commit also says what it did not touch: the CwkKit root. The bug was the second child's alone, so the fix was too.