feat(service,host): a held transcript read wakes at the edge only the clock reaches

Merged
CH
5d ago
+323-45

Stacked on #3282 (luke/live-agent-transcript) and #3265 (luke/neon-realtime-agent-status, merged in). Until #3265 lands, its five files (coding-agent-status.ts and its test, status.ts, bounds.ts, and features/package.json) show up in this diff. They are not part of this change.

Why

#3282 moves the transcript's hold into the host and wakes it on Realtime rows. Two things can end a hold with no row changing:

  • an agent with no turn passing STARTING_GRACE, after which it reads as failed;
  • the window of a title the read waits on closing (TITLE_TIMEOUT).

In #3282 the host only noticed these when the 20 s hold ran out. This PR wakes it at the right moment instead.

What

  • Service:
    • transcriptNow adds wakeIn when the page would hold: the milliseconds from the service's own now to the earliest edge.
    • The starting-grace edge comes from #3265's startingGraceEdge, using the newest turn and whether a line awaits.
    • The title edge comes from the new titleWindowEdge(parent, child?). It is the latest point at which any title the read waits on can still land: a worker's start or the agent's completed turn, plus TITLE_TIMEOUT. Like titleMayLand, it is none on a deployment that generates no titles.
    • wakeIn is a relative delay rather than an instant, so the service's and the desktop's clocks are never compared.
  • Wire: transcriptNowAnswerSchema gains an optional wakeIn. Its golden is regenerated.
  • Host: readUntilSaid races "the statements moved" against Effect.sleep(wakeIn) and reads again on whichever comes first.

Tests

  • coder-host.test.ts (PGlite), using the real relay:
    • an agent that is starting holds, with wakeIn equal to grace end + 1 − now;
    • an ended, untitled agent holds on its title, with wakeIn equal to settledAt + 20 s + 1 − now;
    • an untitled worker's edge is its start + 20 s + 1.
  • compose-coding-agents.test.ts: with no row moving, a read whose page says wakeIn: 3000 is still held at 2999 ms and answered with the next page at 3000 ms, rather than at the 20 s mark.

pnpm check:affected passes apart from the sandbox's app-menu.test.ts, which needs an Electron binary. Typecheck was rerun on its own after the combined run ran out of memory.

Made with Luke


Summary by cubic

Wakes a held transcript read at the moment the clock alone ends it, instead of at the 20 s hold's end. This diff also carries the branches it stacks on; the change here is the wakeIn field and the host's race.

Previously, two situations with no row change held a read to the hold's end: an agent with no turn passing its starting grace, and the window of an awaited title closing. The service's transcriptNow now answers wakeIn — milliseconds from the service's own now to the earliest edge — and the host races the live statements moving against Effect.sleep(wakeIn).

  • wakeIn is relative, so the service's and desktop's clocks are never compared; it is none on deployments that generate no titles.
  • The starting-grace edge reuses the shared status rule; the title edge is the new titleWindowEdge.
  • transcriptNowAnswerSchema gains an optional wakeIn.

Tests

  • The coder-host PGlite tests cover the starting-grace edge, an ended untitled agent's title edge, and an untitled worker's edge.
  • The compose test confirms a page with wakeIn: 3000 is answered at 3000 ms, not at the 20 s mark.