How ai-memory Handles Agent Lifecycle Events and Observations: A Technical Deep Dive

ai-memory captures every agent execution step—from session start to tool use—by converting hook scripts into sanitized observations stored in SQLite, enabling full session reconstruction via a single-writer architecture.

The ai-memory project (akitaonrails/ai-memory) provides a Rust-based persistence layer for AI agents. It records every significant step of an agent's execution as immutable observations, allowing developers to reconstruct session timelines, debug failures, and enable auto-improvement loops. This article examines the complete lifecycle handling pipeline, from shell hook invocation to durable SQLite storage.

Hook-Based Event Capture Architecture

The lifecycle event handling begins with shell scripts invoked by the agent runtime. These hooks convert execution milestones into structured HTTP payloads sent to the ai-memory daemon.

Shell Script Entry Points

Located in hooks/*/session-start.sh, hooks/*/pre-tool-use.sh, hooks/*/post-tool-use.sh, and hooks/*/session-end.sh, these scripts construct JSON payloads describing events. For example, hooks/opencode/post-tool-use.sh captures tool execution details and POSTs them to the /hook endpoint.


# Simplified example based on hooks/opencode/post-tool-use.sh

curl -X POST http://127.0.0.1:49374/hook \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "PostToolUse",
    "title": "run cargo test",
    "body": "Executed `cargo test --workspace`",
    "session_id": "'"$AI_MEMORY_SESSION_ID"'",
    "project_id": "'"$AI_MEMORY_PROJECT_ID"'",
    "workspace_id": "'"$AI_MEMORY_WORKSPACE_ID"'"
  }'

Payload Processing Pipeline

Once the HTTP request reaches the ai-memory-hooks crate, the system deserializes, sanitizes, and routes the payload through a type-safe pipeline.

Router and Deserialization

In crates/ai-memory-hooks/src/router.rs, the router deserializes incoming JSON into a Payload struct defined in crates/ai-memory-hooks/src/payload.rs. The router then hands the structured data to the workstream orchestrator in crates/ai-memory-hooks/src/workstream.rs.

// crates/ai-memory-hooks/src/router.rs (excerpt)
async fn handle(payload: Payload) -> Result<()> {
    let sanitized = Sanitizer::sanitize(payload.into_new_observation())?;
    workstream::process(sanitized).await
}

Privacy Sanitization

Before storage, the Sanitizer in crates/ai-memory-hooks/src/lib.rs strips private data from the payload. This enforces privacy boundaries by returning a Sanitized<NewObservation> type that guarantees sensitive data has been removed before reaching the database layer.

Observation Storage and Indexing

The storage layer ensures durable, searchable records of all lifecycle events while maintaining strict write consistency.

Lifecycle-Only Observations

Not all events produce user-visible pages. Events like SessionStart, PreToolUse, and Stop create lifecycle-only observations signaled by LifecycleOnlyEndOutcome. These rows remain indexed and searchable in SQLite, enabling the UI and API to reconstruct complete session timelines without cluttering the primary observation feed.

The Single-Writer SQLite Actor

The store writer in crates/ai-memory-store/src/writer.rs handles all database operations through a single-writer actor pattern. This ensures transaction safety and prevents write conflicts. The InsertObservation command writes to the observations table, while InsertObservationIngest creates temporary records for the auto-improve pipeline.

// crates/ai-memory-store/src/writer.rs (excerpt)
WriteCmd::InsertObservation {
    obs: sanitized,
    reply: tx,
} => {
    let id = tx
        .send(self.db.insert_observation(obs)?)
        .await?;
}

Querying and Retrieving Observations

The web API in crates/ai-memory-web/src/routes/api.rs exposes endpoints for retrieving observation streams. The /api/v1/sessions/:id/observations endpoint accepts filters for ObservationKind (such as UserPrompt, PostToolUse, or Stop) and ordering parameters (asc or desc).


# Fetch observations for a specific session

curl "http://127.0.0.1:49374/api/v1/sessions/42/observations?kind=UserPrompt&order=asc"

Example response:

{
  "observations": [
    {
      "id": "obs-0001",
      "kind": "UserPrompt",
      "title": "Implement feature X",
      "body": "Detailed request content...",
      "timestamp": "2026-08-26T12:34:56Z"
    },
    {
      "id": "obs-0002",
      "kind": "PostToolUse",
      "title": "run cargo test",
      "body": "Test output summary...",
      "timestamp": "2026-08-26T12:35:10Z"
    }
  ],
  "order": "asc"
}

Auto-Improvement and Consolidation

After session completion, the consolidation pipeline in crates/ai-memory-consolidate/src/lib.rs processes the observation stream. It may invoke LLM providers to generate summary observations (such as additional PostToolUse entries) and inserts them back through the standard ingestion path. This maintains the single-writer invariant while enriching the session history with derived insights.

Summary

  • Hook scripts in hooks/*/ capture agent lifecycle events as JSON payloads posted to /hook
  • The ai-memory-hooks crate processes payloads through router.rs, payload.rs, and workstream.rs
  • Sanitizer in lib.rs enforces privacy boundaries before storage
  • ai-memory-store uses a single-writer SQLite actor in writer.rs for transaction-safe InsertObservation operations
  • Lifecycle-only observations index events like SessionStart and PreToolUse without creating visible pages
  • The web API in routes/api.rs enables filtered querying by ObservationKind and temporal ordering
  • The consolidate pipeline auto-generates derived observations while preserving the single-writer workflow

Frequently Asked Questions

How does ai-memory ensure privacy when storing agent observations?

The system uses the Sanitizer struct in crates/ai-memory-hooks/src/lib.rs to strip sensitive data before storage. By converting raw payloads into Sanitized<NewObservation> types, the architecture enforces privacy boundaries at the ingestion layer, ensuring private information never reaches the SQLite store.

What is the difference between regular observations and lifecycle-only observations?

Lifecycle-only observations (marked with LifecycleOnlyEndOutcome) capture internal agent events like SessionStart, PreToolUse, and Stop that don't produce user-visible pages. While they remain fully indexed and searchable for timeline reconstruction, they don't appear in the main observation feed, keeping the UI focused on substantive interactions.

How can I query specific types of agent lifecycle events?

Use the web API endpoint /api/v1/sessions/:id/observations with the kind parameter to filter by ObservationKind enum values such as UserPrompt, PostToolUse, or SessionStart. Combine this with order=asc or desc to retrieve events in chronological or reverse-chronological sequence.

What prevents write conflicts when multiple hooks fire simultaneously?

The ai-memory-store crate implements a single-writer SQLite actor pattern where all write operations serialize through writer.rs. This design ensures that concurrent InsertObservation commands from multiple lifecycle events execute transaction-safe without race conditions, even during high-frequency tool use sequences.

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 →