Skip to content

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:

ArgumentRequiredDescription
typeyesOne of stop, pre_compact, meeting, manual
contentyesThe body of the event, plain text or markdown
sessionnoSource session ID, if applicable
projectnoProject hint, used by the normalizer to route the record
tagsnoComma-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.

  1. The arguments are validated against the schema.
  2. A unique event ID is generated, retrying on collision.
  3. The event content is composed: YAML frontmatter built from the arguments, followed by the body wrapped under a ## Raw Content heading.
  4. The composed content is written to a temporary file in ~/.cortex/inbox/pending/.
  5. The temp file is os.replace()-ed to the final event path. This is atomic on local POSIX filesystems.
  6. The event ID is returned.

There are no LLM calls, no database writes, no parsing of the body.

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.

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 like stop for 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 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.