Continuum Engine

Complete

Milestone 0

Durable Inference

End-to-end streaming inference with Temporal orchestration, tenant-scoped auth, dual-transport push notifications, and PostgreSQL as the durable truth store.

Overview

The foundation everything else builds on.

Milestone 0 wrapped an LLM inference call in durable infrastructure, Temporal for orchestration, Redis Streams for I/O and PostgreSQL for state, without costing the latency that makes streaming useful.

The industry treats durability, observability and tenant isolation as expensive additions, overhead you accept or skip. Measured against the same model call made directly, over 200 runs per arm on two runtimes, durability adds 16 to 21 ms at p50, and its own time outside the call at p99 is 25 to 32 ms: 30 ms, 1.5% of a 658-token answer, on LM Studio. The whole platform, with tracing and tenant isolation, adds 35 to 46 ms at p50 on LM Studio, 1.9% of a 728-token answer. The durable inference benchmark breaks both tests down, stretch by stretch, with the setup to repeat them.

A client opens a session and sends an inference request. The gateway authenticates the tenant, validates the payload, and hands the work to a Temporal workflow. Temporal orchestrates the inference step - calling the LLM through Microsoft.Extensions.AI, streaming tokens back through Redis Streams, and recording every state transition in PostgreSQL. When tokens arrive, the relay service picks them up and pushes them to the client over either raw WebSockets or SignalR, depending on what the consumer supports.

Every piece of this is durable. If the process crashes mid-inference, Temporal replays the workflow from the last checkpoint. If a relay instance goes down, the Redis backplane ensures another instance picks up the broadcast. Sessions persist across restarts. Nothing is lost because nothing is held only in memory.

Architecture

Four layers, one request path.

Each layer has a single responsibility. The boundaries are strict - no layer reaches into another's internals.

Layer 1

Gateway & API

The gateway is a minimal API host - no MVC controllers, no middleware stacks deeper than necessary. It exposes a streaming endpoint at POST /v1/sessions/{id}/infer that accepts chunked input and returns chunked output. Authentication is tenant-scoped: every request is tied to an identity and an isolation boundary.

Sessions are first-class resources with a lifecycle - create, suspend, resume, close. Suspending a session checkpoints its state to PostgreSQL. Resuming rehydrates it. This means a client can disconnect, reconnect from a different device, and continue where they left off. They could even jump between models, change runtimes (mobile vs desktop), or switch from a Python agent to a Rust CLI without losing context.

The industry needs to stop tying sessions to in-memory state on a single server process. That model doesn't survive real-world usage patterns. These are problems we solved twenty years ago - for nearly identical reasons.

OpenTelemetry is wired end-to-end from the first middleware to the last response byte, even though the services are fully decoupled and distributed. Every inference request carries a trace ID through the full pipeline - gateway, orchestration, LLM call, relay broadcast. When something is slow, you can see exactly where.

Layer 2

Compute & Orchestration

Temporal runs the inference workflow. Each inference request becomes a workflow execution with configurable timeouts and retry policies. The workflow calls the LLM through Microsoft.Extensions.AI - an abstraction that supports any OpenAI-compatible backend (LM Studio for local development, cloud providers for production).

Tokens stream back through Redis Streams, scoped per tenant. Each message carries an integrity hash so consumers can detect corruption or replay. State transitions use compare-and-swap semantics - no step can advance unless the previous step completed and the cache is coherent. This eliminates an entire class of race conditions that plague async pipelines.

The runtime also captures reasoning tokens - the model's chain-of-thought - and preserves them alongside the final output. This is the raw material that Milestone 1's Cortex will use to build the knowledge graph. The whole platform around the model call adds 35 to 46 ms to it, so all of this durability and observability doesn't come at the cost of responsiveness.

Layer 3

Relay & Notifications

The relay service is the last mile - it takes tokens from Redis Streams and pushes them to connected clients. Two transports run simultaneously: raw WebSockets for polyglot consumers (Go, Rust, Python - anything that speaks the wire protocol) and SignalR for .NET clients that want automatic reconnection and hub semantics.

Both transports emit the same unified event model. A consumer doesn't need to know which transport delivered the message - the payload is identical. The SignalR hub uses a Redis backplane so multiple relay instances can scale horizontally without sticky sessions. Per-session connection limits with backpressure prevent any single session from monopolizing resources.

Layer 4

Infrastructure & Observability

.NET Aspire orchestrates the full service graph - gateway, Temporal server, workers, relay, PostgreSQL, Redis. Custom Aspire integrations for Temporal and LM Studio mean dotnet run spins up the entire platform locally with health checks and dependency ordering.

PostgreSQL is the durable truth store with managed schema migrations - every deployment applies migrations forward, never backward. Structured logging flows through Serilog with semantic properties, and distributed tracing through OpenTelemetry connects every service in a single trace waterfall.

Integration tests run against live services using Aspire test fixtures - not mocks. The tests spin up real PostgreSQL, real Redis, real Temporal, and exercise the full request path. If the test passes, the code works in production.

What Shipped

Everything below shipped in Milestone 0.

Gateway & API

  • ✓ Streaming inference API - POST /v1/sessions/{id}/infer with chunked transfer
  • ✓ Tenant-scoped authentication and authorization
  • ✓ OpenTelemetry tracing and metrics end-to-end
  • ✓ Session lifecycle management - create, suspend, resume, close

Compute & Orchestration

  • ✓ Temporal orchestration with configurable timeouts and retry policies
  • ✓ LLM inference via Microsoft.Extensions.AI with OpenAI-compatible backends
  • ✓ Redis Streams for tenant-scoped I/O with integrity verification
  • ✓ Compare-and-swap state transitions with cache coherence
  • ✓ Reasoning token capture - model chain-of-thought preserved in context
  • ✓ Temporal adds 16-21 ms to a raw model call at p50, and its own time at p99 is 25-32 ms (200 runs per arm, measured September 2026)

Relay & Notifications

  • ✓ Raw WebSocket transport for polyglot consumers
  • ✓ SignalR hub with Redis backplane for horizontal scaling
  • ✓ Per-session connection limits with backpressure
  • ✓ Unified event model across both transports

Infrastructure

  • ✓ .NET Aspire orchestration across all services
  • ✓ Custom Aspire integrations for Temporal and LM Studio
  • ✓ PostgreSQL with managed schema migrations
  • ✓ Structured logging and distributed tracing
  • ✓ Integration tests against live services via Aspire test fixtures

Design Decisions

Why it's built this way.

▶ Why Temporal over raw message queues?

An inference pipeline's steps depend on each other: authenticate, validate, call the LLM, stream tokens, record state. A queue can deliver each message, but it can't replay a half-completed workflow from a checkpoint when a process crashes mid-inference.

Temporal gives you durable execution: if the worker dies after the LLM returns tokens but before they're written to PostgreSQL, Temporal replays the workflow from the last successful activity. No manual retry logic, no dead letter queues, no state reconciliation code. The workflow definition is the retry logic.

The tradeoff is operational complexity - running Temporal is heavier than running RabbitMQ. But for a platform where durability is the core value proposition, that tradeoff is worth it from day one.

▶ Why dual-transport - WebSocket and SignalR?

SignalR is excellent for .NET consumers - automatic reconnection, hub method routing, and built-in Redis backplane support. But it's a .NET-native protocol. If you're building a Python agent, a Rust CLI tool, or a Go service that consumes inference tokens, you don't want to pull in a SignalR client library just to read a stream.

Raw WebSockets are the universal transport. Any language with a socket library can consume the stream. The two transports share a unified event model - the same RelayEvent payload regardless of delivery mechanism. Consumers pick the transport that fits their ecosystem without sacrificing any functionality.

▶ Why Redis Streams for I/O?

Redis Streams give you an append-only log with consumer groups - which is exactly what token streaming needs. Each tenant's inference output writes to a dedicated stream. Multiple consumers (relay instances) can read from the same stream without interfering with each other.

The alternative was piping tokens directly from the worker to the relay over gRPC or HTTP. That works until the relay instance isn't available - then you need a buffer, retry logic, and delivery guarantees. Redis Streams already provide all of that. The stream is the buffer, consumer groups handle delivery, and the data persists until explicitly trimmed.

Each message carries an XxHash3 integrity hash - lightweight, non-cryptographic, and cheap enough to verify on every read. If a message is corrupted or replayed incorrectly, the consumer detects it immediately.

▶ Why compare-and-swap state transitions?

An inference pipeline has ordered steps: pending, executing, streaming, completed. In a concurrent system with retries, two workers can both try to advance the same step. Without protection, you get duplicate token streams, double writes, or a step that skips ahead.

Compare-and-swap (CAS) makes each transition atomic - a step can only advance if its current state matches what you expect. If another worker already moved it, your CAS fails and you know to back off. Combined with cache invalidation on every successful transition, the system maintains coherence without distributed locks.

▶ Why PostgreSQL as the truth store?

PostgreSQL is the most boring correct choice - and that's exactly why. Session state, inference history, step transitions, tenant configuration - all of this is relational data that benefits from ACID transactions, foreign keys, and a query planner with decades of production behind it.

The alternative was splitting state across multiple stores early - Redis for sessions, a document store for inference history, something else for configuration. That creates consistency problems between stores on day one. PostgreSQL handles all of it in a single transactional boundary. When scale demands splitting, you split with data to guide the decision.

▶ What does durability cost a model call?

The conventional wisdom is that infrastructure adds latency. Orchestration, durability, observability - every layer is supposed to cost you, and a workflow engine is supposed to be too complicated and too slow for a team that has to move fast.

Measured against the same model call made directly, over 200 runs per arm on two runtimes, Temporal adds 16 to 21 ms at p50, and its own time outside the call at p99 is 25 to 32 ms. The whole platform, with tenant isolation, distributed tracing and replay behind it, adds 35 to 46 ms at p50 on LM Studio, and the model call inside the worker takes as long as the direct one. The milliseconds are fixed, so a short answer carries a larger share: 5.9% of an 88-token answer for durability, and 11.5% of an 80-token one for the whole platform.