> ## Documentation Index
> Fetch the complete documentation index at: https://docs.get-exo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Coding session history

> Backfill the Claude Code and Codex transcripts already on your disk, then keep the graph current as you work.

Your coding agent already writes a full transcript of every session to local disk. Those transcripts are the densest record of how you actually work: what you tried, what you rejected, what you decided. Exo reads them and turns them into graph structure.

This is the fastest way to take an empty organization to a useful one, because the content already exists.

## Before you start

* A key with the `write` scope. See [Scopes](/scopes).
* The `exo` CLI installed and holding that key.
* Transcripts on disk: `~/.claude/projects/**/*.jsonl` for Claude Code, `~/.codex/sessions/**/rollout-*.jsonl` for Codex.

<Note>
  A browser cannot read your local disk, which is why this is a CLI path and not an upload form. There is no way to drag a session transcript into a web page, because the file you would be dragging is not one file.
</Note>

## The short version

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
exo backfill --source all   # one-time import of past Claude Code and Codex sessions
exo daemon                  # tail new sessions as you work
```

`--source claude`, `--source codex` and `--source all` all work. Re-runs are dedup safe: every event carries a stable dedup key, so a second backfill over the same transcripts adds nothing.

## What backfill actually does

<Steps>
  <Step title="Walk the transcripts">
    The CLI reads each session file, drops noise, and normalises every turn into a session event. Claude Code and Codex go through the same builder, so the two sources produce the same shape and nothing downstream has to know which agent you used. Codex session ids are prefixed `codex-` for provenance.

    Redaction happens here, on your machine, before anything leaves it.
  </Step>

  <Step title="Send them in batches">
    Events go to `POST /v1/ingest/session-batch`, up to 500 per call, with retry and backoff on 429 and 5xx. Because each event carries a dedup key, a retried batch cannot double-write.
  </Step>

  <Step title="Finalize once">
    After the last batch is accepted, the CLI calls `POST /v1/ingest/session-finalize` exactly once. This is the step that matters, and it is the one people miss when they script this by hand.

    <CodeGroup>
      ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      curl -X POST https://api.get-exo.com/v1/ingest/session-finalize \
        -H "X-Exo-API-Key: $EXO_KEY"
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      import os, requests

      res = requests.post(
          "https://api.get-exo.com/v1/ingest/session-finalize",
          headers={"X-Exo-API-Key": os.environ["EXO_KEY"]},
      )
      print(res.json())
      ```

      ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      const res = await fetch(
        "https://api.get-exo.com/v1/ingest/session-finalize",
        { method: "POST", headers: { "X-Exo-API-Key": process.env.EXO_KEY! } },
      );
      console.log(await res.json());
      ```
    </CodeGroup>

    ```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    { "enqueued": true, "alreadyInFlight": false, "jobId": "c41e8b02-7f39-4b60-a2d8-51e93c07f4a6" }
    ```

    Session ingestion writes nodes. It does not, on its own, produce graph edges, domains, the identity extraction or the first cognition cycle. Finalize is what kicks that build. Skip it and you get a pile of session nodes and empty recall surfaces, which looks exactly like a broken import.

    It is idempotent. Calling it again while a build is running is a no-op and comes back with `enqueued: false` and `alreadyInFlight: true`.
  </Step>

  <Step title="Watch the layers fill in">
    <CodeGroup>
      ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      curl https://api.get-exo.com/v1/ingest/session-status \
        -H "X-Exo-API-Key: $EXO_KEY"
      ```

      ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      import os, time, requests

      while True:
          res = requests.get(
              "https://api.get-exo.com/v1/ingest/session-status",
              headers={"X-Exo-API-Key": os.environ["EXO_KEY"]},
          )
          s = res.json()
          print(s["sessionNodeCount"], s["ingestPending"], s["warmLayers"])
          if not s["building"] and s["ingestPending"] == 0:
              break
          time.sleep(5)
      ```

      ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
      for (;;) {
        const res = await fetch(
          "https://api.get-exo.com/v1/ingest/session-status",
          { headers: { "X-Exo-API-Key": process.env.EXO_KEY! } },
        );
        const s = await res.json();
        console.log(s.sessionNodeCount, s.ingestPending, s.warmLayers);
        if (!s.building && s.ingestPending === 0) break;
        await new Promise((r) => setTimeout(r, 5000));
      }
      ```
    </CodeGroup>

    ```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
    {
      "sessionNodeCount": 1842,
      "sessionEdgeCount": 5310,
      "totalNodeCount": 1907,
      "lastIngestedAt": "2026-07-14T11:02:44Z",
      "ingestPending": 0,
      "finalizePending": false,
      "building": true,
      "warmLayers": {
        "identity": true,
        "manifold": true,
        "domains": true,
        "insights": false,
        "temporal": false
      }
    }
    ```
  </Step>
</Steps>

<Warning>
  **Poll `building`, not `warmLayers`.** `building` tracks whether a job is actually active. The warm layers are data dependent: a layer can legitimately stay false forever because the content does not support it. Insights, for example, need node pairs that land in a narrow similarity band, and a small organization may produce none.

  A loop that waits for all five warm layers to go true never exits on some organizations.
</Warning>

## The live tail

`exo daemon` watches for new session activity and streams it as you work, so the graph stays current without a second backfill. It watches the Claude Code root, and optionally a Codex root as well.

The daemon and the backfill share one implementation of the event builder, including the noise filter and the redaction pass. They cannot drift apart and start sending differently shaped events.

## Reading sessions back

`GET /v1/recall/sessions` lists the coding sessions in the graph. `DELETE /v1/recall/sessions/{session_id}` removes one, which is the targeted way to drop a session that captured something it should not have.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  curl https://api.get-exo.com/v1/recall/sessions \
    -H "X-Exo-API-Key: $EXO_KEY"
  ```

  ```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  import os, requests

  res = requests.get(
      "https://api.get-exo.com/v1/recall/sessions",
      headers={"X-Exo-API-Key": os.environ["EXO_KEY"]},
  )
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
  const res = await fetch("https://api.get-exo.com/v1/recall/sessions", {
    headers: { "X-Exo-API-Key": process.env.EXO_KEY! },
  });
  ```
</CodeGroup>

To remove a specific memory rather than a whole session, see [Forgetting and portability](/guides/forgetting).

## Agent signals

`POST /v1/ingest/agent-signals` records where a coding agent went wrong: the corrections, retries and dead ends that a transcript alone does not label. Those signals are what the `AgentFailurePattern` object is built from.

<Note>
  The `AgentFailurePattern` object is defined in the v1 contract and the signal ingestion route is real, but the team-facing surface that reads clustered patterns back is not part of v1. You can send signals today. There is no v1 route that returns the clusters. See [AgentFailurePattern](/concepts/agent-failure-pattern) for the concept and the honest status.
</Note>

## When it goes wrong

<AccordionGroup>
  <Accordion title="Session nodes exist but every recall surface is empty" icon="circle-question">
    Finalize was never called, or it is still running. Check `finalizePending` and `building` on `GET /v1/ingest/session-status`. If both are false and the warm layers are still empty, call `POST /v1/ingest/session-finalize` again. It is idempotent, so this is safe.
  </Accordion>

  <Accordion title="A second backfill duplicated everything" icon="circle-question">
    It should not, and if it did, the events were not carrying dedup keys. That happens when a hand-rolled script posts to `POST /v1/ingest/session-batch` directly instead of going through the shared event builder. Use `exo backfill` rather than reimplementing the normaliser.
  </Accordion>

  <Accordion title="429 during a large backfill" icon="circle-question">
    Expected on a big first import, and the CLI already backs off and retries. If you are posting batches yourself, honour `Retry-After` on the 429. See [Rate limits](/platform/rate-limits).
  </Accordion>

  <Accordion title="Codex sessions are missing" icon="circle-question">
    Pass `--source codex` or `--source all`. The default source does not include Codex. Codex support also needs an `exo` CLI release that includes it, and compressed rollouts need the `zstandard` dependency present.
  </Accordion>
</AccordionGroup>

## Next

<Columns cols={2}>
  <Card title="Claude Code" icon="terminal" href="/integrations/claude-code">
    Connect the client so new work is captured as it happens.
  </Card>

  <Card title="Phase" icon="clock" href="/concepts/phase">
    What Exo builds from the dates on all that history.
  </Card>
</Columns>
