feat(service,host): a task's planning conversation is followed through the shared live layer

Merged
CH
6d ago
+973-190

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's userId, and polled (held past after) like main's other live reads.
  • messages.task with hold=none answers brainPageNow, a single look. The service's own held read (brainPagePast) is unchanged.
  • Host (compose-tasks.ts and the new conversation-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 wakeConversation and 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.

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. brainPageNow answers at once.
  • live-statements.test.ts: the conversation's statements pass mutableFunctionsOf and 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=none is accepted only with after.
  • compose-tasks.test.ts with 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.