Skip to content

docs(eve): research - event consumers plan#1205

Closed
ruiconti wants to merge 18 commits into
rui/notification-consumer-prototypefrom
rui/notification-reactors-research
Closed

docs(eve): research - event consumers plan#1205
ruiconti wants to merge 18 commits into
rui/notification-consumer-prototypefrom
rui/notification-reactors-research

Conversation

@ruiconti

Copy link
Copy Markdown
Contributor

Research plan for #1170. Top of the stack: main → #1202 (terminal) → #1204 (relay) → this. Design only, no runtime changes. Recreated after a stack reparent closed the prior PR (#1172) — the branch and commits are intact.

What

Normalizes event consumption around eve's durable session stream. Two lanes, distinguished by delivery guarantee:

The doc frames the relay lane as the on-ramp for every event class that should propagate without waking a session, and records the sidecar run as the graduated tier (its own run, its own journal — the replay-cost firewall) for event classes that later need cursor semantics, ordering, or consumer-held state.

Note

The doc's iteration history explored a per-session sidecar consumer before the prototype (#1204) showed route-direct ingestion satisfies the v1 relay contract with far less machinery. The doc retains the sidecar as the graduated tier rather than the v1 default. Fifteen commits of design iteration are preserved deliberately as the archaeology of that decision.

@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
eve-docs Ready Ready Preview, Comment Jul 25, 2026 1:57am
eve-docs-1644 Ready Ready Preview, Comment, Open in v0 Jul 25, 2026 1:57am
eve-docs-4759 Ready Ready Preview, Comment, Open in v0 Jul 25, 2026 1:57am

Request Review

#1170)

A remote callee's authorization.required/completed events reach the
caller and ride the same durable path a local subagent's do: the callee
forwards each to the caller's session-callback URL as a
status: "notification" event; the caller route projects it into a
subagent-authorization-event and resumeHook()s the parked parent turn,
where the turn workflow proxies it onto the parent stream. No best-effort
side channel — authorization is low-frequency control, so it belongs on
the main run's journal, handled in workflowEntry exactly as local.

The high-frequency progress-relay lane (issue #1170) stays reserved: it
is the one that must avoid the main run's replay cost, and it has no
concrete event type yet.

Built on #1202's terminal envelope. Restores the forwarding stripped
when that PR isolated the terminal half.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
ruiconti added 16 commits July 24, 2026 21:55
Producers append; consumers react. Each session gets one companion
reactor run parked on a notify hook: producers (the remote callback
route, the local subagent proxy, the callee's emit path) deliver
curated child events with one fire-and-forget resumeHook, and the
reactor appends the wrapped subagent.event to the session stream and
performs the side effects (channel rendering, callback forwarding) in
journaled steps. The session workflow is never woken for notifications,
which closes the parked-for-input delivery hole and unifies the local
and remote child-event paths into one shape.

Backed by #1170.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
Per-lane wake accounting: the caller already pays a queue wake per
notification (resumeHook into the session workflow), so the reactor
there replaces a wake rather than adding one. The callee's outbound
forwarding is the only lane where a reactor would introduce a new
per-event wake, so v1 keeps the direct POST and merely defers it off
the emitting step's critical path (today the caller's RTT is awaited
inside the emit). Higher-frequency event types are admitted to the
lane only with cursor-based coalescing, so N events during one
invocation cost one wake.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
…tract

No new channel authoring surface. The reactor invokes the same adapter
event handler the session workflow's proxy step invokes for these
events today, so channels render notifications with zero changes; the
run doing the invoking is the only difference. Adapter state mutated
while handling a notification persists in the reactor's own run state,
which also resolves the edit-in-place rendering question without a
stateless-hook contract. The subagent.event wrapper stays a
stream-representation concern for followers and clients.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
The reactor is a framework-registered workflow function
(workflow//eve//notificationReactor) in the same compiled bundle as the
entry and turn workflows, executing as queue-driven invocations of the
caller's own deployment — no new compute surface. Started plainly by
the entry run (no latest-deployment routing): it holds the entry's
stream writable and channel context, so it is pinned to the entry's
deployment for the session's lifetime.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
…nsumers

Same design; the run is a consumer, and the doc now says so everywhere,
including the workflow id (workflow//eve//notificationConsumer) and the
file name.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
…tion

Lead the summary with the model rather than the bug: the durable
stream is the propagation backbone, producers append, and consumption
is a separately scheduled concern — followers already work this way,
and the notification consumer is the first non-follower consumer moved
onto it. The failure list stays as the evidence, and the lane is named
as the on-ramp for future event classes (progress, proxied HITL) and
for converging the remaining emit-coupled consumers.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
Same design, a third shorter: cut repeated rationale and filler; every
section now states its point once.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
…on normalization

The doc jumped from stream-backbone framing straight into the v1 design
without saying what the proposal is. Lead with it now: the harness funnels
every event a single way and all consumers, channels included, consume from
a durable delivery step, trading one extra hop per event for durable
redelivery.

The failure bullets become an explicit Why list and gain the missing
redelivery reason (failed or refused deliveries are logged and dropped;
nothing durable exists to redeliver from). A new Comparison artifacts
section names the two runnable references for the decision: reopened #1167
as the baseline that pipes notifications through today's continuation
entry point (to be stripped of its eve/tools authorization exposure), and
a prototype of the consumer design against the same evals.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
"Notification" is #1167's wire vocabulary — one status class on the
callback envelope — not the unit this proposal normalizes. Organizing the
plan around it undersold the change: the funnel and the durable delivery
step apply to events as such; which classes are admitted (authorization.*
in v1, working/HITL behind the frequency rule) is an implementation
detail of a given lane.

Rename research/notification-consumers.md to event-consumers.md and sweep
the design identifiers with it: the companion run is
workflow//eve//eventConsumer, the hook is <sessionId>:events, the stream
section is "The propagated event". "notification" survives only where
it is factual: the status field on #1167's callback envelope.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
Every failure bullet now carries commit-pinned code references
(413ce48, the #1167 baseline) plus a concrete example: the drop window
is the authorization.completed POST racing the terminal callback,
verifiable with one curl against an unparked session's callback URL;
rendering-only is defined by example (a child's authorization.required
becoming the channel's sign-in prompt).

Two claims changed under the trace. "Channels implement the same
rendering twice" was wrong: both transports converge on one proxy step
at the caller's inbox (turn-workflow.ts:297), so rendering happens once
— what exists twice is the forwarding (subagent-adapter.ts vs
session-callback-notification.ts, self-described as its "callback-URL
analog"), with divergent failure semantics. "Nothing redelivers" holds,
but the reason is subtler than "dropped at the producer": producers do
emit from journaled steps, yet delivery failures are deliberately
swallowed before the journal sees them (adapter.ts:246,
session-callback-notification.ts:61), and a producer-side retry could
not fix a deterministic "not pending" anyway.

Also adds an event-indexing open question: the consumer lane is where an
indexer would attach, but v1's best-effort callee wire means a
parent-stream index cannot claim cross-deployment completeness.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
Matches the prototype's event-consumer-write-step: the run owns
receiving, the step owns one delivery's durable effects.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
The dropped-events example named the mechanism (independent POSTs, dead
inbox token) but not the stakes. Tell it as the sign-in story: the
forwarded authorization.required is the sign-in prompt in the caller's
channel, so dropping it while the caller is parked for input (the
hitl-collision shape) means the only person who can unblock the parked
child is never asked; on the completion side the card freezes on
"waiting" after the child already finished.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
…rmed inbox

The dropped-events bullet claimed delivery requires a turn parked at its
inbox. The engine says otherwise: resumeHook durably appends
hook_received to the owning run's log and re-queues it, throwing only
for a missing/disposed hook or a terminal run
(@workflow/core@5.0.0-beta.36, dist/runtime/resume-hook.js) — hook
payloads buffer like any mid-turn delivery.

What actually kills delivery is the address. The callee's callback URL
embeds the caller's continuation token as of dispatch
(remote-agent-dispatch.ts:30), the session rekeys to a fresh token at
every park (workflow-entry.ts:251), and retired hooks are disposed
immediately (session-delivery-hook.ts:37) — so any park between dispatch
and the event 404s the POST, and rotation being one-way means no retry
can ever land it. Rename the bullet to what it is: deliveries are
addressed to a park, not to the session. The consumer's <sessionId>:events
hook is the fix stated in those terms — a session-lifetime address under
which the engine's own buffering finally applies.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
A park is not addressable — it is what a run does when it awaits a hook.
Deliveries are addressed to hooks by token. Restate the drop bullet in
those terms: the session exposes no long-lived hook; its delivery hook is
keyed by the current turn's continuation token, rekeyed at every turn
park, retired hooks disposed. The consumer bullet follows: a hook whose
token is stable for the session's lifetime.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
Name the entity precisely: a sidecar run — auxiliary, 1:1 with the
session, lifecycle-coupled to the entry, deployment-pinned, with its own
journal. State the reason it must be its own run rather than the entry
or a concurrent branch inside it: a workflow wake replays the run's
journal, the entry's journal grows with the session, and the sidecar's
holds only its own deliveries — the extra run is the replay-cost
firewall between event consumption and session history.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
Redraw the caller-side consumer flow and the callee-side deferred
forwarding as sequence diagrams — participant lifelines with directed
messages — instead of the nested-tree sketches.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
The split is by replay cost, not delivery guarantee: every resumeHook
into the main run replays its journal, so high-frequency subagent
progress updates must not ride it, while low-frequency terminal and
control events (session.completed/failed, authorization.*,
input.required) can — and belong on the main run's durable path,
handled in workflowEntry like a local subagent. Corrects the earlier
'normalize into one funnel' framing, which had it backwards: the point
is separation.

Signed-off-by: Rui Conti <ruiconti@gmail.com>
@ruiconti
ruiconti force-pushed the rui/notification-reactors-research branch from c37f21c to b7c269b Compare July 25, 2026 01:55
@ruiconti

Copy link
Copy Markdown
Contributor Author

Reparenting: #1204 was closed and replaced by #1207 (remote authorization via the durable path). Recreating this doc PR on top of #1207.

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