## TL;DR
`checkpointer.get_tuple()` returns None when the config you pass does not include `configurable.thread_id`, even when the thread ran fine. The fix is to call it with `{"configurable": {"thread_id": "your-thread-id"}}` instead of a bare string or an empty dict.

```text
get_tuple returned None: config was missing configurable.thread_id
```

## When to use
- `get_tuple` returns None but you know the thread ran
- You passed the thread id as a plain string instead of a config dict
- Checkpoint reads fail right after a working `invoke` or after `interrupt()`

## When not to use
- `get_tuple` raises a connection error (thats the store, not the config shape)
- Threads are missing because the checkpointer was never persisted (thats storage)
- You want to list all threads (use the checkpointer list method instead)

## Steps

1. Call `get_tuple` with a proper config dict, not a bare string:

```python
config = {"configurable": {"thread_id": "abc-123"}}
state = checkpointer.get_tuple(config)
```
Expected output: `get_tuple` returns a CheckpointTuple instead of None. If you were passing just `"abc-123"`, that was the bug.

2. Make sure the thread id matches the one used at invoke time. Thread ids are exact-match:

```python
config = {"configurable": {"thread_id": "abc-123"}}
result = graph.invoke({"input": "..."}, config)
```
Expected output: the same thread id you pass to `invoke` is the one you read back. A typo in the id silently returns None.

3. If you use subgraphs, include the checkpoint namespace:

```python
config = {"configurable": {"thread_id": "abc-123", "checkpoint_ns": ""}}
```
Expected output: reads from the root namespace. Subgraph checkpoints live under their own namespace, so a root-namespace read misses them.

4. Verify the checkpointer actually persisted the write. For SQLite:

```python
import sqlite3
conn = sqlite3.connect("checkpoints.db")
print(conn.execute("select count(*) from checkpoints").fetchone())
```
Expected output: a nonzero count. Zero rows means nothing was written, so the read side was never the problem.

## Variant phrasings

### get_tuple returns None after interrupt
Same cause. After `interrupt()`, resume with the identical config dict, including thread_id.

### thread state missing in a subgraph
The subgraph wrote under its own `checkpoint_ns`. Pass that namespace in the config.

## Why it happens
LangGraph keys every checkpoint by thread id, namespace, and checkpoint id. `get_tuple` builds that key from the config dict; when `configurable.thread_id` is absent it builds a key that matches nothing and returns None instead of raising. The silent None makes a config-shape mistake look like missing data.

## Edge cases
- An in-memory checkpointer loses everything on process restart; None after a restart is expected there, not a bug.
- `get_tuple` with only `thread_id` returns the latest checkpoint. Pass `checkpoint_id` too when you need a specific one.
- Async code must use `aget_tuple`. The sync call inside a running event loop can hang instead of returning.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_O_NA8U9WfRiBzPLkYiBogw
