feat(service,host): a task's planning conversation is followed through the shared live layer
Merged
CH
6d ago+973+973+012345678901234567890123456789-190-190-012345678901234567890123456789
On main. This moves the open task's planning conversation onto the shared live layer (liveRows and the sealed live procedures), in place of the host's own loop of held reads woken by the call's words. Realtime is used only where LUKE_REALTIME=on (#4267); otherwise the follow rides the held poll (#4272).
What changes
- Service:
taskConversationLiveStatements(brain-transcript.ts) holds two parts, every statement immutable:mark: one row keyed by the task, carrying the cursor (messageCursorNow) and the newest turn with its status;workers: the running worker turns, one row each keyed by turn id.
- Live procedure
live.taskConversation(GET /api/live/task-conversation?id=&userId=) reads the task as the account and seals both parts. It answers not-found for a task the account doesn't reach, signed-out for another account'suserId, and polled (held pastafter) like main's other live reads. messages.taskwithhold=noneanswersbrainPageNow, a single look. The service's own held read (brainPagePast) is unchanged.- Host (
compose-tasks.tsand the newconversation-follow.ts):- the open task's conversation is followed through
liveRows, and asked past its cursor in one look each time the mark or the workers move; - the call's activity and end no longer need to wake it, so
wakeConversationand the parked-read machinery are gone; - a seal that isn't answered, or a follow that stops, is sealed again after a pause and resumes from the cursor held;
- signed out, or a task no longer held, ends it.
- the open task's conversation is followed through
Tests
task-work.test.ts(PGlite): the live mark and workers equal the held read's own reads at each step of a planning turn, and every step moves them. A teammate sees the start and a stranger sees nothing.brainPageNowanswers at once.live-statements.test.ts: the conversation's statements passmutableFunctionsOfand the seal check.live-procedures.test.ts: sealed for the bearer and the task, not found for a stranger, and polled without Realtime.apps/web/tests/tasks-app.test.ts:hold=noneis accepted only withafter.compose-tasks.test.tswith scripted Realtime:- each move of the mark or the workers asks one look, and the same rows, the call's words or a long wait ask nothing;
- workers arriving in another order don't move it;
- a mark Realtime stops is polled, and a polled mark that moved asks on from the cursor;
- an unsealed task is sealed again after a pause.
- Locally: features, host and service typecheck; vitest on those suites; knip, oxlint and task-names.