Skip to content

feat(service): the service's held reads and waits on a row are woken by Neon Realtime - #3266

Closed
charleslpan wants to merge 14 commits into
luke/neon-realtime-scope-inboxfrom
luke/neon-realtime-service-wake
Closed

charleslpan wants to merge 14 commits into
luke/neon-realtime-scope-inboxfrom
luke/neon-realtime-service-wake

Conversation

@charleslpan

@charleslpan charleslpan commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

This is part of making Luke as realtime as possible on Neon Realtime and removing hand-rolled freshness logic. It is stacked on #3232 (which is stacked on #3217), and its base is set to #3232's branch.

The desktop-side PRs move the desktop off the held long-polls. This one is the service side. The held routes that released desktops still poll, and the service's own waits on a row, now wait on Realtime instead of re-reading the store every 500 ms or 250 ms.

What changes

  • live/live-wake.ts: LiveWake.
    • It's a Context.Reference with no polling default: unprovided, every watch dies. The web runtime provides liveWakeFromEnvironment, using the same NEON_REALTIME_URL / NEON_REALTIME_SECRET as LiveSealing; missing config is logged as an error and every watch fails loudly. Tests provide a scripted wake (testing/live-wake.ts).
    • Each function instance holds one Realtime direct server, following Neon's "Subscribe in trusted backend code" guide, and closes it with the runtime.
    • A wait subscribes to its statements as raw updates for its length and unsubscribes after. Any update wakes it; a missed update costs a look on the safety beat.
    • Realtime only signals; every answer is still read by the service's own statement.
  • http/held-read.ts:
    • lookedUntil(look, hold, live) looks, then waits for the next change of the watched statements or a 5 s safety beat. It takes no poll.
    • heldUntilMoved is built on it.
  • Callers:

Transcripts, the planning conversation, and the Mac's own held read are left to their own stacks, which other agents are working on.

Tests

  • live-wake.test.ts checks four things:
    • with no wake provided, a wait fails rather than looking on a clock;
    • when watched, it looks again on a change and on the safety beat;
    • the wake over a scripted direct server subscribes per statement, wakes on updates (keeping one that no wait took yet), unsubscribes when the scope closes, and leaves a wait on its safety beat when a subscription is refused.
  • PGlite tests for each new statement:
    • the comment statement equals planCommentStamps for a member, a teammate, and an outsider;
    • the board statement moves on a drawing and on a scene save, carries no image, and is empty for a stranger;
    • the command statement says settled once the command settles, and reads nothing of another account's.
  • pnpm check:affected: typecheck, lint, knip, and discarded-effect pass, and all tests pass except apps/desktop/src/main/app-menu.test.ts, which can't load Electron in the sandbox. The web bundle tests pass.

Realtime is on in production and its variables are in Vercel, so the service's waits follow over Realtime alone, with no polling fallback.

Made with Luke


Summary by cubic

The service's held reads and waits on a row are now woken by Neon Realtime instead of re-reading the store on a poll, so waits like the inbox, plan comments, look_at_board, and run_on_mac's result arrive when the rows change rather than on a clock. Deployments without Realtime, and any watch that breaks or is refused, keep polling as before.

  • Adds LiveWake, a runtime-provided context that subscribes each wait's statements over Neon Realtime's direct server for the length of the wait; every update wakes it and an error hands it back to its poll.
  • Adds lookedUntil in held-read.ts, which looks, then waits on the watched statements or a 5-second safety beat; heldUntilMoved is built on it.
  • Adds small live statements (planCommentStampsLiveStatement, boardImageLiveStatement, macCommandSettledLiveStatement) that carry no words, image, or command output, so only freshness signals travel through Realtime and every answer is still read by the service's own statement.

Written for commit 6c9622d. Summary will update on new commits.

View guided diff Turn on auto-fix

@vercel

vercel Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
luke-web Ready Ready Preview Oct 20, 2026 2:37am UTC

Request Review

@luke-agent
luke-agent Bot changed the base branch from main to luke/neon-realtime-scope-inbox October 17, 2026 01:07
@luke-agent luke-agent Bot closed this Oct 17, 2026
@luke-agent luke-agent Bot reopened this Oct 17, 2026
@luke-agent
luke-agent Bot force-pushed the luke/neon-realtime-scope-inbox branch from 0a6e6cd to 35d1920 Compare October 18, 2026 19:21
@luke-agent
luke-agent Bot force-pushed the luke/neon-realtime-service-wake branch from 6c9622d to f835f49 Compare October 18, 2026 19:40
@luke-agent
luke-agent Bot force-pushed the luke/neon-realtime-scope-inbox branch from 35d1920 to ff729c0 Compare October 18, 2026 21:12
@luke-agent
luke-agent Bot force-pushed the luke/neon-realtime-service-wake branch from f835f49 to 2180711 Compare October 18, 2026 21:40
@luke-agent
luke-agent Bot force-pushed the luke/neon-realtime-scope-inbox branch from ff729c0 to 7c1c859 Compare October 19, 2026 18:52
@luke-agent
luke-agent Bot force-pushed the luke/neon-realtime-service-wake branch from 2180711 to e46eaf4 Compare October 19, 2026 18:52
@luke-agent
luke-agent Bot force-pushed the luke/neon-realtime-scope-inbox branch 2 times, most recently from a7cb527 to 39ca561 Compare October 20, 2026 01:29
@luke-agent
luke-agent Bot force-pushed the luke/neon-realtime-service-wake branch from e46eaf4 to b3338fd Compare October 20, 2026 01:36
charleslpan and others added 8 commits October 20, 2026 01:38
…eon Realtime

A plan's inbox and one agent's inbox, the window's held reads, are now
answered from a Realtime subscription the host holds while a window reads
them, instead of the service re-reading the store every half second:

- The service seals one live statement per scope, its open items and its
  latest settled ones as one result, so an item settling moves in one
  change (GET /api/live/plan-inbox?id=, /api/live/agent-inbox?id=), after
  reading the plan or agent as the account, so one it does not reach is
  not found.
- The follower is now generic (live-follow.ts): every part subscribed on
  one socket and renewed before it expires, its rows decoded by the caller.
- The host keeps a subscription per scope while it is read and 30 seconds
  after, and answers the window's read as soon as the items move past its
  cursor, or unchanged when the hold ends, as the held read did. Where the
  service seals nothing, the held read answers as before.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
…d does

Main's held read of a task's or an agent's inbox now says who started
each item, its answer's files, its thread, when it settled, and whether
it is unread for the reader. The scope's live statement selects the same
columns (the store's own `startedByName`, `ANSWER_FILES`, `MESSAGES`,
`SETTLED_AT`, and `unreadFor`), and live-wire.ts writes each row as the
held read writes its item. The account inbox's two chat parts are followed
by the generic follower too.

The PGlite parity test now covers a teammate's message with a file,
which leaves the item read for them and unread for its starter.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
…ecks

Main's checks refuse a plan-spelled name for a task and a `node:` import
anywhere the host's browser doors reach. The task inbox's live route is
`/api/live/task-inbox` (its file `routes/live/task-inbox.ts`), and the
host digests a scope's cursor with Web Crypto's SHA-256 rather than
`node:crypto`.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
…th no held fallback

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
…ity Neon Realtime requires

mutableFunctionsOf makes the statement a temporary view, its placeholders
written as literals, and reads every function, aggregate, window function,
and operator's function its rule's tree names from pg_rewrite, answering
each whose provolatile is not 'i'. pg_depend records no dependency on a
built-in, so it cannot answer this. Any branch's live statements can be held
to it under getTestDB().

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
…ly immutable functions

Neon Realtime refuses a statement calling any function Postgres does not
mark immutable. The plan and agent inbox's one live statement aggregated
each item's thread and files with jsonb_agg and jsonb_build_object, both
stable, and read its thread's and settlement's instants with extract from a
timestamptz, also stable.

The thread's latest messages and the files an answer or a message carried
are now live parts of their own, which the reader joins to the items
(inboxOfLiveRows), and every instant is read at UTC through liveSelection.
The held read keeps its aggregates. A test holds every statement the live
endpoints seal to immutability (mutableFunctionsOf); it named extract,
jsonb_agg and jsonb_build_object on the task and agent parts before this.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
… read wakes its held read

The service's held read stamps whether each item is unread for the reader,
so their marker moving wakes it. The live scope inbox's cursor left unread
out, so a read or unread mark answered nothing until the hold ended.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
…leAdapter()

Neon's guide seals a Drizzle select through its Drizzle adapter rather than
raw SQL over toSQL(). LiveStatement now carries the select it was rendered
from, and the sealer passes it to createRealtime({ adapter: drizzleAdapter() })
.seal({ query }), which also refuses a select whose key is not the name
Postgres answers it under. A statement written as text alone, as the tests'
stand-ins are, or one binding an instant, which the adapter does not encode,
is still sealed with rawSql.

@neon/realtime-drizzle 0.1.0 declares drizzle-orm >=0.45.2 <0.46.0 as its
peer; it reads only what 1.0.0-rc.4 builds the same way, so the peer is
allowed in pnpm-workspace.yaml and the version pinned.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
A cast through a type's text form (CoerceViaIO) names no function in the
rule's tree, so max(at)::text and text::timestamptz, which call the stable
timestamptz_out and timestamptz_in, passed. Each cast is now read from the
tree: the input function of the type it casts to, and the output function
of the type it casts from, a scalar subquery's by its one column; a cast
from a type the tree does not say is answered as one.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
…th asks

Main's held read of an inbox item (#3992) now carries its v4 messages
without the ones that ask or answer, its whole thread with each ask as it
stands on the item, who closed it, and the answer's files from the message
that answered. The live scope inbox delivers every message either thread
draws, with its ask and answers, and the files of the answering message,
and the reader builds both threads and each ask's standing as the held read
does. The ask's standing (standingAsk, askStillAsked, knownAsk) moves into
@luke/features/inbox-wire, so the store and the host read it one way.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
@luke-agent
luke-agent Bot force-pushed the luke/neon-realtime-scope-inbox branch from 39ca561 to 40e1669 Compare October 20, 2026 01:43
charleslpan and others added 2 commits October 20, 2026 01:43
…by Neon Realtime

Every wait of the service's on a row now watches that row over Realtime
rather than re-reading it on a clock, so the held routes released desktops
still poll, and the service's own waits, stop costing a look every half
second:

- LiveWake (live/live-wake.ts): one Realtime direct server per function
  instance, subscribed to a wait's statements for its length; every update
  wakes it, an error hands it back to its poll. Polling where the
  deployment has no Realtime, and in every test.
- held-read.ts gains lookedUntil: look, then wait for a change of the
  watched statements, or a 5 s safety beat, or the poll unwatched.
  heldUntilMoved is built on it.
- The account, plan, and agent inbox reads watch their live statements;
  plan comments, look_at_board, and run_on_mac's wait for a result watch
  small statements of their own (planCommentStampsLiveStatement,
  boardImageLiveStatement, macCommandSettledLiveStatement), which carry no
  words, image, or output.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
… fallback

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
charleslpan and others added 2 commits October 20, 2026 02:19
…es it to error

Neon's docs: an error is final, so a long-running worker watches for it
with onStateChange and subscribes again, on a new client where the
connection itself failed. The safety beat stays the backstop.

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
@luke-agent
luke-agent Bot force-pushed the luke/neon-realtime-scope-inbox branch 2 times, most recently from ff97170 to 9710e65 Compare October 20, 2026 15:50
@luke-agent

luke-agent Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Closing for now: the developer moved every area onto the central live layer with Neon Realtime off (LUKE_REALTIME, #4267; held polls, #4272), and this change only does something with Realtime on. The branch is kept, so the PR can be reopened and rebased when LUKE_REALTIME goes back on.

@luke-agent luke-agent Bot closed this Oct 20, 2026

This branch was successfully deployed

1 active deployment
Preview — e08f9b2a Deployed Oct 20, 2026 by vercel[bot]
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