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

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, the implementation creates a simple, hyphen-free UUID:

// 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:

// 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. This wrapper uses curl with a strict 500-millisecond timeout to ensure the hook operation never blocks the agent:


# 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, the server parses the incoming query string and validates the key format using the valid_ingest_key function:

// 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 (lines 103-110) extracts the validated key and passes it to the storage layer. The 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:

// 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:

// 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

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 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 (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 generates the key, crates/ai-memory-hooks/src/payload.rs validates it, crates/ai-memory-hooks/src/router.rs routes it, and crates/ai-memory-store/src/ops.rs handles the database-level deduplication in the ingest_keys table.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →