# How Caveman Mode State is Managed Per Session in Claude Code

> Discover how Caveman mode state is managed per session in Claude Code. Learn about its session-specific lifecycle and lack of cross-invocation persistence for efficient agent runs.

- Repository: [Julius Brussee/caveman](https://github.com/JuliusBrussee/caveman)
- Tags: internals
- Published: 2026-09-06

---

**Caveman mode state is determined once per session at run start and exists only for the lifetime of that Claude agent run, with no persistence across invocations.**

The Caveman SDK implements a **stateless, per-run mode selection** system for Claude Code integrations. Each time `runClaudeAgent` executes, it generates a fresh session identifier, evaluates gateway availability, and locks the mode—either `"optimized"` or `"observe-only"`—for that specific run. This design ensures predictable behavior without cross-session contamination, as implemented in [`JuliusBrussee/caveman`](https://github.com/JuliusBrussee/caveman).

## Session ID Generation: The Foundation of Per-Session State

Every Claude agent run receives a unique session identifier constructed from three components: the agent definition ID, a cryptographically random UUID, and a `claude-` prefix.

In [`packages/agent/src/claude-runtime.ts`](https://github.com/JuliusBrussee/caveman/blob/main/packages/agent/src/claude-runtime.ts), lines 138-140, the session ID is assembled as follows:

```typescript
const runID = crypto.randomUUID();
const sessionID = `claude-${definition.id}-${runID}`;

```

This construction guarantees **uniqueness across parallel executions** while making the session traceable to its originating agent definition. The `sessionID` becomes the anchor for all subsequent mode decisions and header propagation.

## Gateway Negotiation: How Mode Gets Determined

The actual mode selection happens through `resolveCaveRoute`, which evaluates two inputs: the caller-provided `gatewayURL` and the `cave` option (`"auto"` or `"off"`).

From [`packages/agent/src/claude-runtime.ts`](https://github.com/JuliusBrussee/caveman/blob/main/packages/agent/src/claude-runtime.ts), lines 35-36:

```typescript
const { useGateway } = await resolveCaveRoute(gatewayURL, options, false);

```

The function returns a boolean flag that directly controls downstream behavior. No complex state machine or persistent configuration is consulted—just this single negotiation at run start.

## Mode Assignment: Optimized vs. Observe-Only

With the gateway negotiation complete, the `RunResult.mode` field is set unconditionally in [`packages/agent/src/claude-runtime.ts`](https://github.com/JuliusBrussee/caveman/blob/main/packages/agent/src/claude-runtime.ts), lines 83-84:

```typescript
mode: useGateway ? "optimized" : "observe-only",

```

- **`"optimized"`** — The local gateway is reachable; requests are compressed, cached, and routed through the Caveman gateway
- **`"observe-only"`** — The gateway is unavailable or disabled (`cave: "off"`); the SDK connects directly to the LLM provider with no compression

This binary assignment is **immutable for the run's duration**. There is no mid-run mode switching or dynamic renegotiation.

## Explicit Stateless Design: No Session Persistence

Critically, Caveman enforces statelessness by hardcoding `persistSession: false` in [`packages/agent/src/claude-runtime.ts`](https://github.com/JuliusBrussee/caveman/blob/main/packages/agent/src/claude-runtime.ts), line 213:

```typescript
persistSession: false,

```

This flag ensures that:

- The `sessionID` is discarded after `runClaudeAgent` returns
- Mode selection is re-evaluated fresh on every invocation
- No local storage, cookies, or files retain session information

Each call to `runClaudeAgent` is therefore **hermetic**—previous runs cannot influence subsequent ones, and concurrent runs operate in complete isolation.

## Header Propagation: Transient Context Passing

While no persistent state exists, the SDK does propagate session context through HTTP headers for downstream coordination. In [`packages/agent/src/claude-runtime.ts`](https://github.com/JuliusBrussee/caveman/blob/main/packages/agent/src/claude-runtime.ts), lines 23-31:

```typescript
const headers = {
  "x-cave-session": input.sessionID,
  "x-cave-cache-epoch":
    `${input.definition.id}:${input.sessionID}:${input.prefixSHA256.slice(0, 16)}`,
  // …
};

```

These `x-cave-*` headers allow the gateway, proxy layers, and Pi extension to associate requests with the correct session and cache epoch. However, this is **one-way communication**—downstream components read these headers but write no durable state back to the client.

## Practical Implementation Examples

### Running with Automatic Mode Detection

```typescript
import { runClaudeAgent } from "caveman-agent";

const definition = /* your AgentDefinition */;
const input = "Explain quantum entanglement in simple terms.";

const result = await runClaudeAgent(definition, input);
console.log(result.mode);  // "optimized" or "observe-only"

```

The session ID is generated internally, gateway availability is tested, and the mode is locked without caller intervention.

### Forcing Observe-Only Mode Explicitly

```typescript
await runClaudeAgent(definition, input, {
  cave: "off",  // Bypass gateway regardless of availability
});

```

Even with a reachable gateway, this run will use `"observe-only"` mode.

### Verifying Per-Run Session Uniqueness

```typescript
// Two sequential calls = two distinct sessions
const run1 = await runClaudeAgent(definition, "Query 1");
const run2 = await runClaudeAgent(definition, "Query 2");

// run1.sessionID !== run2.sessionID (guaranteed)
// run1.mode may differ from run2.mode if gateway state changed

```

## Key Source Files and Responsibilities

| File | Responsibility |
|------|--------------|
| [`packages/agent/src/claude-runtime.ts`](https://github.com/JuliusBrussee/caveman/blob/main/packages/agent/src/claude-runtime.ts) | Generates `sessionID`, calls `resolveCaveRoute`, sets `RunResult.mode`, forces `persistSession: false`, and builds `x-cave-*` headers |
| [`packages/agent/src/runtime.ts`](https://github.com/JuliusBrussee/caveman/blob/main/packages/agent/src/runtime.ts) | Defines `RunResult` interface including the `mode: "optimized" \| "observe-only"` type |
| [`packages/pi-extension/src/provider.ts`](https://github.com/JuliusBrussee/caveman/blob/main/packages/pi-extension/src/provider.ts) | Injects Caveman headers into provider requests for downstream consumption |
| [`packages/cli/src/native-hook-fast.ts`](https://github.com/JuliusBrussee/caveman/blob/main/packages/cli/src/native-hook-fast.ts) | Demonstrates CLI-to-runtime session ID forwarding pattern |

## Summary

- **Per-run session IDs**: Fresh UUID generation on every `runClaudeAgent` call in [`claude-runtime.ts`](https://github.com/JuliusBrussee/caveman/blob/main/claude-runtime.ts)
- **Single negotiation point**: `resolveCaveRoute` determines mode once at run start
- **Immutable mode assignment**: `"optimized"` or `"observe-only"` locked in `RunResult.mode`
- **Explicit statelessness**: `persistSession: false` prevents cross-run contamination
- **Transient header propagation**: `x-cave-session` and `x-cave-cache-epoch` carry context without persistence

## Frequently Asked Questions

### Can Caveman mode change during an active Claude agent run?

No. Mode is determined when `runClaudeAgent` begins and remains fixed for that run's entire lifecycle. The `useGateway` boolean from `resolveCaveRoute` is evaluated exactly once, and the resulting mode string is baked into the `RunResult` object. There is no API to renegotiate or switch modes mid-execution.

### Does Caveman store session state between CLI invocations?

No. The `persistSession: false` setting in [`claude-runtime.ts`](https://github.com/JuliusBrussee/caveman/blob/main/claude-runtime.ts) explicitly disables all persistence. Each CLI invocation, script execution, or programmatic call to `runClaudeAgent` starts with a blank slate—new `sessionID`, fresh gateway check, and independent mode decision.

### What happens if the gateway becomes unreachable mid-run?

The mode has already been locked. If `useGateway` was `true` at run start, the run continues in `"optimized"` mode and will fail if gateway requests cannot complete. Conversely, if the mode was `"observe-only"` at start but the gateway later becomes available, that run will not benefit from optimization. The per-run isolation prevents partial state transitions.

### How can I debug which mode was selected for a specific run?

Inspect the `mode` field on the returned `RunResult` object. For deeper tracing, enable logging in [`claude-runtime.ts`](https://github.com/JuliusBrussee/caveman/blob/main/claude-runtime.ts) to capture the generated `sessionID` and `resolveCaveRoute` decision. The `x-cave-session` header visible in network logs also connects individual HTTP requests back to their originating run.