BrandGhost
Vercel Eve Sessions and Streaming in C#: Recover a Disconnected Client

Vercel Eve Sessions and Streaming in C#: Recover a Disconnected Client

Vercel Eve sessions separate a durable conversation from the C# process watching it: the client preserves a session identifier and a stream cursor so another handle can resume consumption, according to the tagged session documentation. A disconnected screen does not necessarily mean the agent stopped. Recovery starts by finding the same conversation, not by sending the original prompt again.

Imagine a fictional maintenance assistant explaining an inspection note. Your application displays some output, loses its connection, and restarts. The reader's job is simple: recover what was missed without confusing another message with another read. This article follows that job from initial consumption to a bounded catch-up.

I maintain NexusLabs.Eve, the independent .NET client used in these examples. It is not an official Vercel SDK.

These are statically source-reviewed examples, not newly compiled or executed demonstrations. Each C# block is a separate console Program.cs, using .NET 10, C# 14, nullable enabled, and NexusLabs.Eve version 0.1.0-alpha-0014, whose package metadata identifies the alpha package. The pinned public fixture uses Eve 0.63.0 and Node 24.x (fixture manifest). Replace the reserved example host with an accessible deployment; service credentials and deployment setup are outside this recovery explanation.

Vercel Eve Sessions Have Identity and Read Progress

The client state contains SessionId, the runtime-owned conversation identifier, and StreamIndex, the absolute number of consumed events (state record). Those fields answer different questions. Identity tells you which conversation to address. Progress tells you where this reader should begin.

For Vercel Eve sessions, persisting only identity preserves the ability to find the conversation, but not the reader's place. Persisting only a number is worse: event twelve has no useful meaning without its conversation. Store the pair as one application record, alongside whatever ownership information your application needs.

The same conversation can contain several turns; the pinned runtime describes turns as user-triggered work within a durable session (execution model). A session cursor is therefore not a turn identifier. Nor is it a receipt proving that one specific user delivery was accepted.

The response's DeliveryId identifies an accepted message delivery and may be absent for initial creation or human-input responses (response source). Keep delivery identity, turn identity, and reader progress conceptually separate. A screen reopening is a read operation; it should not invent another delivery.

A useful ownership rule is one consumption coordinator per saved checkpoint. Several screens may receive presentation updates from that coordinator. Letting each screen independently overwrite the same state record risks stale saves and ambiguous progress. This is an application design constraint, not a claim that the transport provides a distributed lock.

Consume a Turn Before Saving Its Final Cursor

SendAsync returns an accepted response whose event stream has one owner; GetOutcomeAsync aggregates that stream, while direct enumeration is an alternative, not a second pass (response implementation). Decide which path owns a response before handing it to other application code.

For a small answer, aggregation is the simpler learning path. It gives you an explicit outcome without building a live display reducer. The tradeoff is that it retains the consumed events in memory, as the aggregation implementation shows. It is not a constant-memory log processor.

This first example creates a conversation, saves the accepted identity, consumes its answer, and then saves the advanced state using the actual public record API (session source, state source).

using System;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using NexusLabs.Eve;

using HttpClient transport = new();
EveClient client = new(
    transport, new EveClientOptions("https://agent.example.com"));
EveSession session = client.CreateSession();
EveMessageResponse response = await session.SendAsync(
    "Explain the fictional inspection note for pump-demo-01.",
    CancellationToken.None);

await File.WriteAllTextAsync(
    "eve-session.json", JsonSerializer.Serialize(session.State));

EveTurnOutcome outcome = await response.GetOutcomeAsync(CancellationToken.None);

await File.WriteAllTextAsync(
    "eve-session.json", JsonSerializer.Serialize(session.State));

if (outcome.Status == EveTurnStatus.Failed)
{
    Console.Error.WriteLine($"Session {outcome.SessionId} failed.");
    foreach (EveStreamEvent streamEvent in outcome.Events)
    {
        if (streamEvent.Kind == EveStreamEventKind.SessionFailed)
        {
            Console.Error.WriteLine(streamEvent.Data.GetRawText());
        }
    }

    Environment.ExitCode = 1;
    return;
}

Console.WriteLine($"Observed status: {outcome.Status}");
Console.WriteLine(outcome.Message ?? "No completed message was observed.");

The JSON file is the persistence contract for this illustration. There is no undefined SaveAsync abstraction. Both programs below use the same file in their working directory. File errors surface rather than being disguised as successful recovery.

The first save reduces the window in which accepted identity exists only in memory. It does not remove that window: a process can stop between server acceptance and local persistence. The second save records read progress after aggregation. These are two different checkpoints with two different purposes.

Think through an interruption at each point. Before acceptance, your application may not know whether the server received anything. After acceptance but before the first save, the returned identity can be lost with the process. After that save, another process can at least locate the conversation. After consumption but before the final save, the older cursor may cause already observed events to be read again. This timeline is a conceptual analysis of the example's ordering, not a guarantee that every failure has an automatic remedy.

For Vercel Eve sessions, that last case is often acceptable for a diagnostic viewer: redisplaying historical information is inconvenient but does not itself change equipment records. It becomes a different problem if displaying an event also sends a notification or writes a business record. Keep the reader's checkpoint separate from those effects so a harmless replay does not acquire hidden consequences.

For production Vercel Eve sessions, ordinary file replacement is not a complete durable-store design. Protect access, use an appropriate crash-safe write strategy, and coordinate competing writers. A truncated file must produce a recovery error, not silently become a new conversation. The example exposes these responsibilities instead of pretending a serialization call solves them.

Resume Vercel Eve Sessions Without Resending the Prompt

CreateSession(savedState) restores a handle, whereas AttachSession(sessionId, streamIndex) constructs one from explicit identity and progress (client factories). Neither method needs to send a new user message merely to recover a reader.

Use the saved-state route when your application stored the full checkpoint. The following program reads that checkpoint and follows events until its local time budget expires; StreamAsync reads the conversation rather than submitting another turn (session streaming API).

using System;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using NexusLabs.Eve;

EveSessionState saved = JsonSerializer.Deserialize<EveSessionState>(
    await File.ReadAllTextAsync("eve-session.json"))
    ?? throw new InvalidDataException("Missing Eve session state.");

if (string.IsNullOrWhiteSpace(saved.SessionId))
{
    throw new InvalidDataException("The checkpoint has no remote session ID.");
}

using HttpClient transport = new();
EveClient client = new(
    transport, new EveClientOptions("https://agent.example.com"));
EveSession resumed = client.CreateSession(saved);
using CancellationTokenSource readBudget = new(TimeSpan.FromSeconds(30));

try
{
    await foreach (EveStreamEvent streamEvent in
        resumed.StreamAsync(readBudget.Token))
    {
        Console.WriteLine($"{streamEvent.Type}: {streamEvent.Data.GetRawText()}");
        if (streamEvent.Kind == EveStreamEventKind.SessionFailed)
        {
            Console.Error.WriteLine("The durable session reported failure.");
            Environment.ExitCode = 1;
        }
    }
}
catch (OperationCanceledException) when (readBudget.IsCancellationRequested)
{
    Console.Error.WriteLine("Local reading stopped; server work was not cancelled.");
}

await File.WriteAllTextAsync(
    "eve-session.json", JsonSerializer.Serialize(resumed.State));

This deliberately displays raw diagnostic events rather than reconstructing a chat interface. Use synthetic data when doing that; production payloads may contain information that should not enter logs. The catch is limited to this caller's cancellation. HTTP failures, malformed protocol responses, and persistence failures remain visible.

The restored checkpoint belongs to a particular observer, not necessarily every observer of the same conversation. If an operator console and a background audit reader have different responsibilities, give each an explicit progress record rather than letting the faster reader decide what the slower one has seen. This is conceptual ownership guidance. The examples assume a single process writes the demonstration file and do not implement multi-reader coordination.

Vercel Eve sessions also require an honest recovery message. "Reading stopped" describes the local budget expiring. "The session failed" describes a durable event. "Checkpoint could not be loaded" describes local persistence. Keeping those statements distinct helps the next caller decide whether to retry reading, investigate the saved record, or inspect the agent's failure, rather than starting a replacement conversation blindly.

There is an important tradeoff for Vercel Eve sessions here. Following supports live progress, but it can wait for future work and needs a caller lifetime. A bounded backlog read gives control back after a fixed historical target. Choose according to whether the screen is observing a live conversation or rebuilding its initial view.

Do not send "continue" just because the connection dropped. If work is still active, a new message changes the conversation. The pinned client documents steering as the server default for overlapping sends, with EveTurnPolicy.Queue available when explicit queuing is intended (delivery policy). Recovery reading and intentional follow-up messaging should remain separate actions.

Catch Up to a Fixed Durable Tail

Setting Follow = false reads through the durable tail observed when the stream opens, then finishes instead of waiting for future events (bounded streaming contract). This is a snapshot boundary for consumption, not a request to stop server execution.

Here is the alternative explicit-attachment path for Vercel Eve sessions. Supply a trusted session identifier and a nonnegative cursor as the two command-line arguments; AttachSession validates the handle's cursor contract (factory source).

using System;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using NexusLabs.Eve;

if (args.Length != 2 || string.IsNullOrWhiteSpace(args[0])
    || !int.TryParse(args[1], out int cursor) || cursor < 0)
{
    throw new ArgumentException("Supply a session ID and a nonnegative cursor.");
}

using HttpClient transport = new();
EveClient client = new(
    transport, new EveClientOptions("https://agent.example.com"));
EveSession session = client.AttachSession(args[0], streamIndex: cursor);

await foreach (EveStreamEvent streamEvent in session.StreamAsync(
    new EveStreamOptions { Follow = false },
    CancellationToken.None))
{
    Console.WriteLine($"{streamEvent.Type}: {streamEvent.Data.GetRawText()}");
    if (streamEvent.Kind == EveStreamEventKind.SessionFailed)
    {
        Console.Error.WriteLine("Catch-up observed a failed session.");
        Environment.ExitCode = 1;
    }
}

await File.WriteAllTextAsync(
    "eve-session.json", JsonSerializer.Serialize(session.State));
Console.WriteLine($"Saved cursor: {session.State.StreamIndex}");

The first bounded request negotiates x-eve-stream-tail-index; reconnects keep that initial upper bound rather than chasing a moving tail (bounded read documentation). Events recorded afterward belong to another read. That gives an application a meaningful point at which backlog reconstruction is complete.

A bounded read still needs a valid protocol response. Missing or malformed tail metadata throws EveProtocolException, and negative tail-relative starts cannot be combined with Follow = false (bounded read constraints). "Bounded" describes the event target, not a guaranteed wall-clock completion time.

For Vercel Eve sessions in an application screen, the resulting checkpoint can mark "caught up through the observed tail." It should not be labeled "agent finished" merely because the reader returned. Conversely, an empty catch-up may simply mean this reader was already current. Neither observation proves that a new user request succeeded or that a business action was performed.

Reconnection Is Transport Recovery, Not an Application Transaction

The client already reconnects from the next absolute event index, using its stream policy and a fifteen-second idle read deadline; attached reads have a finite idle reconnect budget, while active response consumption follows its turn until a boundary or caller cancellation (reconnection documentation). Do not replace that mechanism with an unbounded custom retry loop.

For Vercel Eve sessions, distinguish three layers of progress: bytes received, complete events decoded, and application effects committed. The reconnect cursor follows fully consumed events, not arbitrary byte counts (stream recovery contract). An interrupted partial NDJSON line is not a complete event checkpoint and can be encountered again after reconnection.

The public session source also shows a crucial implementation detail: direct StreamAsync advances handle state before yielding an event, whereas active response state merges its counted progress when enumeration unwinds (cursor implementation). Therefore "consumed" must not be interpreted as "my database transaction committed." Saving session.State inside a handler is not automatically an acknowledgement protocol.

Conceptually, a handler could write a notification and crash before saving its checkpoint. Replaying may duplicate that notification. Saving progress first creates the opposite risk: a crash may skip an effect that never completed. A business operation record, transaction boundary, or idempotency key must solve that problem at the application boundary.

This is also why adding another generic HttpClient resilience policy deserves care. Retrying a read and repeating a message submission are not interchangeable. Decide which layer owns recovery before multiplying attempts.

Persisted stream events have stable Metadata.Id values when the protocol supports them, but older events may have no ID; retried execution emits new events rather than replaying the same identity (event identity documentation). Event deduplication cannot establish exactly-once external effects.

The supplied EveStreamEventDeduplicator remembers an unbounded set so rewinds remain recognizable (deduplicator contract). That has a memory cost over long-lived consumption. A bounded set saves memory but forgets older identities; a persisted effect ledger needs its own retention policy. None should be presented as a free, permanent guarantee.

Deltas and Waiting Events Need Context

The pinned stream carries NDJSON events, and text append payloads contain deltas rather than full accumulated state (protocol source, delta contract). General HttpClient streaming concepts help explain incremental reading, but an SSE parser is not the Eve wire contract.

For Vercel Eve sessions, reopening midway through a message can therefore require presentation reconstruction. A suffix of append events is not a complete answer. Keep a compatible display checkpoint or rebuild from an appropriate earlier position, understanding the additional read and memory costs.

The pinned documentation requires contiguous text deltas with matching UTF-16 offsets and treats validated tool input as authoritative rather than an unfinished input preview (streaming guidance). These examples avoid inventing a partial UI reducer. A raw event log and a finished chat bubble are different outputs.

Waiting also needs context: callback authorization may produce an interim session.waiting while the active response remains attached for completion (authorization parking). This matters even if authentication is implemented elsewhere.

Do not manually break an active response loop on IsCurrentTurnBoundary: the property is context-free and remains true for that interim waiting event, while the response applies pending-authorization context (boundary warning). Let the existing response owner decide when its stream ends.

Local Cancellation and Remote Cancellation Are Different Decisions

Cancelling local consumption detaches the caller without itself stopping the durable turn (error and cancellation contract). A closed page and an explicit stop request therefore express different intentions for Vercel Eve sessions.

The final example intentionally requests remote cancellation, rather than simulating a disconnected client. Start the one outcome consumer first; response-level CancelAsync waits for its observed turn identity and sends a guard for that exact turn (cancellation implementation).

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using NexusLabs.Eve;

using HttpClient transport = new();
EveClient client = new(
    transport, new EveClientOptions("https://agent.example.com"));
EveSession session = client.CreateSession();
EveMessageResponse response = await session.SendAsync(
    "Explain the fictional pump inspection in detail.",
    CancellationToken.None);

Task<EveTurnOutcome> outcomeTask =
    response.GetOutcomeAsync(CancellationToken.None);

EveCancellationOutcome cancellation =
    await response.CancelAsync(CancellationToken.None);
EveTurnOutcome outcome = await outcomeTask;

Console.WriteLine(cancellation);
Console.WriteLine($"Observed boundary status: {outcome.Status}");

if (outcome.Status == EveTurnStatus.Failed)
{
    Console.Error.WriteLine("The session failed while cancellation was observed.");
    Environment.ExitCode = 1;
}

Cancellation is cooperative, and a settled response returns NoActiveTurn without another cancellation request (response cancellation contract). The example does not promise that cancellation wins a race with completion. It prints the observed result and keeps consuming through settlement.

When only an attached handle and an observed turn ID remain, the guarded session.CancelAsync(turnId, cancellationToken) overload is available (session cancellation API). Do not substitute the session cursor for that guard or reuse a cancelled local-reading token when intending to send a new remote request.

Finally, EveTurnStatus.Failed represents a streamed failure, while unsuccessful HTTP calls throw EveClientException and malformed successful responses throw EveProtocolException (error taxonomy). Caller cancellation belongs to another branch. Reporting all four as "no answer" hides the recovery decision.

For operational visibility, structured logging in .NET can record safe identifiers and observed failure categories. Outbound HTTP tracing adds transport context, but one request span is not the entire durable conversation.

Frequently Asked Questions

These questions focus on recovering a reader, not administering the runtime or changing its context.

Does reconnecting start another turn?

Reading with StreamAsync attaches to session events, whereas SendAsync submits a message (session API). Recover Vercel Eve sessions by reading first, not automatically resending.

Can I aggregate a response after enumerating it?

No. The response enforces single-use consumption and throws if consumed again (response source). Choose aggregation or direct enumeration for that response.

Does a saved cursor prove my handler committed?

No. Direct session enumeration advances local state before yielding the event (cursor source). Application effects need a separate commit and replay strategy.

Does bounded catch-up stop the agent?

No. Follow = false fixes the read's tail boundary, not the server's execution lifetime (bounded reads). Later events can be consumed separately.

Should every waiting event end my active response loop?

No. Callback authorization can park an active response at an interim waiting event, and the client applies that context (waiting guidance). A manual boundary break loses that behavior.

Does cancelling the reader cancel the server turn?

No. Local cancellation detaches consumption; cooperative remote cancellation is separate (cancellation distinction). Preserve this distinction in both UI language and failure reporting.

Keep the Conversation and the Reader Separate

Recovering Vercel Eve sessions is easier when identity, transport progress, presentation state, and business effects remain separate. Restore the conversation handle. Let the library recover transport interruptions. Choose live following or fixed-tail catch-up. Treat a cursor as read progress, not a guarantee about application transactions.

That mental model explains the central distinction: a reader can stop while durable work continues. Returning to that work should recover observation before changing the conversation.

Vercel Eve Tutorial for C# Developers: Build an ASP.NET Core Agent Client

Follow this Vercel Eve tutorial to connect ASP.NET Core to a pinned agent, manage transport ownership, and read equipment summaries with explicit failures.

Weekly Recap: AI Agents, C# Pipelines, and EF Core Performance [Oct 2026]

Explore durable AI agents, practical AI development workflows, and free LLM tradeoffs. Plus C# pipeline benchmarks, EF Core performance, RAG security, and career advice.

Vercel Eve Explained: Durable AI Agents and C# Integration

Understand Vercel Eve's durable agent runtime, filesystem-first design, replay boundaries, and how C# applications connect through a version-pinned HTTP client.

An error has occurred. This application may no longer respond until reloaded. Reload