feat(service): the voice brain follows a spoken ask's turn on Neon Realtime instead of polling

Draft
CH
6d ago
+104-209

Third of three small PRs that remove Luke's last transcript and voice loops. The goal is realtime with no fallbacks. On the coordinator's call, this is stacked on #3295 (luke/neon-realtime-planning-hold), beside #3757, and not on #3931: it touches only the service. It needs #3266's lookedUntil/LiveWake and #3267's taskConversationLiveStatements, and #3295 carries both.

What changed

  • voice/live-brain.ts: a spoken ask's follow no longer reads the record every 250 ms (POLL), and no longer wakes on eve's session stream with a 60 ms SETTLE.
    • It now calls lookedUntil on the task conversation's mark. The record is read at once, then again each time the mark moves over Neon Realtime. The mark moves when a message lands, when the journal revision goes up, when the newest turn or its status changes, and when a worker starts or ends.
    • The hold is PARKED_FOLLOW, the widest bound. The look itself still says when a turn is past the plain bound or the parked one, as before.
    • LiveWake is taken where the brain is built, on the socket runtime that runWeb provides with liveWakeFromEnvironment, and passed to each follow's fiber.
    • Retired: POLL, SETTLE, STREAM_RETRY, EVE_DELTA_EVENTS, and the per-session stream listener.
  • voice/live-exchange.ts and exchange-attachment.ts: the exchange gets the session's taskId and passes the brain taskConversationLiveStatements(conversation.userId, taskId).
  • eve-sessions.ts: EveStreamEvent is no longer exported, so knip passes. EveSessions.stream has no caller left. Removing that seam means editing eight test fakes, so I left it for a follow-up.

Worth knowing

lookedUntil on this base still looks every LIVE_SAFETY_POLL (5 s) beside the watch. That's #3266's primitive, and I didn't change it. A worker's own journal moving between its start and its end doesn't move the mark. So a worker step shown on the Work tab can arrive up to 5 s late, until the turn's own journal or a worker's start or end moves the mark. I didn't chase that edge case.

Tests

  • live-brain.test.ts:
    • Its real-clock suite runs on tickingLiveWake, a new helper in testing/live-wake.ts whose watch hears a change every 5 ms, standing in for Realtime hearing each write.
    • The test that a turn is heard the moment eve's stream says something is replaced by one where the turn is heard the moment Realtime says the mark moved. It checks that the follow watches the mark it was handed. It asserts nothing about the time before the mark moves, because on CI's shared Postgres the 5 s safety beat can look first. That race failed the first CI run.
  • live-exchange.test.ts and voice-service-exchange.test.ts provide the ticking wake to the exchange they build.

Checks run locally

pnpm check:affected gets OOM-killed in the sandbox, so I ran its pieces one at a time:

  • repository-checks.sh, biome check ., oxlint, knip
  • discarded-effect (with a larger heap)
  • tsc for service and web
  • vitest for src/voice, src/agents/brain-host, and src/live (114 tests)

What to check by hand

  1. In a call on a task, ask Luke something that takes a few steps. His activity line and spoken reply should come at least as quickly as before, without the steady 250 ms reads.
  2. Ask him to hand work to a worker. Its findings should still be spoken when they land.

Made with Luke