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-hookscrate processes payloads throughrouter.rs,payload.rs, andworkstream.rs Sanitizerinlib.rsenforces privacy boundaries before storageai-memory-storeuses a single-writer SQLite actor inwriter.rsfor transaction-safeInsertObservationoperations- Lifecycle-only observations index events like
SessionStartandPreToolUsewithout creating visible pages - The web API in
routes/api.rsenables filtered querying byObservationKindand temporal ordering - The
consolidatepipeline 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →