Capture
capture is the operation that puts something into Cortex. It is exposed
as both an MCP tool and a CLI utility, and the two share an implementation.
Capture takes a small set of structured arguments:
| Argument | Required | Description |
|---|---|---|
type | yes | One of stop, pre_compact, meeting, manual |
content | yes | The body of the event, plain text or markdown |
session | no | Source session ID, if applicable |
project | no | Project hint, used by the normalizer to route the record |
tags | no | Comma-separated tags that propagate into record frontmatter |
It returns the new event ID. That ID is the only thing the caller needs to know to find or reason about the event later.
What happens on capture
Section titled “What happens on capture”- The arguments are validated against the schema.
- A unique event ID is generated, retrying on collision.
- The event content is composed: YAML frontmatter built from the
arguments, followed by the body wrapped under a
## Raw Contentheading. - The composed content is written to a temporary file in
~/.cortex/inbox/pending/. - The temp file is
os.replace()-ed to the final event path. This is atomic on local POSIX filesystems. - The event ID is returned.
There are no LLM calls, no database writes, no parsing of the body.
Why capture is intentionally minimal
Section titled “Why capture is intentionally minimal”Capture is on the hot path of session lifecycle. Every session that ends
calls capture. Every pre-compact calls capture. If capture is slow or
fragile, the entire session ends slow and fragile.
The minimal implementation has three properties that matter:
- Bounded latency. A single file write completes in milliseconds.
- No external dependencies. The capture script needs only the filesystem. It does not need the SQLite sidecar to be available, the MCP server to be running, or the LLM to be reachable.
- Crash-safe. Atomic rename means a crash mid-write leaves no partial files. The next pass sees either the full event or nothing.
Event types
Section titled “Event types”The type argument tags the source. The normalizer treats some types
differently:
stop— produced by the Stop hook at session end. Typically yields one record summarising the session.pre_compact— produced by the PreCompact hook. Treated likestopfor normalization purposes; tagged separately so it can be filtered out of session counts.meeting— produced by meeting ingest. Yields multiple records per event (the meeting itself, plus action items, decisions, context).manual— explicit user capture. Routed by the project hint if provided, otherwise to a default scope.
New types can be added by extending the validation list and adding a handler in the normalizer. The capture path itself does not need to change.
Capture as the asynchronous boundary
Section titled “Capture as the asynchronous boundary”Capture is the boundary between two halves of the system:
- Above it: session lifecycle, hooks, manual triggers, ingest pipelines. Synchronous, lifetime-bounded, must not fail.
- Below it: normalization, indexing, distillation. Asynchronous, can take time, allowed to fail per event.
The capture file is the message. The inbox is the queue. The normalizer is the consumer.
This is the same pattern as outbox in transactional systems —
write to a durable medium, return success, let a separate process handle
the rest. It is a reliable shape, and it works here for the same reason
it works there.