You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(voice): stop means stop — the abort now reaches the reader and the camera
The device found three failures in one call, all of them the same shape:
"stop" was reported as done while the work carried on.
**The abort never stopped anything local.** `applyInterrupt` has always
claimed the turn's reader was torn down; nothing did it, so an aborted
task was read to its natural end and Sai reported the result of work she
had just been told to abandon. And a turn about what the user is looking
at spends most of its life on this phone waiting for the camera, so a
"wait, stop" still ended with a shutter firing and a photo arriving for a
task that no longer existed. `abort()` now stops reading, cancels the
device's own work, and only then asks the server. A generation counter
covers the gap `ensureActive` cannot see: an event already parsed and
parked on the FSM's mutex — the mutex the abort itself holds.
**There was no way to say "drop this one and carry on."** `interrupt`
took the queue with it, `cancelQueued` cannot touch a running task, and
`relayToAgent` only narrows one from the inside — so the model reached
for `resetSession`, which wipes the conversation and stops no work at
all. `interrupt` gains a scope; an absent or unknown one still means
everything, because a bare "stop" means stop.
**And `resetSession` was one mis-heard word from a wipe.** It now
confirms once. The flag is cleared by any batch that is not another
reset, because a held reset happens with nothing running, so no turn
starts to clear it and a declined "no, just drop that" left the yes
standing.
Two more from the same log. Ask-first fired after a 40s capture and
silenced a result the user was sitting there waiting for — quiet is now
measured to the moment work STARTED, keeping the first stamp after they
speak, so the camera spinning up ends their silence rather than the
forward tens of seconds later. Its nudge also says plainly that it is a
delay and not a discard, after Sai offered to retry a task whose answer
she was holding; `ActivityLog` keeps that outcome so `getSaiStatus` can
answer with it.
The camera's retries were self-inflicted: the frame counter used
`take(N).collect`, which cancels the subscription on the Nth frame, so
every `capturePhoto` was issued against a stream nobody was reading. The
collector now stays subscribed until the still is in hand.
Calls no longer mint their own session. A conversation outlives a call —
five quiet minutes, folded glasses or a misheard goodbye all end one —
and each mint added a page to the user's sidebar. What made the original
hazard unescapable was a second bug, since fixed: "start fresh" rotated
the terminal's session instead of this client's.
One review fix on top: the abort POST is best-effort in code and not only
in its comment. It threw, which returned before the turn was closed out,
leaving the FSM in `working` with its reader already gone — no event
could arrive to end it, and admission held every later task behind a turn
that could not finish.
Docs: VOICE_FSM gains the two one-shot confirmations and the
conversation-per-call trade; three stale counts corrected (63 scenarios,
fourteen effects).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/SAI_GLASSES_APP.md
+6-1Lines changed: 6 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -103,7 +103,7 @@ while nothing is running, for instance.
103
103
|`ActivityLog` / `ConciergeProtocol`| The canonical nudge and activity-log wording (`describeAgentEvent` with prompt-injection fencing). Originally ported verbatim from the server's `core/activity-log.ts` / `core/nudges.ts`; those are gone and these are the only copy, pinned by the string goldens (§8.1). Feed `getSaiStatus` + the UI activity view. |
104
104
|`GlassesGestureSession`| DAT `DeviceSession` (no display/camera capability) reacting to the only temple gestures DAT surfaces: tap = mute/unmute Sai, tap-and-hold/doff/fold = end (all just `DeviceSessionState`; no gesture is remappable — see §6 "Glasses gestures"). |
105
105
|`SaiFiApp` / `MainActivity`| App init (`Wearables.initialize` once); `MainActivity` is now just the DAT-registration deep-link callback host (`saiwearables`) — the CameraAccess sample UI was pruned in productization. |
106
-
|`fsm/` (13 files) |**The conversation state machine**, ported from the server: modes and transitions, the bounded effect grammar and its parse boundary, the admission rule that holds a mid-turn task instead of folding it in, the local held-task queue, the cost guard, and every line the FSM speaks. Everything but `Concierge.kt` is pure, which is what makes the 59-scenario golden catalog runnable as JVM tests; `Concierge.kt` serialises all four input kinds through one `Mutex`, because two forwards interleaving at a suspension point books the restaurant twice. Design: [`VOICE_FSM.md`](VOICE_FSM.md). **This drives every call** — `CallService.buildConcierge` builds one `VoiceSession` per call and feeds the model's tool calls straight into it. |
106
+
|`fsm/` (13 files) |**The conversation state machine**, ported from the server: modes and transitions, the bounded effect grammar and its parse boundary, the admission rule that holds a mid-turn task instead of folding it in, the local held-task queue, the cost guard, and every line the FSM speaks. Everything but `Concierge.kt` is pure, which is what makes the 63-scenario golden catalog runnable as JVM tests; `Concierge.kt` serialises all four input kinds through one `Mutex`, because two forwards interleaving at a suspension point books the restaurant twice. Design: [`VOICE_FSM.md`](VOICE_FSM.md). **This drives every call** — `CallService.buildConcierge` builds one `VoiceSession` per call and feeds the model's tool calls straight into it. |
107
107
108
108
## 3. The client contract (frozen; the Kotlin app ports the browser reference client)
109
109
@@ -176,6 +176,11 @@ programmatic channel, as with a rebound Telegram link), so coming back to a mach
176
176
`recallHistory` will not reach what you did there before. Within a call that costs nothing: the Live
177
177
model holds the whole call in its own context.
178
178
179
+
That retirement is the server's, and the client no longer adds a rotation of its own on top of it.
180
+
Ending a call and starting another stays in one conversation — one page in the desktop sidebar — and
181
+
the only thing that rotates the session is the user saying "start fresh". See
182
+
[`VOICE_FSM.md`](VOICE_FSM.md) §7b for what that trades away.
0 commit comments