# How the ai-memory Hook Command Spools Events with Idempotency Keys

> Discover how the ai-memory hook command spools events with unique UUID ingest keys. Learn how servers deduplicate observations during network retries using idempotency keys.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: how-to-guide
- Published: 2026-08-23

---

**The ai-memory CLI generates a unique UUID-based ingest key for every lifecycle event and embeds it in the POST request URL, enabling the server to deduplicate observations via the `ingest_keys` table even during network retries.**

The `akitaonrails/ai-memory` repository implements a fire-and-forget hook mechanism that captures agent lifecycle events and spools them to a remote server. To guarantee exactly-once ingestion despite transient network failures or timeouts, the native hook command implements client-side idempotency key generation coupled with server-side deduplication logic.

## Generating Client-Side Idempotency Keys

The idempotency workflow begins in the CLI hook command, where each event receives a unique identifier before transmission.

### UUID Construction in hook.rs

When the `hook` command executes, it generates a fresh **ingest key** using Rust's UUID library. This key serves as the idempotency token for the entire request lifecycle.

In [`crates/ai-memory-cli/src/commands/hook.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/hook.rs), the implementation creates a simple, hyphen-free UUID:

```rust
// crates/ai-memory-cli/src/commands/hook.rs (lines 563-566)
let ingest_key = uuid::Uuid::new_v4().simple().to_string();

```

The `.simple()` method strips hyphens from the UUID, producing a 32-character alphanumeric string that functions as the canonical identifier for this specific event instance.

## Constructing the Spool Request

Once generated, the ingest key is embedded directly into the request URL as a query parameter, ensuring it reaches the server for validation.

### URL Assembly and Event Transmission

The CLI constructs the full endpoint URL by appending the ingest key alongside other context metadata such as event type, agent name, and session identifiers:

```rust
// crates/ai-memory-cli/src/commands/hook.rs
let ingest_key = uuid::Uuid::new_v4().simple().to_string();
let url = format!(
    "{base}/hook?event={event}&agent={agent}{session_qs}{cwd_qs}{ingest_key}",
    base = base_url,
    event = event,
    agent = agent,
    session_qs = session_qs,
    cwd_qs = cwd_qs,
    ingest_key = ingest_key,
);

```

The actual network transmission occurs via the `ai_memory_post_hook` function defined in [`hooks/_lib.sh`](https://github.com/akitaonrails/ai-memory/blob/main/hooks/_lib.sh). This wrapper uses `curl` with a strict 500-millisecond timeout to ensure the hook operation never blocks the agent:

```sh

# hooks/_lib.sh (lines 70-81) – ai_memory_post_hook

if [ -n "${AI_MEMORY_AUTH_TOKEN:-}" ]; then
    curl -s --max-time 0.5 -X POST "$1" \
        -H "Content-Type: application/json" \
        -H "Authorization: Bearer $AI_MEMORY_AUTH_TOKEN" \
        --data-binary @-
else
    curl -s --max-time 0.5 -X POST "$1" \
        -H "Content-Type: application/json" \
        --data-binary @-
fi

```

## Server-Side Idempotency Validation

Upon receiving the POST request, the server extracts and validates the ingest key before persisting any observation data.

### Validating the Ingest Key

In [`crates/ai-memory-hooks/src/payload.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/payload.rs), the server parses the incoming query string and validates the key format using the `valid_ingest_key` function:

```rust
// crates/ai-memory-hooks/src/payload.rs (lines 55-62)
fn valid_ingest_key(key: &str) -> bool {
    let len = key.len();
    len >= 1 && len <= 64 && key.chars().all(|c| c.is_ascii_alphanumeric() || "_-".contains(c))
}

```

This validation ensures the key contains only safe ASCII characters and falls within acceptable length bounds before database operations proceed.

### Database Deduplication Logic

The router in [`crates/ai-memory-hooks/src/router.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/router.rs) (lines 103-110) extracts the validated key and passes it to the storage layer. The [`crates/ai-memory-store/src/writer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/writer.rs) file handles the atomic insertion via `insert_observation_keyed`, which first checks for existing entries.

The underlying idempotency mechanism resides in [`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs):

```rust
// crates/ai-memory-store/src/ops.rs (lines 1116-1147)
tx.execute(
    "INSERT INTO ingest_keys (project_id, key, seen_at, completed_at) VALUES (?1, ?2, ?3, NULL)",
    params![project_id.as_bytes(), key, now],
)?;

```

The `ingest_keys` table enforces uniqueness at the database level. Before insertion, the system queries for existing records:

```rust
// crates/ai-memory-store/src/ops.rs
let completed: Option<i64> = tx.query_row(
    "SELECT completed_at FROM ingest_keys WHERE project_id = ?1 AND key = ?2",
    params![project_id.as_bytes(), key],
    |row| row.get(0),
)?;
if completed.is_some() {
    // already processed → skip
}

```

If the query returns a row, the observation is discarded as a duplicate, ensuring **exactly-once processing** semantics.

## Retry Safety and Network Resilience

The idempotency architecture guarantees safety during network retries. If the `curl` request times out or fails, the CLI can retry using the same `ingest_key` value (cached in the splice state file). Because the server recognizes duplicate keys and ignores them, transient network hiccups cannot create duplicate observations.

This design pattern ensures that each logical lifecycle event results in at most one database entry, regardless of how many times the client attempts transmission.

## Summary

- The **ai-memory hook command** generates a UUID-based ingest key for every event in [`crates/ai-memory-cli/src/commands/hook.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/hook.rs).
- The key is embedded as a URL query parameter and transmitted via `curl` with a 0.5-second timeout defined in [`hooks/_lib.sh`](https://github.com/akitaonrails/ai-memory/blob/main/hooks/_lib.sh).
- Server-side validation occurs in [`crates/ai-memory-hooks/src/payload.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/payload.rs) using the `valid_ingest_key` function.
- The `ingest_keys` table in [`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs) provides atomic deduplication, treating duplicate inserts as no-ops.
- Retry operations reuse the same key, making the entire pipeline idempotent against network failures.

## Frequently Asked Questions

### What is an idempotency key in ai-memory?

An **idempotency key** (called an **ingest key** in the codebase) is a client-generated UUID that uniquely identifies a specific lifecycle event observation. According to the `akitaonrails/ai-memory` source code, this key ensures that retrying a failed network request does not result in duplicate database entries.

### How does ai-memory prevent duplicate observations during network retries?

The CLI caches the generated ingest key in the splice state file, allowing retries to reuse the same identifier. When the server receives a request with a previously seen key, [`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs) detects the existing entry in the `ingest_keys` table and skips the insertion, effectively discarding the duplicate.

### What happens if the ingest key validation fails?

If the key fails the `valid_ingest_key` check in [`crates/ai-memory-hooks/src/payload.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/payload.rs) (lines 55-62), the server rejects the request early in the processing pipeline. The validation requires the key to be 1-64 characters containing only ASCII alphanumeric characters, underscores, or hyphens.

### Where is the idempotency logic implemented in the ai-memory codebase?

The idempotency workflow spans multiple crates: [`crates/ai-memory-cli/src/commands/hook.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/hook.rs) generates the key, [`crates/ai-memory-hooks/src/payload.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/payload.rs) validates it, [`crates/ai-memory-hooks/src/router.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-hooks/src/router.rs) routes it, and [`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs) handles the database-level deduplication in the `ingest_keys` table.