# Sessions and runtime

> Agent session lifecycle, session services, runtime events, queueing, tree navigation, and session-scoped vs durable state.

- Repository: PrimeIntellect-ai/prime-agent
- GitHub: https://github.com/PrimeIntellect-ai/prime-agent
- Human docs: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1
- Complete Markdown: https://grok-wiki.com/public/docs/primeintellect-ai-prime-agent-3367c32760b1/llms-full.txt

## Source Files

- `packages/coding-agent/src/core/agent-session.ts`
- `packages/coding-agent/src/core/agent-session-runtime.ts`
- `packages/coding-agent/src/core/agent-session-services.ts`
- `packages/coding-agent/src/core/agent-session-config.ts`
- `packages/coding-agent/examples/sdk/13-session-runtime.ts`
- `packages/coding-agent/test/suite/agent-session-runtime.test.ts`

---

---
title: "Sessions and runtime"
description: "Agent session lifecycle, session services, runtime events, queueing, tree navigation, and session-scoped vs durable state."
---

`AgentSessionRuntime` owns one active `AgentSession` plus its cwd-bound `AgentSessionServices`. Modes (interactive TUI, print/JSON/RPC, SDK, daemon worker) sit above this pair: they rebind UI/extension subscriptions when the runtime replaces the session, and they do not own provider calls, queues, transcript writes, or child RLM runtimes.

```mermaid
flowchart TB
  subgraph clients["Clients"]
    tui["Interactive TUI"]
    headless["Print · JSON · RPC"]
    sdk["SDK createAgentSession / createAgentSessionRuntime"]
  end

  subgraph runtime["AgentSessionRuntime"]
    session["AgentSession"]
    services["AgentSessionServices"]
    children["RLM subagent runtimes"]
    store["ActionStore<br/>steer · followUp"]
    kernel["IPython kernel"]
  end

  subgraph durable["Durable storage"]
    jsonl["Session JSONL tree<br/>~/.prime/agent/sessions/"]
    auth["auth.json · models.json · settings"]
    harness["Harness / refine snapshots"]
  end

  providers["Model providers BYOK"]

  tui --> runtime
  headless --> runtime
  sdk --> runtime
  runtime --> session
  session --> services
  session --> store
  session --> kernel
  session --> children
  session --> jsonl
  session <-->|"stream"| providers
  services --> auth
```

## Runtime stack

| Layer | Type / factory | Owns |
|-------|----------------|------|
| Runtime | `AgentSessionRuntime` / `createAgentSessionRuntime` | Active session, services, diagnostics, session lease, hosted RLM children, replace/dispose |
| Services | `AgentSessionServices` / `createAgentSessionServices` | cwd-bound infrastructure: auth, settings, models, resource loader, MCP |
| Session | `AgentSession` / `createAgentSession` or `createAgentSessionFromServices` | Agent loop, queues, events, compaction, goals, tree navigation, tools, kernels |
| Transcript | `SessionManager` | Append-only JSONL tree, leaf pointer, branch/fork files |
| Config snapshot | `AgentSessionRuntimeConfig` | Fixed process-level options reused across session replacements |

Services are recreated when the effective session cwd changes. Session options (model, tools, RLM depth, autonomous flags) resolve against those services before `AgentSession` construction.

### AgentSessionServices

```ts
interface AgentSessionServices {
  cwd: string;
  agentDir: string;
  authStorage: AuthStorage;
  settingsManager: SettingsManager;
  modelRegistry: ModelRegistry;
  resourceLoader: ResourceLoader;
  mcpManager: McpManager;
  diagnostics: AgentSessionRuntimeDiagnostic[];
}
```

`createAgentSessionServices` returns diagnostics instead of printing or exiting. Callers decide whether warnings are shown and whether errors abort startup. CLI resource paths should be absolute before they reach service creation so cwd switches do not reinterpret them.

### AgentSessionRuntimeConfig

Runtime config is a mergeable snapshot of provider/model, tools, skills, extensions, themes, autonomous gates, `serializedRefine`, and `initialGoal`. It is not the settings file: settings live under `SettingsManager`; this object is what a host (CLI flags, JSON client, daemon worker) passes into the runtime factory for every rebuild.

| Field | Role |
|-------|------|
| `cwd` / `agentDir` / `sessionDir` | Paths for discovery and session storage |
| `provider` / `model` / `apiKey` / `thinking` | Initial model selection |
| `tools` / `noTools` / `noBuiltinTools` | Tool surface |
| `extensions` / `skills` / `promptTemplates` / `themes` + `no*` flags | Resource discovery gates |
| `autonomous` | Continuation policy |
| `serializedRefine` | Auto-refine between turns (print/headless), survives `appMode="daemon"` handoff |
| `initialGoal` | Seeds a top-level goal once when the branch is still seedable |

## Session lifecycle

### Create

```ts
const createRuntime: CreateAgentSessionRuntimeFactory = async ({
  cwd, sessionManager, sessionStartEvent,
}) => {
  const services = await createAgentSessionServices({ cwd });
  return {
    ...(await createAgentSessionFromServices({
      services,
      sessionManager,
      sessionStartEvent,
    })),
    services,
    diagnostics: services.diagnostics,
  };
};

const runtime = await createAgentSessionRuntime(createRuntime, {
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  sessionManager: SessionManager.create(process.cwd()),
});
```

`createAgentSessionRuntime` acquires a session lease (when enabled), asserts the session cwd exists, runs the factory, and stores the factory for later `/new`, resume, `/fork`, and import.

### Replace

`AgentSessionRuntime` replacement methods share one teardown → build → rebind sequence:

1. Emit cancellable `session_before_switch` or `session_before_fork` to extensions.
2. Acquire a replacement lease on the target session path.
3. Emit `session_shutdown`, flush agent-trace upload, run `beforeSessionInvalidate`, `disposeAsync()` (kernel snapshot flush), dispose hosted subagent runtimes.
4. Build a new runtime with the stored factory and commit the lease.
5. Call `rebindSession` / `onSessionReplaced` listeners, then optional `withSession`.

| Method | Purpose | `session_start.reason` | Shutdown reason |
|--------|---------|------------------------|-----------------|
| `newSession()` | New JSONL (optional `parentSession`) | `new` | `new` |
| `switchSession(path)` | Open existing session file | `resume` | `resume` |
| `fork(entryId)` | New file (or in-memory branch) from a tree position | `fork` | `fork` |
| `importFromJsonl(path)` | Copy import into session dir and open | `resume` | `resume` |
| `dispose()` | Final teardown | — | `quit` |

All return `{ cancelled: boolean }` when an extension cancels the before-event. Dispose is idempotent: concurrent `dispose()` calls share one promise; subagent dispose errors still tear down remaining children and surface the first error.

### Rebind after replacement

After any replacement, rebind session-local subscriptions to `runtime.session`. Holding a stale `session` reference after `newSession` / `switchSession` is a common integration bug.

```ts
async function bindSession(runtime: AgentSessionRuntime) {
  unsubscribe?.();
  const session = runtime.session;
  await session.bindExtensions({});
  unsubscribe = session.subscribe((event) => {
    if (event.type === "session_action_update") {
      // steering + followUps previews
    }
  });
  return session;
}
```

## Durable storage

Sessions auto-save as JSONL under `~/.prime/agent/sessions/` (override agent dir via `PRIME_AGENT_CODING_AGENT_DIR`). Current layout is a flat directory of `<session-id>.jsonl`; older per-project dirs migrate on load.

```bash
prime-agent --continue          # most recent session for cwd
prime-agent --resume [path|id]  # picker or direct resume
prime-agent --no-session        # ephemeral; do not save
prime-agent --fork <path|id>    # fork into a new session file
```

`SessionManager` factories:

| Factory | Persistence |
|---------|-------------|
| `SessionManager.create(cwd, sessionDir?)` | New file under default or given session dir |
| `SessionManager.open(path, sessionDir?, cwdOverride?)` | Existing file |
| `SessionManager.inMemory(cwd?)` | No disk file (`sessionFile` undefined) |

Header shape (first line):

```ts
interface SessionHeader {
  type: "session";
  version?: number; // CURRENT_SESSION_VERSION = 3
  id: string;
  timestamp: string;
  cwd: string;
  parentSession?: string;
  rlmDepth?: number;
  git?: GitContext;
}
```

Entries form a tree via `id` / `parentId`. The leaf pointer is the active conversation tip. `buildSessionContext()` walks root → leaf, applying compaction and branch summaries for the LLM context.

### Interactive session commands

| Command | Effect |
|---------|--------|
| `/session` | Current file, id, message count |
| `/resume` | Session picker (search, rename, delete) |
| `/new` | Runtime `newSession()` |
| `/name <name>` | Display name on the active session |
| `/tree` | In-file leaf navigation |
| `/fork` | New session file from a prior user message |
| `/clone` | Duplicate active branch into a new session file |
| `/compact` | Manual compaction |
| `/usage` | Tokens, cost, context |

## Prompt admission and queueing

All turn work flows through a session-owned `ActionStore`, not `Agent.steer` / `Agent.followUp` directly. Delivery policies:

| Schedule | Delivery policy | When it runs |
|----------|-----------------|--------------|
| `steer` | `next_turn_boundary` | After the current assistant finishes its tool calls, before the next LLM call |
| `followUp` | `when_run_idle` | Only when the run is idle (no tools, no steering) |

### Prompt APIs

```ts
await session.prompt(text, options?);
await session.promptUntilAccepted(text, options?); // ownership accepted; may still be queued
await session.promptAndWait(text, options?);       // accepted + completion
await session.steer(text, images?, options?);
await session.followUp(text, images?, options?);   // returns boolean (coalesce may drop)
```

Important `PromptOptions`:

| Option | Default / constraint |
|--------|----------------------|
| `expandPromptTemplates` | `true` |
| `streamingBehavior` | Required when already streaming or busy with queued work: `"steer"` \| `"followUp"` |
| `followUpQueueKey` | Coalesce so only one pending follow-up exists for that key |
| `queueIfBusy` | Queue when idle but unfinished actions remain |
| `source` | Extension input source; default `"interactive"` |
| `preflightResult` | RPC hook: `(success, queued?) => void` |

Action lifecycle (legal transitions enforced):

```text
queued → selected → preparing → committing → running → completed
                                      ↘ failed / cancelled
```

Visible queue state is projected as `SessionActionSnapshot` and emitted as `session_action_update`:

```ts
interface SessionActionSnapshot {
  queuedCount: number;
  steering: readonly string[];
  followUps: readonly string[];
  active?: {
    kind: "turn" | "session_command";
    phase: "preparing" | "committing" | "running";
    label?: string;
  };
}
```

Daemon recovery can restore unfinished actions via `SessionActionRecoverySnapshot` (`SESSION_ACTION_RECOVERY_FORMAT_VERSION = 1`). Restored delivery records start with `durable: false` until re-committed to the transcript.

Branch mutations and turn dispatch share commit fences: `navigateTree` acquires a queued-work pause and a session-action commit fence so the leaf and action store stay consistent.

## Tree navigation vs fork

| Operation | File | API | Typical use |
|-----------|------|-----|-------------|
| `/tree` / `navigateTree` | Same session | `AgentSession.navigateTree(targetId, options)` | Explore alternatives in place |
| `/fork` / `runtime.fork` | New session file (when persisted) | `AgentSessionRuntime.fork(entryId)` | Separate lineage from a user message or position |
| `/clone` | New session file | Branch at current leaf | Snapshot active work before continuing |

### navigateTree selection rules

- **User or custom message:** leaf moves to the message's parent; message text returns as `editorText` for edit-and-resubmit (new branch).
- **Assistant / tool / compaction / other:** leaf moves to that entry; editor empty; continue from that point.
- **Root user message:** leaf resets; original prompt placed in the editor.

Optional branch summary when leaving a path: summarize abandoned entries into a `branch_summary` attached at the new leaf. Extensions can cancel or supply the summary via `session_before_tree` / `session_tree`.

`fork` with `position: "before"` (default) requires a user message entry and returns selected user text. `position: "at"` forks including the selected entry. Persisted sessions call `SessionManager.createBranchedSession(leafId)` to write a new JSONL containing only the root→leaf path; in-memory sessions branch in place.

## Events

### AgentSessionEvent (session.subscribe)

Extends core `AgentEvent` (message start/update/end, tool execution, turn/agent boundaries) with session-specific types:

| `type` | Payload highlights |
|--------|--------------------|
| `session_action_update` | `actions: SessionActionSnapshot` |
| `session_info_changed` | `name` |
| `thinking_level_changed` / `service_tier_changed` | Level / tier |
| `compaction_start` / `compaction_end` | reason, result, aborted, severity |
| `auto_retry_start` / `auto_retry_end` | attempt, delay, success |
| `auth_stale` | provider, sourceTokens |
| `rlm_child_update` | `RlmChildAgentSnapshot` |
| `recap_update` / `goal_update` | recap string / `GoalState` |
| `bash_start` / `bash_output` / `bash_end` | user bash runs |
| `refine_complete` / `refine_failed` | refinement result / error |
| `ipython_sent_agent_message` | kernel-sent agent message |

Session persistence of messages is internal (on message end). Multiple listeners are supported; each `subscribe` returns its own unsubscribe.

### Extension session events

| Event | Cancellable | When |
|-------|-------------|------|
| `session_start` | no | `startup` \| `reload` \| `new` \| `resume` \| `fork` |
| `session_before_switch` | yes | Before new/resume |
| `session_before_fork` | yes | Before fork |
| `session_before_tree` | yes | Before leaf change; can supply summary |
| `session_tree` | no | After leaf change |
| `session_before_compact` / `session_compact` | yes / no | Compaction |
| `session_shutdown` | no | `quit` \| `reload` \| `new` \| `resume` \| `fork` |

## Session-scoped vs durable state

| State | Lifetime | Storage |
|-------|----------|---------|
| Action queue (steer/followUp), pump epoch, commit fences | Process / until drain | Memory (`ActionStore`) |
| Agent in-memory messages | Until rebuild or navigate | `agent.state.messages` (rebuilt from JSONL) |
| Active IPython kernel + namespace | Until dispose/rebuild | Process + optional snapshot dir on dispose |
| Extension runner, tool registry, base system prompt | Session instance | Memory; rebuilt on bind/reload |
| RLM child run maps, retained child sessions | Parent session lifetime | Memory (+ child JSONL if persisted) |
| Transcript entries (messages, model/thinking changes, labels, compactions, branch summaries, custom) | Durable | Session JSONL tree |
| Goals (`thread_goal_state`) | Durable on branch | JSONL custom/goal entries |
| Auth / models / user settings | Cross-session | `~/.prime/agent/auth.json`, `models.json`, settings |
| Harness prompts, memories, skill/subagent specs | Cross-session (refine) | Continual harness store (separate from session tree) |
| Session leases | While runtime holds path | `agentDir/session-leases/` when `PRIME_AGENT_INTERNAL_SESSION_LEASES` enabled |

```text
Session-scoped (dies with AgentSession instance)
  queues · kernel · extension bindings · active child maps · in-memory agent state

Durable (survives restart / reattach)
  JSONL tree + artifacts · auth · settings · harness snapshots · optional kernel namespace snapshot
```

Ephemeral mode (`SessionManager.inMemory()` or CLI `--no-session`) keeps the same runtime APIs without writing a session file. Daemon-backed long-running work rehydrates from JSONL and optional action-recovery snapshots; see daemon and long-running docs for worker recovery.

## Session metadata and activity

| Getter / property | Meaning |
|-------------------|---------|
| `session.sessionId` | Stable session id from header |
| `session.sessionFile` | Absolute JSONL path, or undefined if in-memory |
| `session.sessionName` | Optional display name |
| `session.rlmDepth` | RLM spawn depth (0 = root) |
| `session.model` | Active model |
| `session.isStreaming` | Agent turn streaming |
| `session.isSessionActive` | Streaming, compacting, retrying, bash, refine, branch summary, or unfinished actions |
| `session.queuedActionCount` | Visible queued steer + follow-up count |
| `runtime.metadata` | `kind: "top-level" \| "subagent"`, parent ids, spawn prompt/code |
| `runtime.diagnostics` | Non-fatal create-time issues |
| `runtime.modelFallbackMessage` | Startup model warning; suppressed once a model is selected |

## Constraints and failure modes

| Condition | Behavior |
|-----------|----------|
| Prompt while streaming without `streamingBehavior` | Throws: specify `'steer'` or `'followUp'` |
| Extension command as steer/followUp text | Rejected |
| Fork invalid entry / non-user with `position: "before"` | Throws `Invalid entry ID for forking` |
| `navigateTree` unknown id | Throws `Entry … not found` |
| Summarize without model | Throws `No model available for summarization` |
| Import missing path | `SessionImportFileNotFoundError` |
| Missing/unresolvable session cwd | `MissingSessionCwdError` / assert failure before open |
| Session already leased by another process | `SessionAlreadyActiveError` (when leases enabled) |
| Replacement build fails after teardown | Error propagates; uncommitted lease released; caller must handle empty runtime |
| Concurrent dispose | Single teardown; later callers await the same promise |

Provider keys are BYOK: `ModelRegistry` + `AuthStorage` resolve API keys/OAuth per provider. Session runtime does not embed a hosted model service.

## Minimal runtime example

From `packages/coding-agent/examples/sdk/13-session-runtime.ts`:

```ts
const runtime = await createAgentSessionRuntime(createRuntime, {
  cwd: process.cwd(),
  agentDir: getAgentDir(),
  sessionManager: SessionManager.create(process.cwd()),
});

let session = await bindSession(runtime);
const original = session.sessionFile;

await runtime.newSession();
session = await bindSession(runtime);

if (original) {
  await runtime.switchSession(original);
  session = await bindSession(runtime);
}

await runtime.dispose();
```

Persistence-only recipe (`11-sessions.ts`): `SessionManager.inMemory()`, `SessionManager.create(cwd)`, or open a path—then `createAgentSession({ sessionManager })` without a multi-session runtime when you never replace the session.

## Related pages

<CardGroup>
  <Card title="Session configuration reference" href="/session-configuration">
    Config keys, defaults, reload, and settings surfaces used by runtime services.
  </Card>
  <Card title="Agent connection modes" href="/agent-connection">
    Daemon, in-process, and snapshot paths; active session state and transfer constraints.
  </Card>
  <Card title="Run daemon-backed sessions" href="/daemon-sessions">
    Detach/reattach, resume selectors, and worker recovery.
  </Card>
  <Card title="Sessions and full control (SDK)" href="/sdk-sessions-control">
    SDK session management, settings injection, and full-control composition.
  </Card>
  <Card title="Continual Harness" href="/continual-harness">
    Durable harness state versus session-scoped transcript state.
  </Card>
  <Card title="Long-running tasks" href="/long-running-tasks">
    Goals, compaction, heartbeats, and autonomous continuation across disconnects.
  </Card>
  <Card title="Subagents and messaging" href="/subagents-messaging">
    RLM child runtimes, agent-message surface, and multi-agent constraints.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Invalid resume selectors, auth failures, and connection-mode probes.
  </Card>
</CardGroup>
