She thinks for seconds and the dock says nothing true about it #83
Labels
No labels
needs-info
needs-triage
ready-for-agent
ready-for-human
v2
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wayfinder:ticket
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
aiko/denpa#83
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Held a real voice conversation on 2026-07-31 and the operator's verdict on what was legible in the moment was: "I could only see tool calls in the logs. that's it."
That is not a treatment problem. It is that the two signals designed to say what she is doing during her think time are, respectively, a discarded frame and a state the client infers instead of reading.
Two halves
1. WAITING does not survive the think time.
conversation-chain-startfires only once Hermes has produced the whole turn (single_conversation.py'ssend_conversation_start_signalsruns after the finished text arrives, not when the turn is accepted), so the gap between the press and her first word is the entire think time. WAITING auto-returns to IDLE after 2s, so the dock reads 入力中 → 待機 → 送信中. The meter never changes — 待機 and 入力中 share the idle floor — so what actually differs is the word, the PTT key returning to 押して送信, and 割り込み un-dimming for a turn that is still in flight.#58's close-out flagged this and asked for a decision. Decided 2026-08-01: hold WAITING until chain-start or a fault. The 2s is not a new timer being removed — it exists to un-stick a typing debounce, and a sent turn does not need un-sticking. Applying it to a sent turn was the mistake.2.
typingarrives on every turn and is thrown away.DenpaAdapter.send_typingandstop_typingare thePlatformAdaptermethods Hermes' owngateway/platforms/base.pycalls around every turn;AdapterHandler.handle_frametranslates them andWebSocketHandler.send_to_devicedelivers them. The client has no case —websocket-handler.tsx'shandleWebSocketMessagefalls todefault:and logsUnknown message type. Established by denpatou#32.So the truthful signal for exactly the state half 1 is about is already on the socket, arriving, and being discarded — while the client infers the same state from
aiState.What is needed
typingand uses it as the signal, rather than inferring the state.active: trueis her working;active: falsewithout a chain-start is the interesting case and wants a deliberate answer rather than falling through.#58already decided for its two seconds.Acceptance
typingframe is consumed rather than warned about, and drives the stateRelated
denpa#58(WAITING's treatment, and the call this settles),denpatou#32(the dropped frame),denpa#59(the fault screens WAITING hands off to)Merged locally as
e2c069a(feature2e60978, cross-seam fixaf62cc6). Nothing pushed.Acceptance
ai-state-context.tsxgrows aholdoption and a purewaitingPush(current, options); the two sender surfaces (use-text-input.tsx'shandleSend,transmission.ts'spttUp) pass it and the two keystroke surfaces do not.chrome-spec.ts'sinterruptIsDimreadsaiState === WAITING, so it follows from the hold.typingframe is consumed and drives the state —services/typing.ts's puretypingStep(active, aiState), 13 cases plus a wiring scan. The producer was verified on the go-between's merged tree:adapter_handler.py'sFRAME_TYPINGbranch sends{"type": "typing", "active": …}to the device, coercing an unsaidactiveto false, which is why the client reads=== true.runFailureAfterMessage'sturnIsOutstandingdropped it silently. Real improvement, not itself covered by a test.active: falsewith no chain-startRe-enters WAITING un-held, handing it to the same 2s return. Holding would strand the dock at 入力中 with no way back but a redial, since nothing else is coming for that turn; snapping to IDLE would flicker if a chain-start is racing in behind the stop, and the ordinary order is chain-start then stop. The 2s window is exactly the un-sticking that timer was written for.
What the cross-seam review found, and it is the reason this merged with
#82onPttUpdraws the held WAITING synchronously, because a release has to feel immediate. Whether the press reached Rust is only knowable once the press chain settles — a short tap can release whilemic_startis still opening the device. Before this issue, a press that never landed was swept up by the 2s return. With the hold, nothing swept it up, so a refusedmic_startleft the dock at 入力中 waiting for a chain-start no turn had earned, until the socket moved.#82adds a brand-new way for that refusal to happen —startForegroundthrowing.The obvious fix, refusing to draw WAITING unless the press is held, is wrong the same way: at release time a good press may still be opening, and that tap would then never show WAITING at all. Fixed instead by walking it back where the answer is final —
pressNeverLandedintransmission.ts, reached from the release chain.Per-issue review could not have seen this. It is what the batch's combined-diff pass is for.
Deliberately left
A turn that dies upstream with neither a chain-start nor a
stop_typingstill strands the hold until the socket moves. No second release path was added for it: that is the go-between's to answer, and inventing a client-side timeout would rebuild the two-second return this issue removed. The escape hatch is that a reconnect bringsset-model-and-conf, which sets IDLE — unverified that the go-between re-sends it on every reconnect.Also unverified: that a
typingframe arrives on this route in practice.denpatou#32established the producer by reading; no gateway was run.Gates
npm test941 across 73 files (was 917/70),npm run typecheckexits 0,cargo test28.npm run lintis broken repo-wide — theairbnbconfig it references cannot be resolved — which predates this work.