docs(eve): research - event consumers plan#1208
Open
ruiconti wants to merge 17 commits into
Open
Conversation
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>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Research plan for #1170. Top of the stack:
main → #1202 (terminal envelope) → #1207 (remote authorization, durable path) → this. Design only, no runtime changes. Recreated after the #1204→#1207 replacement reparented the stack.The core principle (stated plainly)
Two lanes exist for one reason: keep high-frequency subagent progress updates out of the main workflow run's replay journal. Every
resumeHookinto the main run replays its journal (bounded by the engine's replay budget, growing with the session).session.completed/failed,authorization.*,input.required)resumeHook(continuationToken), durable, handled inworkflowEntrylike a local subagentsubagent.event, off the main run's journalThe split is by replay cost, not delivery guarantee. Low-frequency control events — including authorization — ride the durable main-run path (implemented in #1202 for terminal, #1207 for authorization). Only high-frequency progress needs the separate lane, and that lane is reserved until a concrete progress event type exists.
Note on the iteration history
The commits preserve the design's evolution — including a per-session sidecar consumer explored before it was clear that (a) authorization is control, not progress, so it belongs on the main run, and (b) the separate lane's sole justification is progress replay cost. The sidecar is retained in the doc as the graduated tier for if/when a progress event class needs cursor semantics or consumer-held state.