# How Session Pinning State Is Persisted and Invalidated in WorkWeave Router

> Discover how WorkWeave Router persists session pinning state in PostgreSQL and invalidates pins via TTL expiration error thresholds degenerate response detection loop detection and policy deadline violations.

- Repository: [Weave/router](https://github.com/workweave/router)
- Tags: internals
- Published: 2026-08-30

---

**WorkWeave Router persists session pinning state in a PostgreSQL table using a store facade with `Get`, `Upsert`, and `Clear` operations, while invalidating pins through TTL expiration, consecutive error thresholds, degenerate response detection, loop detection, or policy-deadline violations.**

WorkWeave Router maintains multi-turn conversation coherence by "pinning" specific models to user sessions. According to the WorkWeave Router source code, this session pinning state is durably stored in PostgreSQL and automatically invalidated when health signals indicate the pin is stale, erroneous, or otherwise undesirable.

## PostgreSQL Storage Schema

The underlying persistence layer relies on a table named `session_pins` defined in the migration file [`db/migrations/0010_session_pin_last_served_model.up.sql`](https://github.com/workweave/router/blob/main/db/migrations/0010_session_pin_last_served_model.up.sql). This schema stores the correlation between an installation, a role, and the selected model.

Key columns include:

- `installation_id` and `role` — the composite key identifying the conversation context
- `last_served_model` — the pinned model identifier
- `expires_at` — timestamp for TTL-based eviction
- `action_history` — a **JSONB** column capturing runtime attributes such as consecutive error counters, usage statistics, and TTL metadata

## The Session Pin Store Interface

The router interacts with persistence through an abstraction defined in [`internal/router/sessionpin/store.go`](https://github.com/workweave/router/blob/main/internal/router/sessionpin/store.go). This store acts as a thin I/O façade over the concrete repository implementation.

The interface exposes three primary operations:

- **`Get(ctx, installationID, role)`** — retrieves the current pin if it exists
- **`Upsert(ctx, pin)`** — inserts a new pin or updates an existing one using PostgreSQL's `ON CONFLICT ... DO UPDATE` semantics
- **`Clear(ctx, installationID, role)`** — removes the pin entirely

The concrete SQL implementation resides in [`internal/postgres/session_pin_repo.go`](https://github.com/workweave/router/blob/main/internal/postgres/session_pin_repo.go), where `Upsert` executes an `INSERT ... ON CONFLICT (installation_id, role) DO UPDATE` statement to atomically persist state changes.

## Loading and Persisting Pins

When processing a request, the router loads the existing pin to determine if a model has already been selected for the session. If the routing logic decides to pin a model—whether via the `/force-model` endpoint, a turn-type heuristic, or the HMM side-car—it writes the state back through the store.

**Loading an existing pin:**

```go
pin, ok, err := router.SessionPinStore.Get(ctx, installationID, role)
if err != nil {
    // Handle repository error
    return err
}
if ok {
    // Pin exists: proceed with pin.Model, check pin.ExpiresAt, etc.
    selectedModel = pin.Model
}

```

**Creating or updating a pin:**

```go
pin := router.SessionPin{
    InstallationID: installationID,
    Role:           role,
    Model:          chosenModel,
    ExpiresAt:      time.Now().Add(pinTTL),
    ActionHistory:  updatedHistoryJSON,
    // ... additional metadata
}

if err := router.SessionPinStore.Upsert(ctx, pin); err != nil {
    log.Error("session pin upsert failed", "err", err)
}

```

## State Evolution and Runtime Attributes

After a pin is loaded, the router enriches it with ephemeral runtime data stored in the `action_history` JSONB column. This allows the pin to accumulate health signals across multiple turns.

The helper logic in [`internal/router/percallband/state_snapshot.go`](https://github.com/workweave/router/blob/main/internal/router/percallband/state_snapshot.go) and [`features.go`](https://github.com/workweave/router/blob/main/features.go) updates this blob after each turn, tracking:

- **Consecutive error counters** for upstream failure detection
- **Usage statistics** such as request frequency and model utilization
- **TTL boundaries** derived from policy configuration

This mutable state snapshot enables the router to make eviction decisions based on historical behavior rather than single-turn heuristics.

## Invalidation and Eviction Policies

WorkWeave Router implements five distinct invalidation mechanisms that can clear a session pin:

**TTL Expiration** — The [`internal/proxy/pin_eviction.go`](https://github.com/workweave/router/blob/main/internal/proxy/pin_eviction.go) file checks the `expires_at` column against the current time. When the deadline passes, the pin is considered stale and eligible for eviction.

**Consecutive Upstream Errors** — The `evictPinAfterDegenerateResponse` function monitors the error counter stored in `action_history`. After a configurable threshold of non-retryable errors, the pin is removed to prevent repeated routing to a failing model.

**Degenerate Response Detection** — If a model returns low-confidence output, repetitive text, or other classifications defined as "degenerate," the router triggers the same `evictPinAfterDegenerateResponse` path to invalidate the pin.

**Loop Detection** — The turn-loop logic in [`internal/proxy/turnloop.go`](https://github.com/workweave/router/blob/main/internal/proxy/turnloop.go) analyzes conversation patterns for repetition cycles. When a loop is detected, the system clears the pin to force a fresh model selection on the next turn.

**Policy-Deadline Fallback** — In [`internal/proxy/service.go`](https://github.com/workweave/router/blob/main/internal/proxy/service.go), when the HMM side-car misses its decision deadline, the router evaluates the existing pin's freshness. If the pin exceeds policy-defined staleness thresholds (the "kill-switch" logic), the system drops the pin and falls back to default routing.

When any eviction rule fires, the store either calls `Upsert` with an empty pin state or invokes `Clear` to delete the row, effectively removing the persisted state.

## Asynchronous Write-Back Behavior

After a successful turn completes, the router writes the updated pin state back to PostgreSQL. This write-back includes the latest `last_served_model`, incremented usage counters, refreshed TTL values, and updated `action_history`.

Errors during this phase are logged but do not abort the user request. As implemented in [`internal/proxy/service.go`](https://github.com/workweave/router/blob/main/internal/proxy/service.go):

```go
observability.Get().Error("session pin usage writeback failed", 
    "installation_id", installationID,
    "role", role,
    "error", err)

```

This fire-and-forget approach ensures that database latency or transient failures do not impact response times, at the cost of potentially losing the most recent usage statistics.

## Summary

- Session pins are stored in a PostgreSQL `session_pins` table with a composite key of `installation_id` and `role`.
- The [`internal/router/sessionpin/store.go`](https://github.com/workweave/router/blob/main/internal/router/sessionpin/store.go) façade provides `Get`, `Upsert`, and `Clear` methods, backed by [`internal/postgres/session_pin_repo.go`](https://github.com/workweave/router/blob/main/internal/postgres/session_pin_repo.go).
- Runtime state including error counts and usage stats accumulates in the JSONB `action_history` column via helpers in [`internal/router/percallband/state_snapshot.go`](https://github.com/workweave/router/blob/main/internal/router/percallband/state_snapshot.go).
- Invalidation triggers include TTL expiration ([`pin_eviction.go`](https://github.com/workweave/router/blob/main/pin_eviction.go)), consecutive errors, degenerate responses, loop detection ([`turnloop.go`](https://github.com/workweave/router/blob/main/turnloop.go)), and policy-deadline violations ([`service.go`](https://github.com/workweave/router/blob/main/service.go)).
- Write-back operations occur asynchronously after request completion, with errors logged non-fatally.

## Frequently Asked Questions

### How does WorkWeave Router store session pinning state?

WorkWeave Router stores session pinning state in a PostgreSQL table named `session_pins`, using columns for the installation ID, role, selected model, expiration timestamp, and a JSONB blob for runtime metadata. The [`internal/postgres/session_pin_repo.go`](https://github.com/workweave/router/blob/main/internal/postgres/session_pin_repo.go) file implements the actual SQL `SELECT` and `UPSERT` logic, while [`internal/router/sessionpin/store.go`](https://github.com/workweave/router/blob/main/internal/router/sessionpin/store.go) provides the abstracted interface used by the proxy service.

### What triggers the invalidation of a session pin?

Session pins are invalidated by five primary mechanisms: TTL expiration when `expires_at` passes, consecutive upstream errors exceeding configured thresholds, detection of degenerate model responses, conversation loop detection in [`internal/proxy/turnloop.go`](https://github.com/workweave/router/blob/main/internal/proxy/turnloop.go), and policy-deadline violations when the HMM side-car fails to respond within acceptable windows.

### What happens if the session pin write-back fails?

If the asynchronous write-back operation fails after a successful turn, the error is logged via the observability package in [`internal/proxy/service.go`](https://github.com/workweave/router/blob/main/internal/proxy/service.go) but the user request completes normally. This design prioritizes low latency over strict consistency, meaning the pin state may lag behind reality until the next successful write or until the pin is evicted and recreated.

### Can session pins be cleared manually?

Yes, explicit clearing is supported through the store's `Clear` method, which removes the row from PostgreSQL. This is typically invoked via administrative endpoints such as `/unforce-model`, which allows operators or automated systems to reset routing decisions for specific installation-role pairs without waiting for TTL expiration or error-based eviction.