Reactive sessions and workflow events

Recommendation and scope

Start with a small host adapter for invoking an existing operation and observing its result. Reuse Mini App operation dispatch, invocation identity, message folding, and cleanup. Two app hosts already repeat the same admission sequence: the web useAppRuntime hook and the headless debug_app runtime. Extract that shared sequence, prove that a non-app caller can use it, and only then consider a broader reactive-session module.

A reactive session names the caller’s interaction lifetime. It is not a new workflow executor, job scheduler, state store, event broker, or background service. The initial delivery handles one declared operation and finite workflow results. Persistent streaming and durable triggers remain their existing execution paths, with separate adapters only when needed.

App agents and running games are initial consumers. Common code must not depend on conversation models or rendering clocks. The app agents plan owns agent integration and conversation behavior. The shared variables plan owns new typed bindings and revision-checked publication. The control-flow regions plan owns branching and iteration within a workflow.

This review inspected the sources below and existing test cases. It did not execute them or measure realtime performance. Proposed contracts are distinguished from current implementation.

What already exists

Finding Inspected behavior Consequence
F1. The web app host already coordinates reactive work useAppRuntime feeds an input binding into an owned active runner with streamInput. Its reactive path starts a full run first, then uses eligible browser subgraphs or falls back to a full run. During a reactive subgraph run it retains one latest pending trigger in reactive.pending. Its dispatch(action, source?) returns void, and the hook exports no other entry point Extract or adapt the smallest reusable host coordination. Do not rebuild a reactive runner. The pending-trigger behavior is specific to this path, not a universal queue guarantee. A caller outside React cannot await a web-host operation today
F2. App run policy and input capture already exist decideRun returns { kind: "start" }, { kind: "replace"; cancel }, or { kind: "queue"; after }. The web host resolves operation parameters with resolveOperationParams, reserves the invocation synchronously, then cancels or waits with cancelInvocations/awaitSettled Reuse the pure decision and host behavior together. The decision function alone is not a scheduler. Preserve admitted input snapshots
F3. App result folding already correlates messages fold.ts messageToEvents resolves each message through FoldContext.resolveInvocation(job_id) and produces state events. state.ts applyEvent, liveInvocations, and isLiveInvocation apply invocation/output ownership rules Reuse folding and the authoritative app state. Add consumer request/world revision checks around delivery where needed. Existing ownership does not prove a game target is still valid
F4. Streaming workflow execution is available ExecutionSession exposes pushInput, finishInputStream, result, and cancel. Its messages getter works only when the session was created with captureMessages: true. Runner input dispatch passes each value through the input node’s executor, then puts it into the downstream inbox A successful push means input processing/enqueue completed, not that downstream computation or a response completed. The session result ends the whole run. Per-request replies need explicit correlation in graph outputs
F5. Input buffering has one opt-in limit and one default limit NodeInbox awaits capacity in put only when a per-handle bufferLimit is set, and that limit defaults to null. Its correlation pending-key caps default to 10,000 Per-handle buffering is unbounded unless configured. Inspect actual configuration and await behavior before using a high-rate source. Keep this out of the first finite-operation adapter
F6. Durable triggers are an existing subsystem Boot wiring injects DrizzleTriggerInputStore and DrizzleDurableInboxStore. Wakeup delivery stores/deduplicates inputs. Dispatcher starts headless jobs, separates acceptance from completion, and implements parallel/queue/skip Keep durable input ingestion and dispatch here. The wakeup class’s name does not establish general live-run resumption. Its default stores are in memory, while the normal enabled server boot supplies persistent stores. Accepted-run replay has a documented at-least-once crash window
F7. Invocation surfaces have different result contracts Workflow service returns RunWorkflowOutcome. Its background path returns an inline receipt payload with job_id and status: "running", and results come through existing job tools. App-operation execution executeAppOperation reserves by invocation identity and returns AppOperationOutcome. A reused reservation returns reused: true with no messages and no terminal completion Preserve each service’s authority. Reservation/acceptance is not completion. Do not promise a common completion stream until its adapter is verified
F8. A non-React app host already exists HeadlessAppRuntime backs debug_app and nodetool app debug. Its dispatch is async and awaits a run action. It repeats the web host’s admission order: resolve parameters, decideRun, cancel or wait, then runStarted. Its runWorkflow(params) callback takes no abort signal, cancel only marks state, a timeout leaves the invocation live, invocation IDs are local headless-N values, and messages are folded after the run returns Two hosts already duplicate admission. The extraction target is the sequence both repeat, not a third host. This host proves app semantics without React, but it does not prove streamed folding, real cancellation, or server reservation identity. Its debug recording path reaches executeAppOperation through run-recording.ts

The inline graph runner already routes eligible graphs to the browser and others to the existing WebSocket path, returning a result promise and supporting message observation. Use that through the existing app host where it already participates. Bypassing app reservation and accounting to call it directly would not be equivalent app execution.

The app-runtime package is shared across hosts and intentionally dependency-free. Its pure state, policy, and fold functions stay there. Browser transport, React lifecycle, credentials, and server execution must not be imported into that package as part of extraction.

Ownership and integration

Responsibility Authority
Current inputs, variables, and revisions Existing app state or the caller’s game/session state. The adapter samples that owner and does not keep another mutable copy
Which event requests work Declared host binding or specialized behavior. Editing a value alone does not implicitly request execution
Invocation policy and authorized dispatch Existing app/operation host, capability gate, or trigger dispatcher, according to the entry path
Graph computation, job status, usage, artifacts Existing execution owner and run records
Request correlation and consumer lifetime A small caller adapter mapping request identity to existing invocation/job identity and the consumer’s reset generation
Whether a result is still relevant Consumer policy, using target identity and source revision where needed

Reuse the same mechanics for a background action that an app agent turn requests and for a game’s semantic event. The app agent’s turn is itself an app operation run. A game continues ticking and applies an eligible workflow result at its own update point. Neither calls model inference synchronously inside audio or rendering callbacks.

A sequential caller may await a workflow’s terminal result before its next logical step. An interactive caller can observe that same completion asynchronously while continuing unrelated updates. This does not require separate execution modes or a second job.

Values, events, and media streams retain different semantics. Shared input-binding work is not a prerequisite for the first adapter. Use current resolved operation inputs and consumer-owned revision tokens. Audio and texture buffers remain in their dedicated paths, not in an app variable or a general event queue.

Smallest first implementation

The first deliverable is one finite operation invoked from an existing Mini App host and from a minimal non-React caller fixture. Use a delayed deterministic workflow, not a paid provider or a new event source. Do not build a general session API first. Finding, action, and decision IDs are local to this plan and are independent of the app agents plan’s IDs.

Step Concrete work Acceptance
A1. Pin existing host behavior Extend the web app host tests and instance tests and the headless runtime tests with one declared finite operation, delayed result, and reset during execution. Record where the two hosts’ admission sequences differ before extracting code Both fixtures exercise input capture, decideRun, invocation identity, folding, and terminal status. The web fixture also covers server reservation. Existing queue/replace behavior remains covered. No provider call is needed
A2. Expose one caller adapter Neither host offers a usable entry point as is: the web dispatch returns void, and HeadlessAppRuntime has no abort path or streamed folding. Extract the shared admission sequence into a helper with reservation, cancellation, waiting, and execution injected. Keep its pure parts in app-runtime and its effects in the hosts. Add an awaitable completion handle to the helper’s result An event submits the declared operation with a captured input snapshot and consumer generation. The caller receives its existing invocation identity and a terminal result or explicit observation-ended outcome. It may await completion or receive a callback
A3. Prove a second consumer Drive the helper from an agent-free fixed-tick loop built on the game simulation session, calling createGameSession(...).step(input) once per tick. Use A1’s Mini App fixture as the other consumer and share it with app agent work The loop keeps stepping while work runs. A result carries the original request identity. Reset or target change prevents stale application. A result does not implicitly request another run
A4. Integrate without parallel ownership Move the web host and HeadlessAppRuntime onto the extracted helper. Add consumer close/unsubscribe and explicit task-cancellation handling There is one implementation of the admission sequence, one authoritative app/run state, and one counted operation. Failure, duplicate observation, reset, and late completion have defined outcomes. debug_app reports are unchanged for existing fixtures

For the new non-React fixture, allow at most one outstanding request per caller and report busy explicitly for a second request. This limits the first integration, not the existing app’s behavior or all agent tools globally. Do not add a queue or new replacement policy. Existing app operations keep their declared policies. A local consumer admission limit is not a replacement for server budgets or authorization.

The adapter needs only a declared operation reference, captured inputs, request identity, consumer generation, and optional target revision. A target revision tracks relevant input or object changes, not every render tick. Obtain the effective user/instance authority from the existing host, never from an untrusted event payload. Reuse invocation IDs when their scope is sufficient. A new consumer request ID is correlation, not a claim of durable deduplication.

Keep local admission, persisted reservation, and terminal execution distinct. An observer must attach to a reused active invocation’s existing completion/status path instead of treating its reservation as a finished result. Do not automatically retry an effectful request after an uncertain acceptance.

Closing observation invalidates delivery, removes subscriptions, and settles any pending observer wait with an explicit observation-ended outcome. A wait timeout does the same unless the caller explicitly requests job cancellation. Neither outcome claims that execution stopped. Explicit cancellation calls the existing execution owner. Closing a caller does not request job cancellation (D2). Reset must invalidate old observers before new work is admitted. An error or late result can remain in existing run history without being applied to the new consumer.

Detaching or resetting does not free execution capacity for work still running. Preserve the host’s in-flight accounting until settlement, including after a cancellation request. The initial caller limit must not be bypassed by repeatedly resetting its observer.

Use existing promises and messages for finite completion. Neither host exposes a safe completion handle today (F1, F8), so add a narrow handle at the extracted helper’s interface. Do not introduce a global registry, new database table, generic transport, or replacement message fold merely to provide one. Settle the caller once, even if terminal status is observed through more than one existing path.

Decisions for the first adapter

Decision Choice Reason
D1. Where the shared code lives Pure admission steps in app-runtime, effectful steps injected by each host app-runtime must stay dependency-free and shared by web, mobile, and app debug. The hosts keep transport, persistence, and credentials
D2. Whether closing a caller cancels its job No. Close and timeout end observation only. Cancellation is a separate explicit call Matches HeadlessAppRuntime, whose timeout leaves the invocation live. Implicit cancellation on close would turn a navigation or reset into a lost paid result
D3. How the headless host cancels Add an optional abort signal to its runWorkflow callback before claiming cancellation in A4 Today its cancel only marks state, so a cancellation fixture would pass without stopping work
D4. Second-consumer fixture A fixed-tick loop over game-runtime, with no renderer and no live scene commands The simulation is deterministic and already headless. A live scene command channel does not exist and belongs to the app agents plan’s A10

Subsequent adapters, only when needed

Follow-up Reuse Additional proof before enabling
A5. Persistent streaming input Existing app streamInput, ExecutionSession.pushInput, stream completion, and captured messages Confirm the graph/input supports repeated data and is ready. Serialize ingress where ordering is required. Distinguish input enqueue from response completion. Correlate repeated outputs, propagate ingress errors, and verify bounded buffering, end-of-stream, cancellation, and cleanup
A6. Durable trigger source Existing registration, wakeup stores, dispatcher, scheduler, and headless jobs Route completion to an authorized consumer only when needed. Test acceptance versus completion and redelivery without claiming exactly-once side effects. Do not insert a second dispatcher between stored events and execution
A7. Higher-rate interaction Existing app reactive routing and source-appropriate policies Define queue bounds or observation coalescing, stale-result handling, and overflow reporting. Preserve full-run fallback and effect restrictions. Prove that expensive work cannot starve the host loop

A5–A7 do not block the first finite-operation adapter. They are independent integration needs, not a mandatory new runtime platform. The game fixture proves reuse without adding a game event editor or full agent-directed scene actions.

App operations use parallel/queue/replace, trigger registrations use parallel/queue/skip, and the web reactive-subgraph path has its own latest-pending behavior. Keep those meanings explicit. Do not normalize them into a single enum that silently changes cancellation, snapshots, or event loss. Latest-value coalescing is appropriate for replaceable observations, not arbitrary semantic commands.

Trigger scheduling and recovery polling are not render clocks. Same-process dispatcher notification can start accepted work promptly, but that is not a hard realtime guarantee. Streaming pushInput does not establish crash recovery, per-event acknowledgments, or a global total order. State each adapter’s actual delivery and lifetime guarantees.

Verification and plan coordination

Use existing execution, app, and game harnesses. Test unknown/foreign invocation messages, out-of-order completion, input changes after admission, reset, duplicate terminal events, workflow failure, observer disposal, and explicit cancellation. Assert that the fixture inspected a real invocation and that invalid ownership/revision cases fail. Measure timing on target hardware separately from deterministic correctness tests.

The app agents plan may proceed with existing agent and Mini App paths while this adapter is being proven. This plan’s Mini App/game fixtures can likewise proceed without an app agent implementation. Share the finite-operation fixture and host integration. An app agent’s message is the existing chat composer run. Its interrupt-response, transcript, and conversation state remain its own agent binding, not new verbs on the finite-operation adapter. General variables, regions, voice, and durable personal tasks are not prerequisites.

For code changes, follow repository verification requirements, including dependency builds before affected tests and the harness gate. This documentation-only review requires link and text checks, not application test execution.