9.6 KiB
External lifecycle capture
An orchestrator that observes its agents can send lifecycle events to one shared
ai-memory server through /hook/batch. Framework adapters belong outside core;
the server continues to own storage, retrieval, consolidation and durable memory.
Choose one capture path before launching
Set AI_MEMORY_CAPTURE_OWNER to your producer namespace in the environment of
the harness you launch, for example example.runtime. Any non-whitespace value
opts that process and its inheriting children into external capture. Unset,
empty and whitespace-only values preserve normal native capture.
Orchestrator
|-- launches harness with AI_MEMORY_CAPTURE_OWNER=example.runtime
|-- records its lifecycle events with stable IDs
`-- sends those events through POST /hook/batch
Harness
|-- installed hooks keep delivering supported startup context
`-- MCP keeps providing recall and deliberate memory writes
Updated native hook commands, POSIX/PowerShell hook bundles, and generated
OpenCode/OpenCode 2/OMP/Pi/OpenClaw integrations honor the variable. Reapply
install-hooks after upgrading to refresh staged scripts or generated code.
Older installations do not understand this context.
| Responsibility | With external capture |
|---|---|
| Native lifecycle observations | Skipped before spool, queue or POST |
| Native CLI hook drains and SessionStart backfill | Skipped |
| Supported handoff, briefing and inbox-notice delivery | Preserved |
| Native identity used by handoff/session-aware MCP | Preserved |
| MCP recall and deliberate writes | Unchanged |
| Existing repository capture policy | Unchanged; an opted-out repository stays opted out |
Claude Code receives context through SessionStart. Kimi Code receives it through
UserPromptSubmit because it discards SessionStart output. The same existing
delivery paths remain in use. Context delivery may register the receiving session
and claim a handoff; suppressing capture does not make it read-only. Harnesses
without automatic injection can still use memory_handoff_accept through MCP.
Do not also claim that handoff in the orchestrator if the hook owns delivery.
This is inherited process context, not server configuration, a security identity, or a lease. Keep it out of the shared server's environment and global shell configuration. Standalone launches without it continue to capture normally. A runtime hosting multiple agents in one process applies the variable to all its installed ai-memory integrations; it must manage finer capture ownership itself.
ai-memory hook --check-capture reports external_capture: true and
admits_capture: false when this context is active. It performs no ingestion or
handoff delivery. The producer namespace itself is not printed or authenticated.
Reuse the public event contract
For a live harness, send its actual wire agent (such as claude-code or codex)
and native session_id. Use the same session ID, cwd, scope and authenticated
operator as its remaining native integration. The body's session ID takes
precedence over a query parameter. Replacing only the ingestion ID with an
orchestrator's logical agent ID separates it from native handoff/MCP identity.
Use explicit workspace and project for each event. Preserve canonical
lifecycle names (session-start, user-prompt-submit, post-tool-use,
session-end, etc.) and the supported harness payload shape. session-end
means the execution ended, not a turn completed. Sessions containing only
unknown events are not substantive sessions for automatic summaries.
Set extension=<producer-namespace> and an explicit source_event=<event-name>
for provenance. Both are needed to preserve the namespace on recognized events.
extension accepts up to 64 ASCII token characters; source_event accepts 128.
Letters, digits, ., _, - and : are allowed. These fields describe the
source; they do not expand the core event enum or retain arbitrary metadata.
Keep logical agents, parent/child relationships and workflow IDs in adapter-side
state for now. Do not repurpose native agent_id fields: some harness payloads
use them as subagent markers. The existing managed-workstream managed_run
field also has its own lease, ledger and handoff behavior.
Stable retry identity
ingest_key is an existing project-scoped key, independent of extension.
Construct it once from stable event identity and persist it with the event before
delivery. Never hash prompt/tool content to decide whether events are equal.
For example, an adapter using Node can derive a 64-character key as follows:
import { createHash } from "node:crypto";
function ingestKey({ producer, actor, agent, sessionId, sourceEvent, eventId }) {
const identity = JSON.stringify([
"external-capture-v1", producer, actor, agent,
sessionId, sourceEvent, eventId,
]);
return createHash("sha256").update(identity, "utf8").digest("hex");
}
Here actor is a stable adapter-side operator namespace, never a bearer token.
Include the coordinates within which eventId is unique. An adapter with
producer-wide unique event IDs may use a smaller tuple. Keep its encoding and
values stable across process restarts and implementation languages.
The same producer/event identity yields the same key on retry. Distinct event
IDs or producer namespaces yield distinct keys even for equal payloads. Reusing
the raw event ID as ingest_key under two extensions does not namespace it.
Keys accept 1 to 64 ASCII letters, digits, _ or -. Malformed keys fall back
to unkeyed ingestion, so validate them before sending.
The server claims the key and observation in one transaction. A completed replay is skipped; a pending replay resumes downstream work without inserting another observation. Keys expire after 30 days. This is bounded retry protection, not permanent deduplication or an exactly-once guarantee for every wiki side effect.
Ordered batches and recovery
A batch uses the existing array of {url, body} items:
[
{
"url": "/hook?event=user-prompt-submit&agent=claude-code&workspace=team&project=app&extension=example.runtime&source_event=user-prompt-submit&ingest_key=example-runtime-run-17-prompt-4",
"body": {
"session_id": "native-session-17",
"cwd": "/work/app",
"prompt": "Continue the parser fix."
}
}
]
Send this JSON to POST /hook/batch with the normal bearer authentication. The
URL in each item supplies the event query; the server does not fetch that URL.
The short key above assumes the runtime assigned a producer-wide unique run ID.
Use the tuple recipe when event IDs have narrower scope.
- At most 256 items fit in a batch. Existing body limits and sanitization apply.
- Batch processing happens inline, in array order. There is no global ordering across concurrent requests and no all-or-nothing transaction for a batch.
- Prefer
accepted_indiceswhen present. Otherwiseacceptedis the contiguous acknowledged prefix. Preserve every unacknowledged item for retry. - A rate-limited source can be skipped while other sources advance. Inspect
acknowledgements even on HTTP 429 or a partial failure;
failed_indexidentifies a processing failure after earlier skips. An acknowledgement can also mean a deliberate policy drop, not a new observation. - On a timeout or lost response, retry unchanged items and keys. Back off on saturation rather than opening an unbounded number of requests.
Maintain a durable producer-side queue if offline/restart recovery matters. The optional lifecycle relay supplies one through a separate CLI. It accepts events from the orchestrator and sends only the first pending event of each session in a batch. Flush earlier events for a session before sending its terminal event. Serialize delivery within a session where order matters; different sessions can share the server concurrently. Late observations and a later terminal event use existing resumed-session behavior. Stopping a parent does not synthesize endings for its children; the orchestrator must report those lifecycle transitions.
POST /hook remains available, but its 202 response precedes persistence.
Prefer /hook/batch when the producer needs an inline processing acknowledgement.
Boundaries
The variable does not erase or cancel existing spool entries. Explicit
hook-drain and other standalone hooks can still deliver that backlog.
Generated integrations also retain their existing queue disposal behavior,
which can drain previously captured events. Choose the capture owner before
the execution's first event; switching midway cannot deduplicate events already
produced by independent paths.
Manual backfill, the importer, finalize-session and managed transcript import
through ai-memory run retain their existing behavior. Do not combine them as
independent lifecycle producers for the same execution. External capture does
not disable native harness transcript files or other vendors' hooks.
Auth, owner checks, admission rules and sanitization still apply. All items in
one batch share one authenticated identity; separate batches by operator when
representing multiple users. extension is self-declared provenance and the
capture variable grants no permissions. ai-memory has a shared single-tenant
wiki with multi-user attribution, not per-project RBAC or producer isolation.
See multi-user attribution.
No framework adapter, workflow engine, generic plugin runtime or OpenTelemetry collector is introduced. A future OpenTelemetry adapter can map conversation and tool-call identities at the boundary. Keep ingestion producer identity separate from the model provider and preserve the native session identity needed by ai-memory's existing integrations.