Skip to content

CL-6432: turn-phase typing indicator (reopened) - #183

Merged
TheGreatAxios merged 2 commits into
mainfrom
cl-6432-turn-phase
Aug 21, 2026
Merged

TheGreatAxios merged 2 commits into
mainfrom
cl-6432-turn-phase

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

The typing dots stayed up after Myra's reply again. This replaces the
event-pair rule PR #159 shipped with a turn-phase state machine.

Why the first fix failed live

#159 cleared the indicator on connector.reply and
message.run.ended, and its unit test drove both. Live, the second event
never arrives at that point: a folded run parks after the turn rather
than closing its bracket. What actually happens is

  1. message.run.started — the harness dequeues the user's message
  2. the reply streams (inference.start → inference.text.delta …)
  3. connector.reply — the orchestrator posts the persisted reply (CL-6432: clear the typing pulse on folded-run turn-terminal events #159
    cleared here, correctly)
  4. a post-reply tool-only round (memory writes) emits its own
    inference.start, which re-opened the empty pending pulse
  5. that round's textless inference.done left the pulse up, by the
    deliberate "an empty pending survives a tool round" rule
  6. the run parks — no message.run.ended, no reactor.done, nothing
    terminal ever arrives

So the indicator went back up right after the reply rendered and stayed
up until the 120s backstop. The unit test passed because it fed a run-end
event that a live parked run does not emit.

The fix

StreamingReplyState now carries an explicit phase:

  • "awaiting" — a turn is in flight; empty text renders the pulse,
    streamed text renders the growing bubble (the old behaviour)
  • "replied" — this turn's connector.reply has posted; renders
    nothing, and every inference.start / inference.done /
    reactor.start is inert, so post-reply tool rounds cannot re-open it
  • null — idle

The indicator comes back only on a genuinely new turn, keyed to a
real event rather than a timer: message.run.started from the stream
(the harness's per-dequeued-message bracket open, the same event
chat-orchestrator.ts keys new turns off via messageRunStarted), or
the local send that opens one (openPendingReply). The hard-terminal
events — reactor.done/reactor.error, message.run.ended,
inference.error, and a turnFailed notice — still return to idle from
any phase, so no failure path is left stranded.

Tests

The CL-6432 describe block is rewritten to the real order above: no
fabricated message.run.ended after the reply. It walks reply → memory
rounds → park, asserting the indicator stays down through every
post-reply round, and asserts it returns on the next turn.

chat-ui (620) and folded-runs (71) suites pass; chat-ui typechecks clean.

Live verification: pending

Being straight about this, since it is exactly what reopened the ticket.
I booted a scratch stack (own DB, port 3042, Studio Ollama connected as
an openai-compatible provider with qwen3.8:27b) and drove real turns
through the chat API, but could not get Myra off the seed-time
claude-sonnet-5 pin on that stack — every turn came back as the
"can't reach a model" notice rather than a real reply, so the
reply-then-memory-rounds sequence never ran there. The event order encoded
in the tests is the one the ticket documents from the owner's live repro,
not one I captured myself. A live confirmation on a stack with a working
provider is still owed.

Fixes CL-6432

Rewrites the CL-6432 block to the event order a live folded run actually
produces: message.run.started opens the turn, the reply streams and posts
via connector.reply, then post-reply tool-only rounds (memory writes) run
inference again and the run PARKS — no message.run.ended at that point.
The old block's fabricated run-end event is gone; the post-reply rounds
now assert the indicator stays down, and the next turn asserts it comes
back.
StreamingReplyState carries an explicit phase. connector.reply moves the
current turn to "replied" instead of clearing to idle, and in that phase
inference.start / inference.done / reactor.start are inert — the
post-reply memory rounds of a parked folded run can no longer re-open the
pending pulse. The indicator returns only on a genuinely new turn:
message.run.started from the stream, or the local send that opens one.
Hard-terminal events (reactor.done/error, message.run.ended,
inference.error, a turnFailed notice) still return to idle from any phase.
@TheGreatAxios
TheGreatAxios merged commit 580af22 into main Aug 21, 2026
0 of 2 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-6432-turn-phase branch August 25, 2026 15:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant