Skip to content

Latest commit

 

History

History
1740 lines (1257 loc) · 117 KB

File metadata and controls

1740 lines (1257 loc) · 117 KB

← Back to README

Engram — Technical Reference

Persistent memory for AI coding agents

This is the complete technical reference for Engram. For getting started, see the README. For per-agent setup, see Agent Setup.


Quick Navigation

Section What you'll find
Database Schema Tables, FTS5, SQLite config
HTTP API All REST endpoints with request/response details
MCP Tools Detailed reference for all 23 memory tools
MCP Project Resolution Auto-detection algorithm, response envelope, tool categories
Memory Protocol When/how agents should use the tools
Project Name Normalization Auto-detection, normalization, similar-project warnings
Features FTS5 search, timeline, privacy, git sync, compression
TUI Screens, navigation, architecture
Running as a Service systemd setup
Design Decisions Why Go, why SQLite, why no raw auto-capture

For other docs:

Doc Description
Installation All install methods + platform support
Engram Cloud Cloud landing page, quickstart path, branding, and reference links
Agent Setup Per-agent configuration + compaction survival
Codebase Guide Definitive guide to repository structure, package ownership, flows, and maintainer guardrails
Architecture How it works, session lifecycle, CLI reference, project structure
Plugins OpenCode & Claude Code plugin details
Team Usage Scope conventions, language strategy, and sync behavior for collaborative teams
Comparison Why Engram vs claude-mem

Database Schema

Tables

The live schema is created and incrementally migrated by Store.migrate in internal/store/store.go; treat that migration as the source of authority when this summary and the database differ.

  • sessionsid (TEXT PK), project, ownership_mode, directory, started_at, ended_at, summary, runtime_lease_expires_at (local-only runtime liveness; never synced or exported)
  • observationsid (INTEGER PK AUTOINCREMENT), sync_id, session_id (FK), type, title, content, tool_name, project, scope, topic_key, normalized_hash, revision_count, duplicate_count, last_seen_at, pinned, review_after, expires_at, embedding, embedding_model, embedding_created_at, created_at, updated_at, deleted_at
  • observations_fts — FTS5 virtual table synced via triggers (title, content, tool_name, type, project, topic_key)
  • user_promptsid (INTEGER PK AUTOINCREMENT), sync_id, session_id (FK), content, project, created_at; prompt_tombstones records deleted prompt sync_id, session_id, project, and deleted_at
  • prompts_fts — FTS5 virtual table synced via triggers (content, project)
  • sync_chunkstarget_key (TEXT), chunk_id (TEXT), imported_at; composite PK (target_key, chunk_id) for target-scoped chunk tracking
  • sync_state — one row per target_key, with lifecycle, sequence, retry/backoff, lease, error, success, and update metadata; sync_mutations — ordered mutation queue with target, project, entity, operation, payload, source, acknowledgement, and disposition metadata
  • sync_enrolled_projects — enrolled project and enrollment timestamp; cloud_upgrade_state — per-project upgrade stage, repair class, snapshot, findings, actions, error, and update metadata
  • memory_relations — stores conflict-surfacing verdicts from mem_judge; columns include id (INTEGER PK AUTOINCREMENT), sync_id (TEXT UNIQUE), source_id, target_id, relation, judgment_status (pending | judged | orphaned | ignored), provenance, supersession, and timestamp metadata. The SQLite table does not store a project column; project is carried in relation sync payloads and derived from joined observations for project-scoped listing. Syncs across machines via local chunks and via cloud autosync when the project is enrolled.
  • sync_apply_deferred — holds pulled mutations that could not be applied locally due to a missing FK dependency (e.g. relation references an observation not yet present), including target, remote sequence, entity, operation, project, scope, retry, status, and error metadata. Rows with apply_status='dead' have exceeded the retry cap (5 attempts) and will not be retried automatically.

SQLite Configuration

  • WAL mode for concurrent reads
  • Busy timeout 5000ms
  • Synchronous NORMAL
  • Foreign keys ON

HTTP API Endpoints

Project-scoped read migration

Project-aware reads resolve an omitted project to the canonical current project: explicit project, then ENGRAM_PROJECT, then cwd detection. To read across every project, pass all_projects=true; do not combine it with project. CLI counterparts use --all. This intentionally replaces formerly implicit-global behavior for recent lists, review, prompts, export, stats, and conflict inspection. GET /sync/status resolves and validates one current or explicit project but rejects all_projects=true because its provider cannot aggregate project status.

Engram exposes two different runtimes. Keep routes split by runtime:

  • Local runtime (engram serve, JSON on 127.0.0.1:7437 by default, or a POSIX Unix socket when selected)
    • GET /health (local service health)
    • includes memory CRUD/search/context endpoints documented below
    • includes GET /sync/status (local node sync status)
  • Cloud runtime (engram cloud serve)
    • GET /health (cloud service health)
    • GET /sync/pull, GET /sync/pull/{chunkID}, POST /sync/push, POST /sync/mutations/push, GET /sync/mutations/pull (cloud sync transport)
    • GET /dashboard/* HTML routes (browser dashboard)

Dashboard route tree (engram cloud serve):

  • Public
    • GET /dashboard/health — dashboard subsystem health
    • GET /dashboard/login — login surface (authenticated mode), redirects to /dashboard/ when already authenticated
    • POST /dashboard/login — login submit (authenticated mode), redirect-only no-op in insecure mode
    • POST /dashboard/logout — clear session cookie and redirect to login
    • GET /dashboard/static/* — embedded CSS/JS assets
  • Protected (requires dashboard session in authenticated mode; open in insecure mode)
    • GET /dashboard and GET /dashboard/ — dashboard overview
    • GET /dashboard/stats
    • GET /dashboard/activity
    • GET /dashboard/browser
    • GET /dashboard/browser/observations (HX-Request: true returns fragment; plain GET returns full page)
    • GET /dashboard/browser/sessions (HX-Request: true returns fragment; plain GET returns full page)
    • GET /dashboard/browser/sessions/{sessionID}
    • GET /dashboard/browser/prompts (HX-Request: true returns fragment; plain GET returns full page)
    • GET /dashboard/projects
    • GET /dashboard/projects/list — HTMX partial; paginated project list with "Paused" badges
    • GET /dashboard/projects/{project}
    • GET /dashboard/projects/{name}/observations — HTMX partial for project detail
    • GET /dashboard/projects/{name}/sessions — HTMX partial for project detail
    • GET /dashboard/projects/{name}/prompts — HTMX partial for project detail
    • GET /dashboard/contributors
    • GET /dashboard/contributors/list — HTMX partial; paginated contributor list
    • GET /dashboard/contributors/{contributor}
    • GET /dashboard/admin (also requires admin token/session)
    • GET /dashboard/admin/projects
    • GET /dashboard/admin/users (admin-gated)
    • GET /dashboard/admin/users/list (admin-gated; HTMX partial)
    • GET /dashboard/admin/health (admin-gated)
    • POST /dashboard/admin/projects/{name}/sync (admin-gated; toggle sync enabled/disabled)
    • GET /dashboard/admin/projects/{name}/sync/form (admin-gated; HTMX partial)
    • GET /dashboard/admin/audit-log (admin-gated)
    • GET /dashboard/admin/audit-log/list (admin-gated; HTMX partial)
    • GET /dashboard/sessions/{project}/{sessionID} — session detail with observations + prompts sub-lists
    • GET /dashboard/observations/{project}/{sessionID}/{syncID} — observation detail
    • GET /dashboard/prompts/{project}/{sessionID}/{syncID} — prompt detail

Engram is local-first: local SQLite is authoritative; cloud features are optional replication/shared access and enrollment controls.

Mutation materialization attribution

For an accepted POST /sync/mutations/push, each future materialized cloud chunk records the authenticated server principal's display name. If it is blank, the server uses the principal ID, then unknown. The mutation-envelope created_by value cannot control accepted chunk attribution. This is distinct from explicit POST /sync/push, which preserves its chunk created_by metadata. Existing cloud rows are not repaired or rewritten by this behavior.

Health

  • Local runtime (engram serve): GET /health — Returns {"status": "ok", "service": "engram", "version": "0.1.0"}
  • Cloud runtime (engram cloud serve): GET /health — Returns {"status": "ok", "service": "engram-cloud"}

Sessions

  • POST /sessions — Create or renew a runtime session. Body: {id, project, directory, ownership_mode?}
    • ownership_mode accepts shared or project_owned; when omitted it defaults to shared.
    • A successful create or renewal writes a local 30-minute runtime_lease_expires_at without changing the persisted session identity. Leases are local liveness evidence only: they are neither synced nor exported.
    • A project_owned registration cannot reuse a session with a nonblank persisted project different from its requested project. It returns 409 with {error, code:"session_project_conflict", session_id, owner_project, requested_project} and does not mutate the session or local sync journal. Same-project registration remains idempotent; omitted or shared registration retains compatibility for shared sessions.
    • An ended session is terminal: renewal returns 409 and never reopens it. POST /sessions/{id}/end remains the only endpoint that sets ended_at.
    • An invalid non-empty ownership_mode returns 400 and does not create a session.
  • POST /sessions/{id}/end — End session. Body: {summary}
  • GET /sessions/recent — Recent sessions. Query: ?project=X&all_projects=true&limit=N
    • No-result responses return 200 with [] (never null)
  • GET /sessions/{id} — Get single session by ID
  • DELETE /sessions/{id} — Delete session
    • 200 when deleted
    • 404 when session does not exist
    • 409 when session still has observations (delete/migrate observations first)
    • For cloud-enrolled projects: returns 200 and additionally enqueues a session/delete mutation that propagates the deletion to cloud replicas

Observations

  • POST /observations — Add observation. Body: {session_id, type, title, content, tool_name?, project?, scope?, topic_key?}
    • 400 when title or content is missing, empty, or whitespace-only. The observation-create paths (engram save, mem_save, POST /observations) enforce the same title rule because cloud sync rejects observation upserts without a title, and one rejected mutation blocks every later mutation for the project
  • GET /observations — Recent observations compatibility endpoint. Query: ?project=X&all_projects=true&scope=project|personal|global&limit=N&sort=created_at:desc
  • GET /observations/recent — Recent observations. Query: ?project=X&all_projects=true&scope=project|personal|global&limit=N
    • No-result responses from both observation collection endpoints return 200 with [] (never null)
  • GET /observations/{id} — Get single observation by ID
  • PATCH /observations/{id} — Update fields. Body: {title?, content?, type?, project?, scope?, topic_key?}
    • 400 when title or content is provided but empty or whitespace-only. Omitting a field leaves its current value unchanged
  • PUT /observations/{id}/pin — Pin an observation on this device. Returns {id, pinned: true}.
  • DELETE /observations/{id}/pin — Unpin an observation on this device. Returns {id, pinned: false}.
    • Both pin routes are idempotent, return 400 for an invalid ID, and return 404 when the observation does not exist
    • Pin state is local-only for sync: these routes do not change updated_at or enqueue sync work. Direct backups preserve pin state, but shared sync payloads continue to omit it.
  • DELETE /observations/{id} — Delete observation (?hard=true for hard delete, soft delete by default)
    • 200 when deleted
    • 404 when observation does not exist
  • POST /topic-keys/suggest — Suggest a stable topic key using the same heuristic as mem_suggest_topic_key. Body: {type?, title?, content?}. Returns {topic_key}.
    • At least one of title or content must be non-empty; invalid JSON or missing suggestion input returns 400

Review

  • GET /review — List observations due for local review. Query: ?project=X&all_projects=true&limit=N
  • POST /review/mark_reviewed — Reset one observation's local review cycle. Body: {observation_id}; legacy {id} is accepted.
    • 200 with the refreshed observation payload when marked reviewed
    • 400 when observation_id/id is missing or the JSON body is invalid
    • 404 when the observation does not exist
    • Local-only: updating review_after does not enqueue a sync mutation or propagate to other machines.

Search

  • GET /search — FTS5 search. Query: ?q=QUERY&type=TYPE&project=PROJECT&scope=SCOPE&limit=N
    • 200 with a JSON array of search results
    • No-result example: GET /search?q=definitely-no-hit returns 200 with [] (never null)

Timeline

  • GET /timeline — Chronological context. Query: ?observation_id=N&before=5&after=5

Prompts

  • POST /prompts — Save user prompt. Body: {session_id, content, project?}
  • GET /prompts/recent — Recent prompts. Query: ?project=X&all_projects=true&limit=N
  • GET /prompts/search — Search prompts. Query: ?q=QUERY&project=X&all_projects=true&limit=N
    • No-result responses from both prompt collection endpoints return 200 with [] (never null)
  • DELETE /prompts/{id} — Delete prompt
    • 200 when deleted
    • 400 for invalid prompt id
    • 404 when prompt does not exist

Context

  • GET /context — Manual formatted context scoped by project and optional scope. Query: ?project=X&scope=project|personal|global&observations=N&prompts=N&sessions=N&pinned=N&compact=BOOL&max_bytes=N
    • observations/prompts/sessions/pinned: 0 (or omitted/invalid) uses that section's legacy default, >0 caps it (silently clamped to a 500 ceiling), <0 omits the section and its ### ... header entirely
    • compact=true drops the inline content preview from Pinned/Recent Observations bullets, keeping just - [type] **title**
    • max_bytes: 0, omitted, invalid, or negative preserves the unbounded legacy output; a positive value caps the complete rendered context at that many bytes (silently clamped to 65536). Truncation is UTF-8-safe and appends a [truncated] marker when it fits within the requested budget.
    • Invalid or unparseable values silently fall back to their default — never a 400
  • GET /context/compaction — Runtime compaction context scoped strictly to one persisted session. Query: ?session_id=X. The server derives the session project; this endpoint does not accept project or scope selection.

Passive Capture

  • POST /observations/passive — Scan submitted text and persist only structured learnings recognized by the parser. Body: {content, session_id, project?, source?}. General or raw text is not saved as an observation.

Export / Import

  • GET /export — Export current-project data as a versioned JSON backup
    • Optional ?project=<name> selects a known project; ?all_projects=true exports every project
    • Current format 0.2.0 preserves observations (including local pin state), prompts, and complete memory-relation judgment and supersession metadata.
    • 400 for blank, malformed, or conflicting selectors
  • POST /import — Import one JSON backup atomically. Current 0.2.0 backups and legacy 0.1.0 backups that omit pins and relations are accepted; unsupported versions are rejected before mutation. Every imported relation must reference observations present in the same resulting store.

Stats / Diagnostics

  • GET /stats — Current-project memory statistics. Use ?project=<name> or ?all_projects=true to select scope.
  • GET /doctor — Read-only operational diagnostics. Query: ?project=X&check=CHECK_CODE
    • Returns the same diagnostic report envelope as engram doctor --json and MCP mem_doctor
    • project and check are optional; omitted project uses current project detection
    • Unknown explicit projects return 404 with {error, code:"unknown_project", available_projects:[...]}

Project Detection / Migration

  • GET /project/current — Detect the current project. Query: ?cwd=/path/to/repo
    • Always returns a success envelope with {project, project_source, project_path, cwd, available_projects} plus optional warning/error_hint
    • Ambiguous cwd is a successful discovery response: project is empty, project_source is ambiguous, available_projects lists the candidates, and error_hint explains why no project was selected.
    • Other current-project-scoped HTTP routes return 404 for an unknown explicit project, 409 with {error, code:"ambiguous_project", available_projects, project_source, project_path} for an ambiguous cwd, and 400 for an invalid selector or configuration.
    • For automatic Git detection, Engram creates a private versioned binding in the repository's shared Git metadata. It retains the first normalized remote/root label through remote renames, linked worktrees, and repository moves; the binding is not tracked and clones or forks create their own opaque ID. A corrupt or unwritable binding fails closed: configure .engram/config.json with the intended project name rather than relying on a renamed remote. Global local/cloud project_id propagation and alias migration remain deferred.
  • POST /projects/rescue-ownership — Bulk-assign ownership to explicitly selected historical records that carry none. POST /projects/migrate is a deprecated compatibility alias routed to the same handler. The JSON body is limited to 8 KiB: {target_project, confirmed:true, observation_ids?:[], session_ids?:[], prompt_ids?:[]}.
    • A configured ENGRAM_HTTP_TOKEN, matching Authorization: Bearer <token>, target_project, confirmed:true, and at least one positive observation/prompt ID or non-blank session ID are required. Missing server token returns 503; missing or wrong credentials return 401; malformed or invalid requests return 400.
    • This route is a convenience, not the only repair. engram projects rescue-ownership --project <name> [--session <id>] [--observation <id>] [--prompt <id>] performs the same operation against the local store and needs no server token, so ownership stays repairable in a zero-config install.
    • 200 returns {status, complete, blocked, target_project, rescued_observations, rescued_sessions, rescued_prompts, conflicting_records, skipped_records, journaled_local, reconciliation_status}. Owned records are never reassigned.
    • status is rescued when complete is true and everything selected now belongs to target_project, or partially_rescued when something was left behind. blocked then names each item exactly — {kind, id, reason, owned_by} with kind one of session/observation/prompt and reason one of owned_by_other_project, session_owned_by_other_project, dependent_record_owned_by_other_project, missing — so a partial outcome is never inferred from counters.
    • The whole plan is resolved before anything is written: which sessions and which records will move is decided first, then applied. An unowned session that already parents a record owned by a different project is therefore left in place rather than moved out from under it, in either direction. A blank project is treated exactly like NULL — neither identifies an owner — and no sync mutation is ever journaled for a blank-owned record.
    • journaled_local means a canonical pending local mutation exists after the call, whether inserted by the call or already pending. A local journal is not a cloud acknowledgement; autosync reports subsequent reconciliation state.

Conflict Audit (admin — local runtime only)

These endpoints are served by engram serve on the local runtime only. They are not exposed on the cloud runtime. All routes are additive — no existing routes changed.

GET /conflicts

List memory_relations rows with optional filters.

Query params: project (string), all_projects=true (explicit global scope), status (string — raw judgment_status, currently pending | judged | orphaned | ignored), since (RFC3339), limit (int, default 50, max 500 — silently clamped), offset (int, default 0).

Response:

{
  "total": 80,
  "limit": 50,
  "offset": 0,
  "relations": [
    {
      "id": 42,
      "sync_id": "rel-abc123",
      "relation": "conflicts_with",
      "judgment_status": "pending",
      "source_id": "obs-source123",
      "source_title": "Original architecture decision",
      "target_id": "obs-target456",
      "target_title": "Updated architecture decision",
      "created_at": "2026-01-15 12:00:00",
      "updated_at": "2026-01-15 12:30:00"
    }
  ]
}

POST /conflicts/judge

Record a verdict on an existing pending relation surfaced by memory conflict detection.

Body:

{
  "judgment_id": "rel-abc123",
  "relation": "related|compatible|scoped|conflicts_with|supersedes|not_conflict",
  "reason": "optional explanation",
  "evidence": "optional JSON or text evidence",
  "confidence": 0.9,
  "session_id": "optional-session-id"
}

Response:

{ "relation": { "sync_id": "rel-abc123", "judgment_status": "judged" } }

Status codes:

  • 200 when judged
  • 400 for invalid JSON, missing required fields, unknown relation, or invalid relation state

POST /conflicts/compare

Persist an agent-supplied semantic verdict for two observation IDs.

Body:

{
  "memory_id_a": 5,
  "memory_id_b": 6,
  "relation": "related|compatible|scoped|conflicts_with|supersedes|not_conflict",
  "confidence": 0.99,
  "reasoning": "brief explanation",
  "model": "optional-model-id"
}

Response:

{ "sync_id": "rel-abc123" }

not_conflict persists a judged relation and returns its sync_id, preventing future scans from re-evaluating the pair.

Status codes:

  • 200 when accepted
  • 400 for invalid JSON, missing required fields, invalid relation, invalid confidence, or cross-project pairs
  • 404 when either observation ID does not exist

GET /conflicts/{relation_id}

Get full detail for one relation row, including source and target observation snippets.

  • 200 with full relation + source_snippet + target_snippet
  • 404 with a JSON error containing the not-found message when relation_id does not exist
  • 400 with JSON error body when relation_id is not a valid integer

GET /conflicts/stats

Aggregate counts for the current project, an explicit project, or every project when all_projects=true.

Response:

{
  "project": "my-project",
  "by_relation": {
    "conflicts_with": 3,
    "supersedes": 1
  },
  "by_judgment_status": {
    "pending": 3,
    "judged": 1
  },
  "deferred": 4,
  "dead": 1
}

POST /conflicts/scan

Run conflict candidate scan for a project. Synchronous.

Request body:

{
  "project": "my-project",
  "limit": 100,
  "apply": false,
  "max_insert": 100,
  "semantic": false,
  "concurrency": 5,
  "timeout_per_call_seconds": 60,
  "max_semantic": 100
}
  • limit — observations per page (default and maximum 100); rows are ordered by observation ID
  • cursor — optional next_cursor from a completed previous page; omit to start the first page
  • apply: false (default) — dry-run for the non-semantic lexical scan; reports candidates without inserting pending rows
  • apply: true — non-semantic lexical scan inserts new pending relation rows up to max_insert cap (default 100)
  • semantic: true — after FTS5 lexical scan, run LLM-judge semantic detection on the candidate pairs returned by FindCandidates. It does not discover totally lexically unrelated pairs on its own. Requires ENGRAM_AGENT_CLI to be set on the server to claude or opencode.
  • Semantic dry-runs evaluate verdicts without persisting them; not_conflict results are reported as skipped. Applied semantic scans persist every valid verdict, including not_conflict.
  • concurrency — worker pool size for parallel LLM calls when semantic: true (default 5, range 1–20)
  • timeout_per_call_seconds — per-LLM-call timeout in seconds when semantic: true (default 60, range 1–600)
  • max_semantic — hard cap on LLM calls per scan (default 100); scan stops collecting new pairs once reached
  • Omitted project resolves the current project; all_projects:true explicitly scans every project
  • With semantic: true, concurrency outside [1, 20] or timeout_per_call_seconds outside [1, 600] returns 400

Response:

{
  "project": "my-project",
  "inspected": 100,
  "ranked_queries": 100,
  "candidates_found": 5,
  "next_cursor": 520,
  "already_related": 2,
  "inserted": 0,
  "capped": false,
  "dry_run": true,
  "semantic_judged": 0,
  "semantic_skipped": 0,
  "semantic_errors": 0
}

semantic_judged, semantic_skipped, and semantic_errors are always present (zero when semantic: false). next_cursor is present only after every candidate for the completed page has been handled. Scans never auto-loop through pages.

When any scan cap is reached, including max_insert for lexical apply scans or max_semantic for semantic scans, no next_cursor is returned. Re-run from the same incoming cursor with a higher cap; the response includes this warning:

{
  "project": "my-project",
  "inspected": 100,
  "candidates_found": 150,
  "already_related": 0,
  "inserted": 50,
  "capped": true,
  "dry_run": false,
  "semantic_judged": 0,
  "semantic_skipped": 0,
  "semantic_errors": 0,
  "warning": "cap reached: this page has no continuation; rerun from the same cursor with a higher applicable cap"
}

GET /conflicts/deferred

List rows from sync_apply_deferred. Query params: status (string — deferred | dead | applied), limit (int, default 50, max 500), offset (int, default 0; accepted for pagination but not echoed in the response envelope).

Response:

{
  "total": 3,
  "limit": 50,
  "rows": [
    {
      "sync_id": "rel-abc123",
      "entity": "relation",
      "payload": {
        "sync_id": "rel-abc123",
        "source_id": "obs-source123",
        "target_id": "obs-target456",
        "relation": "conflicts_with",
        "judgment_status": "pending",
        "project": "my-project",
        "created_at": "2026-01-15 12:00:00",
        "updated_at": "2026-01-15 12:00:00"
      },
      "payload_raw": "{\"sync_id\":\"rel-abc123\",\"source_id\":\"obs-source123\",\"target_id\":\"obs-target456\",\"relation\":\"conflicts_with\",\"judgment_status\":\"pending\",\"project\":\"my-project\",\"created_at\":\"2026-01-15 12:00:00\",\"updated_at\":\"2026-01-15 12:00:00\"}",
      "payload_valid": true,
      "apply_status": "deferred",
      "retry_count": 2,
      "last_error": "source FK not found",
      "last_attempted_at": "2026-01-15 12:05:00",
      "first_seen_at": "2026-01-15 12:00:00"
    }
  ]
}

POST /conflicts/deferred/replay

Call ReplayDeferred() synchronously. Returns counts of rows processed.

Response:

{
  "retried": 4,
  "succeeded": 3,
  "failed": 0,
  "dead": 1
}

Sync Status (local runtime only)

  • GET /sync/status — Runtime sync-state status for the local node (engram serve only).
  • In engram serve, sync status is wired to persisted SQLite sync state (project-scoped for detected/current project).
  • ?project=<name> selects one known project; ?all_projects=true returns HTTP 400 with code: "unsupported_project_scope".
  • Response fields when provider is injected:
    • enabled
    • phase
    • last_error
    • consecutive_failures
    • backoff_until
    • last_sync_at
    • reason_code
    • reason_message
    • deferred_count — number of pulled mutations awaiting retry (FK dependency not yet local)
    • dead_count — number of pulled mutations that exhausted retries (5 failures) and will not be retried
    • upgrade (nested object)
      • stage
      • reason_code
      • reason_message
  • enabled semantics:
    • true when cloud runtime is configured for the resolved + enrolled project, or when meaningful persisted sync state exists for that resolved project while runtime is not configured.
    • false when no explicit project scope resolves, cloud runtime is malformed/missing, or enrollment/status checks fail.
  • Generic/embedded local server usage may return the fallback enabled=false response if no provider is injected.

Environment Variables

Release update checks are skipped for version, --version, -v, help, --help, and -h. engram tui performs its single update check from inside the TUI. Set ENGRAM_NO_UPDATE_CHECK=1 to disable every update check, including the TUI check.

Variable Description Default
ENGRAM_DATA_DIR Engram CLI data directory. Empty or whitespace-only values use the platform default; nonblank values are used as provided. ~/.engram
ENGRAM_PORT Override HTTP server port. Use an unsigned decimal value from 1 through 65535; invalid values fall back to 7437 in engram serve and Claude Bash hooks. 7437
ENGRAM_SOCKET POSIX-only Unix-domain socket path for engram serve and Claude Bash hooks. Socket mode listens exclusively on this path; it cannot be combined with an explicit ENGRAM_PORT or positional port. The default TCP listener remains unchanged when unset. PowerShell stays TCP-only. Bash hooks warn on stderr if socket transport cannot preserve memory capture. (unset)
ENGRAM_PROJECT Process-level default project override for current project-scoped operations. Precedence: explicit request project (engram save --project, an MCP tool project argument) → process override (engram mcp --project, then ENGRAM_PROJECT) → cwd detection. The value must be a project name, not a path. Explicit/process values are checked against known context when an operation must not establish a bucket; documented creation and recovery writes retain that behavior. Deliberately global operations such as mem_review list with no project and mem_search(all_projects=true) remain global. cwd-detected project
ENGRAM_HTTP_TOKEN Optional Bearer auth for the local HTTP server. When set, DELETE /sessions/{id}, DELETE /observations/{id}, DELETE /prompts/{id}, GET /export, and POST /import require Authorization: Bearer <token>. POST /projects/rescue-ownership (and deprecated alias POST /projects/migrate) always requires a configured token and matching Bearer credential. Comparison is constant-time. Token is read at request time (no restart needed). Other routes remain open when unset (zero-config default). Ownership repair never depends on this token: engram projects rescue-ownership performs the same repair against the local store. (unset — HTTP rescue route not served; CLI repair still available)
ENGRAM_TIMEZONE Timezone for timestamp display in the TUI and cloud dashboard. Accepts any IANA zone name (e.g. America/New_York, Europe/Berlin). Falls back to system local time when unset or invalid. system local
ENGRAM_AGENT_CLI LLM runner name used by engram conflicts scan --semantic and the HTTP /conflicts/scan endpoint. Accepted values: claude, opencode. (unset)
ENGRAM_NO_UPDATE_CHECK Set to 1 to disable GitHub release update checks for every caller, including the TUI. true, yes, and on are also accepted. (unset — eligible commands check for updates)
ENGRAM_CLOUD_AUTOSYNC Set to 1 to enable background autosync. Requires ENGRAM_CLOUD_TOKEN and ENGRAM_CLOUD_SERVER to also be set. (unset — disabled)
ENGRAM_CLOUD_SERVER Cloud server URL used by the autosync manager and engram sync --cloud. (unset)
ENGRAM_DATABASE_URL Postgres DSN for engram cloud serve. (unset)
ENGRAM_CLOUD_HOST Bind host for engram cloud serve. 127.0.0.1
ENGRAM_CLOUD_MAX_PUSH_BYTES Max cloud push payload bytes. 8388608
ENGRAM_CLOUD_TOKEN Bearer token required in authenticated engram cloud serve mode. (unset)
ENGRAM_CLOUD_INSECURE_NO_AUTH Set to 1 for local insecure cloud serve (no auth). Cannot be combined with ENGRAM_CLOUD_TOKEN. (unset)
ENGRAM_CLOUD_ALLOWED_PROJECTS Comma-separated project allowlist enforced by engram cloud serve. Required in both token-auth and insecure modes. Use * to allow all projects (dev/internal deploys) — bypasses per-project name enforcement while still requiring a non-empty project on each request. (unset)
ENGRAM_JWT_SECRET Required in authenticated cloud serve mode. Must be explicitly set to a non-default value. (unset)
ENGRAM_CLOUD_ADMIN Optional legacy dashboard-admin token in authenticated cloud serve mode. It can access dashboard admin read surfaces, project sync controls, and audit logs, but not managed-user, token, or grant mutations. Ignored/rejected in insecure mode. (unset)
ENGRAM_CLOUD_TOKEN_PEPPER Dedicated secret used to hash managed cloud tokens. Required both to issue tokens via engram cloud bootstrap admin --issue-token (and the admin API/dashboard) and to enable managed-token authentication on engram cloud serve. Distinct from ENGRAM_JWT_SECRET on purpose — see Managed users, tokens, and CLI bootstrap. (unset)

Conflict Audit CLI (admin)

The engram conflicts sub-command provides admin/maintainer access to the conflict layer. It is NOT for end users — end users interact with conflicts via the normal agent conversation flow.

When --project is omitted, the cwd-detected project is used.

engram conflicts list [--project <name>] [--status <pending|judged|orphaned|ignored>] [--since <RFC3339>] [--limit <N>]

List memory_relations rows. Output: label-colon aligned columns (id, sync_id, relation, judgment_status, source, target, created_at).

engram conflicts show <relation_id>

Show full detail for one relation: relation_id, sync_id, relation, judgment_status, created_at, updated_at, source_id, source_title, target_id, target_title. Exits non-zero when relation_id does not exist.

engram conflicts stats [--project <name>]

Print aggregate grouped judgment_status counts (pending | judged | orphaned | ignored) plus deferred and dead queue sizes. When relation counts exist, also prints By relation type counts.

engram conflicts scan [--project <name>] [--dry-run] [--apply] [--max-insert <N>]
                      [--since <RFC3339>] [--limit <N>] [--cursor <ID>]
                      [--semantic] [--concurrency <N>] [--timeout-per-call <N>]
                      [--max-semantic <N>] [--yes]

Walk observations for the project, run FindCandidates, and report or insert new pending relation rows.

  • --dry-run (default): for non-semantic lexical scans, reports candidates found with 0 pending rows inserted.
  • --apply: inserts up to --max-insert (default 100) new rows; prints WARNING when cap is reached.
  • --dry-run and --apply are mutually exclusive; combining them in either order exits with an error before opening the store.
  • --since RFC3339: scan only observations created at or after the timestamp.
  • --limit N: inspect 1–100 observations per page (default 100), ordered by observation ID.
  • --cursor ID: resume after a printed next_cursor; no automatic follow-up page is run.
  • --semantic: enable LLM-judge semantic detection on FTS5 candidate pairs returned by FindCandidates. It can improve verdict quality for candidates that share lexical terms, but it does not discover totally lexically unrelated pairs on its own. Requires ENGRAM_AGENT_CLI=claude or ENGRAM_AGENT_CLI=opencode.
  • With --semantic, --dry-run evaluates verdicts without persistence and reports not_conflict as skipped. --apply persists every valid verdict, including not_conflict.
  • --concurrency N: worker pool size for parallel LLM calls (default 5, max 20).
  • --timeout-per-call N: per-LLM-call timeout in seconds (default 60).
  • --max-semantic N: hard cap on LLM calls per scan run (default 100).
  • --yes: skip the cost-estimate confirmation prompt before LLM calls.
engram conflicts deferred [--status <deferred|dead|applied>] [--limit <N>] [--inspect <sync_id>] [--replay]

Inspect or replay the sync_apply_deferred queue.

  • Default: list rows with sync_id, apply_status, retry_count, first_seen_at.
  • --inspect <sync_id>: print full decoded payload for one row; exits non-zero when not found.
  • --replay: call ReplayDeferred() and print retried/succeeded/failed/dead counts.

Cloud CLI (opt-in)

  • engram cloud status — show current cloud config state plus auth/sync readiness without mutating local state. When cloud is configured, also probes the local engram serve daemon at 127.0.0.1:7437 (respects ENGRAM_PORT) and prints a Local daemon: line (running / not running / unreachable) so you can detect a silently dead autosync. The probe currently uses the TCP daemon endpoint, so ENGRAM_SOCKET socket-only mode can report the daemon as not running. Exit code is unaffected; the line is informational
  • engram cloud enroll <project> — enroll one project for cloud replication
  • engram cloud config --server <url> — persist cloud server URL to ~/.engram/cloud.json
  • engram cloud serve — run cloud backend API + dashboard (/dashboard) using Postgres config from env
  • engram cloud upgrade doctor --project <project> — deterministic read-only readiness diagnosis (ready|blocked, class/reason)
  • engram cloud upgrade repair --project <project> [--dry-run|--apply] — deterministic local-safe repair planner/apply (no remote mutation)
  • engram cloud upgrade bootstrap --project <project> [--resume] — resumable checkpointed enroll/push/verify flow
  • engram cloud upgrade status --project <project> — show upgrade stage/class/reason
  • engram cloud upgrade rollback --project <project> — restore pre-upgrade local snapshot before bootstrap_verified; blocked afterwards
  • engram cloud repair materialize-mutations --project <project> (--dry-run|--apply) — explicit server-side Postgres repair that backfills existing cloud_mutations into compatible cloud_chunks without deleting remote data
  • engram cloud bootstrap admin --username <name> [--email <email>] [--grant-project <project>]... [--issue-token [name]] — create the first managed admin (see Managed users, tokens, and CLI bootstrap)
  • engram cloud bootstrap recover-token [--name <name>] [--revoke-existing] — recover the stranded managed admin token state described below

engram sync --cloud --import --project <project> runs in the foreground and prints plain-text import progress that is safe for non-interactive logs. It emits an initial snapshot, bounded event-count-throttled updates, and a final 100% / 0 pending snapshot before the normal import summary. Each snapshot includes local, remote, and pending chunk counts; percentage is based on the pending work captured at import start, so retries do not inflate completion.

Cloud auth token is provided at runtime via ENGRAM_CLOUD_TOKEN (not by a dedicated CLI subcommand). Cloud server startup fails closed when the token is missing unless ENGRAM_CLOUD_INSECURE_NO_AUTH=1 is explicitly set for local insecure development. ENGRAM_CLOUD_INSECURE_NO_AUTH=1 cannot be combined with ENGRAM_CLOUD_TOKEN. Cloud server always requires ENGRAM_CLOUD_ALLOWED_PROJECTS (comma-separated), including insecure mode, so project scope remains server-enforced. ENGRAM_CLOUD_TOKEN + ENGRAM_CLOUD_ALLOWED_PROJECTS are server-side requirements for authenticated mode and must be configured before engram cloud serve (or compose startup). Authenticated mode also requires an explicit non-default ENGRAM_JWT_SECRET; implicit development defaults are rejected. Dashboard requests support browser login in authenticated mode: use /dashboard/login to exchange the bearer token for an HttpOnly dashboard cookie scoped to /dashboard. Protected /dashboard/* HTML routes require that cookie and do not treat raw Authorization: Bearer ... headers as an authenticated browser session. Sync API routes (/sync/pull, /sync/pull/{chunkID}, /sync/push, /sync/mutations/push, /sync/mutations/pull) remain header-auth only. In insecure mode (ENGRAM_CLOUD_INSECURE_NO_AUTH=1 + no ENGRAM_CLOUD_TOKEN), dashboard auth is bypassed and /dashboard/login redirects to /dashboard/.

ENGRAM_CLOUD_ADMIN is optional in authenticated mode. Its sessions can access the existing dashboard admin read surfaces, project sync controls, and audit logs, but managed-user, token, and project-grant mutations require a managed admin token. ENGRAM_CLOUD_ADMIN is rejected in insecure mode (ENGRAM_CLOUD_INSECURE_NO_AUTH=1) to avoid an incoherent admin/browser auth path.

Cloud runtime bind host is controlled by ENGRAM_CLOUD_HOST:

  • default: 127.0.0.1 (local-only, safer default)
  • container/compose: set ENGRAM_CLOUD_HOST=0.0.0.0 so published host ports can reach the cloud server

Cloud runtime envs for engram cloud serve:

Variable Required Notes
ENGRAM_DATABASE_URL yes Postgres DSN for cloud chunk storage/dashboard read model
ENGRAM_PORT no Runtime port (default 8080)
ENGRAM_CLOUD_HOST no Bind host (default 127.0.0.1; use 0.0.0.0 for containers)
ENGRAM_CLOUD_MAX_PUSH_BYTES no Max chunk/mutation push request body bytes (default 8388608)
ENGRAM_CLOUD_ALLOWED_PROJECTS yes Comma-separated allowlist; always required (authenticated + insecure modes). Use * to allow all projects (dev/internal deploys) — bypasses per-project name enforcement while still requiring a non-empty project on each request.
ENGRAM_CLOUD_TOKEN yes (authenticated mode) Enables bearer auth mode
ENGRAM_JWT_SECRET yes (authenticated mode) Must be explicitly set and non-default when token mode is enabled
ENGRAM_CLOUD_INSECURE_NO_AUTH no Set to 1 only for local insecure mode; cannot be combined with ENGRAM_CLOUD_TOKEN
ENGRAM_CLOUD_ADMIN no Optional legacy dashboard-admin token for read surfaces, project sync controls, and audit logs; managed-user mutations require a managed admin token; rejected in insecure mode
ENGRAM_CLOUD_TOKEN_PEPPER no (required to enable managed-token auth) Dedicated managed-token hashing secret. Must differ from ENGRAM_JWT_SECRET. Required both by engram cloud bootstrap admin --issue-token and by engram cloud serve to accept managed tokens at runtime (see below).

Managed users, tokens, and CLI bootstrap

engram cloud bootstrap admin creates the first managed admin — a principal record stored in the cloud Postgres database (cloud_principals / cloud_human_users), independent from the legacy ENGRAM_CLOUD_TOKEN / ENGRAM_CLOUD_ADMIN env-token model:

# Create the first managed admin (safe: refuses to create a duplicate first admin)
engram cloud bootstrap admin --username alice

# Also grant project access and issue a sync token in the same command
engram cloud bootstrap admin --username alice \
  --grant-project my-project \
  --issue-token first-token
  • --username is required; --email is optional.
  • --grant-project <project> may be repeated to grant one or more projects (managed principals are deny-by-default: no grants means no sync access).
  • --issue-token [name] issues a managed bearer token and prints the raw token exactly once in the command output. It is never logged, persisted, or re-printed — store it immediately. Issuing a token requires ENGRAM_CLOUD_TOKEN_PEPPER to be set to a dedicated secret (distinct from ENGRAM_JWT_SECRET); the command fails clearly, before creating anything, if the pepper is missing.
  • Running the command again once a managed admin already exists is rejected (no silent duplicate first-admin creation); the attempt is still recorded as a denied bootstrap.cli audit event.
  • Every bootstrap attempt (accepted or denied) writes a bootstrap.cli audit event to cloud_auth_audit_log, with the same non-secret metadata rules (no raw tokens, hashes, or bearer headers) as every other cloud auth audit event.
  • Grant/role/duplicate-admin validation reuses the exact same cloudstore methods and last-admin guard used by the dashboard's own first-admin bootstrap flow — there is no parallel/looser bootstrap path.

If a historical failed bootstrap left exactly one enabled managed human admin, retained its grants, and created no principal token anywhere in the deployment, run the explicit recovery command:

engram cloud bootstrap recover-token --name replacement

It requires ENGRAM_CLOUD_TOKEN_PEPPER, preserves existing grants, and prints the recovered raw token exactly once only after the token and its bootstrap.cli recovery audit event commit together. It refuses all other states, including multiple enabled managed human admins or any existing principal token; it does not create users, grants, or partial tokens.

If the bootstrap token was persisted but its one-time output was lost, explicitly opt in to replacement:

engram cloud bootstrap recover-token --name replacement --revoke-existing

This path requires exactly one enabled managed human admin and exactly one active principal token. That token must be unrevoked, never used, and owned by the sole enabled admin; the command atomically revokes it and creates the replacement while preserving grants. Earlier revoked recovery tokens may remain as audit history. If the replacement output is lost again, retry --revoke-existing while the current token remains unused; once it has been used, this escape hatch never replaces it. The command refuses used, revoked/inactive, ambiguous, or multiple-active-token states.

Runtime authentication: engram cloud serve resolves managed tokens first, then falls back to the legacy env-token credentials (ENGRAM_CLOUD_TOKEN for sync and ENGRAM_CLOUD_ADMIN for dashboard access), on every /sync/*, /admin/*, and dashboard-login request. Authentication does not grant managed-user mutation authority: only a managed-token admin principal may create or enable/disable users, create or revoke tokens, or create or revoke project grants.

  • Set ENGRAM_CLOUD_TOKEN_PEPPER to enable managed-token authentication. A token issued by engram cloud bootstrap admin --issue-token (or by the dashboard//admin/* token-create routes) then authenticates directly against /sync/* and /admin/*, and can log into the dashboard as its resolved principal/role.
  • If ENGRAM_CLOUD_TOKEN_PEPPER is not set, managed-token authentication is simply disabled: the server still starts normally, and ENGRAM_CLOUD_TOKEN / ENGRAM_CLOUD_ADMIN continue to authenticate exactly as before (legacy-only mode).
  • Managed principals are deny-by-default for project sync: a managed token only reaches projects explicitly granted via --grant-project (or the dashboard//admin/* grant routes). Legacy ENGRAM_CLOUD_TOKEN keeps its existing ENGRAM_CLOUD_ALLOWED_PROJECTS allowlist behavior, unaffected by managed grants.
  • Disabled managed users, revoked managed tokens, and revoked project grants stop authenticating/authorizing on the very next request — no server restart required.
  • No rollback action is required to keep using legacy credentials: legacy ENGRAM_CLOUD_TOKEN continues to use its existing sync allowlist, and ENGRAM_CLOUD_ADMIN continues to provide dashboard read access, project sync controls, audit logs, and the first-admin dashboard bootstrap entry point whether or not ENGRAM_CLOUD_TOKEN_PEPPER is configured. The CLI recovery command remains limited to its documented stranded-admin state. Use a managed admin token for managed-user administration.

Managed admin API response JSON

The managed-admin API returns snake_case JSON keys for user and grant objects:

  • POST /admin/users returns one user object and GET /admin/users returns an array of user objects. Each object contains principal_id, username, email, display_name, role, enabled, and created_at.
  • POST /admin/users/{principalID}/grants returns one grant object and GET /admin/users/{principalID}/grants returns an array of grant objects. Each object contains principal_id, project, granted_by_principal_id, and created_at.

Cloud sync is still local-first and explicit:

# Explicit cloud sync call
engram sync --cloud --project my-project

# Optional env toggle for cloud mode in sync command
ENGRAM_CLOUD_SYNC=1 engram sync --status --project my-project

When engram sync --cloud --project <project> or autosync hits a known repairable cloud sync/upsert/canonicalization failure, Engram preserves the original error and appends guidance to run:

Cloud Upgrade Flow

engram cloud upgrade doctor --project <project>
engram cloud upgrade repair --project <project> --dry-run
engram cloud upgrade repair --project <project> --apply
engram sync --cloud --project <project>

Sync/autosync never auto-applies repairs; only the explicit repair --apply command mutates local repairable upgrade state.

When cloud sync receives policy_forbidden, Engram preserves the server's denied project message and advises the server administrator to check ENGRAM_CLOUD_ALLOWED_PROJECTS. A managed principal's project grant may also need checking; the client does not expose allowlist contents.

For cloud servers that already accepted mutation pushes before mutation payloads were materialized into chunk history, run the server-side backfill against the Postgres DSN used by engram cloud serve:

ENGRAM_DATABASE_URL='postgres://...' engram cloud repair materialize-mutations --project <project> --dry-run
ENGRAM_DATABASE_URL='postgres://...' engram cloud repair materialize-mutations --project <project> --apply

The backfill is project-scoped, non-destructive, and idempotent: it inserts missing compatible chunks and leaves existing cloud_mutations and chunks in place.

engram cloud serve also runs this materialization repair automatically for every configured ENGRAM_CLOUD_ALLOWED_PROJECTS entry at startup. The explicit repair command remains available for operator verification, dry-runs, and re-running a project after an upgrade.

Local Cloud Bring-Up (Docker + Postgres)

# 1) SERVER-SIDE startup requirements (configure before startup)
# docker-compose.cloud.yml includes defaults for browser-demo smoke usage:
# ENGRAM_CLOUD_INSECURE_NO_AUTH=1
# ENGRAM_CLOUD_ALLOWED_PROJECTS=smoke-project
docker compose -f docker-compose.cloud.yml up -d

# source-run flow (without compose): set BOTH token + allowlist before startup
# ENGRAM_DATABASE_URL="postgres://engram:engram_dev@127.0.0.1:5433/engram_cloud?sslmode=disable" \
# ENGRAM_JWT_SECRET="replace-with-32+-byte-random-secret" \
# ENGRAM_CLOUD_TOKEN="your-token" \
# ENGRAM_CLOUD_ALLOWED_PROJECTS="my-project" \
# engram cloud serve

# 2) CLIENT-SIDE CLI setup
# compose runtime flow: published :18080
engram cloud config --server http://127.0.0.1:18080
# compose runtime default is insecure local-dev mode; keep token unset
# client sync preflight only requires the configured cloud server URL; no
# client-side ENGRAM_CLOUD_INSECURE_NO_AUTH flag is required for compose flow
unset ENGRAM_CLOUD_TOKEN

# 3) Enroll project + run explicit cloud sync
engram cloud enroll smoke-project
engram cloud upgrade doctor --project smoke-project
engram cloud upgrade repair --project smoke-project --dry-run
engram cloud upgrade repair --project smoke-project --apply
engram cloud upgrade bootstrap --project smoke-project --resume
engram cloud upgrade status --project smoke-project
engram sync --cloud --status --project smoke-project

# source-run client endpoint (without compose): default :8080
# engram cloud config --server http://127.0.0.1:8080

# cloud mode enforces a single explicit project scope
# engram sync --cloud --all  # blocked by design

Deterministic reason codes shared across store/CLI/server:

  • blocked_unenrolled
  • auth_required
  • cloud_config_error
  • policy_forbidden — check the server-side ENGRAM_CLOUD_ALLOWED_PROJECTS policy for the denied project; a managed principal's project grant may also need checking. The client does not expose allowlist contents.
  • paused
  • transport_failed

Cloud Status Visibility Matrix

Cloud failure visibility must stay deterministic across supported surfaces:

Scenario Expected deterministic reason Surfaces
Unconfigured cloud sync preflight (missing server URL) cloud_config_error CLI stderr
Cloud runtime not configured in status provider (takes precedence even if project scope is unresolved) cloud_not_configured /sync/status
/sync/status project cannot be resolved (no query/default project) while cloud runtime is configured project_required /sync/status
Unenrolled project cloud sync blocked_unenrolled CLI stderr + /sync/status
Runtime auth/policy failure from remote API auth_required / policy_forbidden CLI stderr + /sync/status
Explicit paused state paused /sync/status
Remote/network failure transport_failed CLI stderr + /sync/status

engram sync --cloud --status --project <name> is read-only: it does not mutate /sync/status lifecycle fields.

Machine-actionable validation/policy failures from cloud sync routes include:

  • error_class (repairable | blocked | policy | invalid_request)
  • error_code (stable deterministic code)
  • error (human-readable message)

This envelope is used consistently by /sync/push validation/control failures and by /sync/pull / /sync/pull/{chunkID} project-required or policy failures. /sync/mutations/push uses the envelope for empty batches, empty projects, project policy failures, and pause-control failures; relation-payload validation currently returns error, reason_code, and invalid instead. /sync/mutations/pull success responses include the project envelope, but internal listing errors currently use plain http.Error.


MCP Project Resolution

Engram resolves the project at MCP tool call time. The default source is the server process working directory (cwd), not MCP startup state, but some write tools have stronger context: mem_session_start(directory=...) resolves from the provided directory, and mem_save may use a validated explicit project or an existing session_id project before falling back to cwd detection. The explicit field is treated as a validated selection, not a free-form creation hint. This eliminates project drift caused by agents supplying different names for the same repo.

Detection algorithm

Case Condition Source Project
1 nearest .engram/config.json exists within the enclosing git root, or at cwd outside git config project_name from config
2 cwd is inside a git repo that currently has an origin remote git_remote if the binding is absent, initialize it from the remote repo name; otherwise reuse the stored binding label
3 cwd is inside a git repo that currently has no origin remote git_root if the binding is absent, initialize it from the git-root basename; otherwise reuse the stored binding label
4 cwd has exactly one git-repo child git_child child repo name (warning included)
5 cwd has multiple git-repo children ambiguous error — write tools fail fast
6 no git repo near cwd dir_basename basename of cwd

Child scan constraints: depth=1, max 20 entries, 200ms timeout, skips hidden dirs and noise dirs (node_modules, vendor, .venv, __pycache__, target, dist, build, .idea, .vscode).

The Git binding is private to each clone and shared by that clone's linked worktrees. Independent clones and forks establish fresh opaque bindings. Cross-clone identity sharing and alias propagation are not currently supported.

Initialize an explicit project identity

Use engram init [project_name] [--force] to write .engram/config.json in the current directory. When project_name is omitted, Engram uses the current directory basename. --force replaces an existing config; without it, init stops and tells you that the config already exists.

This is the explicit resolution path for a non-Git aggregator workspace that contains multiple child repositories. Run engram init aggregator-name at the aggregator root so its config resolves the workspace identity before child-repository scanning reports ambiguity.

Response envelope

Most successful MCP tool responses use this envelope:

{
  "project": "engram",
  "project_source": "git_remote",
  "project_path": "/home/user/engram",
  "result": "...(tool output)..."
}

Error responses include available_projects when the error is ambiguous_project or unknown_project.

When a Git repository binding cannot be read or created, MCP returns repository_binding_unavailable with guidance to configure the repository's .engram/config.json with the intended canonical project. This is not an ambiguity and does not include ambiguity recovery tokens.

Exceptions:

  • mem_current_project returns detection fields directly (project, project_source, project_path, cwd, available_projects, optional warning / error_hint) and does not wrap them in result.
  • mem_doctor returns the same JSON report shape as engram doctor --json; it uses read-project resolution before running diagnostics but does not wrap the report in the common MCP envelope.

Write tools (explicit/session/cwd project resolution)

mem_session_start resolves from its explicit directory argument when supplied; otherwise it auto-detects from cwd. mem_session_end and mem_capture_passive auto-detect project from cwd; any project argument the LLM sends to them is ignored. mem_session_summary supports explicit project override (project, project_choice_reason, recovery_token) matching mem_save's project resolution.

mem_update uses ID-based updates and auto-detects project only for response envelope metadata. Its public schema does not expose project; raw legacy clients may still send a non-empty project argument, and the handler tolerates it as an observation project update for compatibility.

mem_save resolves writes by precedence: validated explicit project, project already associated with session_id, repo/cwd detection (nearest .engram/config.json within the enclosing git root, git remote/root/child), then directory-basename fallback.

Guardrails:

  • Invalid explicit project names fail loudly instead of silently falling back.
  • Valid-looking explicit project names are accepted only when backed by known context: an existing local project in the store, a matching existing session project, the nearest resolvable .engram/config.json, or exact ambiguous-project recovery after the user selected one available project.
  • An unbacked explicit project fails loudly and does not create a new bucket.
  • If a non-empty session_id is supplied and no session exists, mem_save fails with a structured error and does not write.
  • If both explicit project and session_id are supplied, they must resolve to the same normalized project or mem_save fails with a structured error and does not write.
  • An explicit session_id is authoritative. When a write omits it, Engram uses the current process directory only to narrow active non-manual runtime sessions for the resolved project. A valid, unexpired local lease takes precedence over legacy unleased rows in the same directory; every live leased owner remains a candidate, so multiple live leases fail closed. Expired, malformed, and nonblank invalid leases are excluded. Only when a directory has no live lease do unleased rows use the legacy seven-day effective-activity fallback (latest observation, then started_at). This precedence is applied independently for every requested directory. Engram attaches to a session only when exactly one candidate remains, uses the project manual-save session when none remain, and fails closed when multiple candidates remain rather than selecting by recency. Selection is read-only and never changes ended_at. Directory is not session identity; callers with concurrent sessions must supply session_id, end other active matching sessions, or save independently with engram save "TITLE" "CONTENT" --project PROJECT --type TYPE --topic TOPIC_KEY. The CLI fallback writes to an independent project manual-save session and does not bind it to the current MCP session. Claude Code currently may require ending other active matching sessions because its MCP transport does not expose runtime identity to each tool call.
  • project_choice_reason=user_selected_after_ambiguous_project is only honored when cwd resolution is actually ambiguous. On a non-ambiguous cwd, stale recovery flags do not override explicit-project precedence or session mismatch validation.
  • If ambiguous-project recovery is active, project must exactly match one of the previously returned available_projects; invented or normalized guesses are rejected.
  • Exact ambiguous-project choices can still fail with project_name_collision when multiple available names collapse to the same stored project bucket after normalization. Rename or disambiguate the colliding projects before retrying.
  • Ordinary explicit mem_save(project=...) calls can also fail with project_name_collision when the raw explicit name collapses into an existing config-backed, session-backed, or store-backed project bucket, such as foo--bar colliding with foo-bar.

For monorepos, detection now honors the nearest .engram/config.json at or below the enclosing git root. That lets repo/backend/.engram/config.json and repo/frontend/.engram/config.json behave as independent projects without letting ~/.engram/config.json leak into nested workspaces.

mem_save_prompt keeps the older cwd/default behavior by default and only uses project for the narrow ambiguous-project recovery override: after a previous ambiguous_project error, the agent may retry with project=<one of available_projects> and project_choice_reason=user_selected_after_ambiguous_project.

Read tools (optional project override)

mem_search, mem_context, mem_timeline, mem_stats, mem_doctorproject is an optional argument. If supplied, it is validated against the store via ProjectExists. Unknown project names return a structured error with available_projects. mem_get_observation resolves project from cwd for envelope metadata and does not accept a project override.

Admin tools

mem_delete is ID-based and requires id; optional hard_delete=true permanently deletes the observation. It does not accept or auto-detect project.

mem_merge_projects requires from (comma-separated source project names) and to (canonical target project name). It does not accept or auto-detect project.

mem_current_project

Use mem_current_project as the first call in a session to inspect the detection result:

{
  "project": "engram",
  "project_source": "git_remote",
  "project_path": "/home/user/engram",
  "cwd": "/home/user/engram",
  "available_projects": [],
  "warning": ""
}

Returns success even when cwd is ambiguous — empty project + non-empty available_projects signals the agent to navigate to a specific repo before writing.


MCP Tools (23 tools)

mem_search

Search persistent memory across all sessions. Supports FTS5 full-text search with type/project/scope/limit filters.

Set all_projects: true to search across every project instead of the resolved one. This bypasses project detection entirely and ignores the project argument, so an agent can recall a decision logged elsewhere without knowing the project key. The response envelope reports project_source: "all_projects" and an empty project to reflect the cross-project scope.

Scope values accepted by the scope parameter: project (default), personal, global. When scope: personal is passed without an explicit project override, the project filter is cleared and personal observations are searched across all projects (cross-project personal scope).

Each structured search result includes lifecycle metadata: state (active or needs_review) and, when set, review_after. Text output also appends state: needs_review for stale observations.

When an observation has judged relations in memory_relations, the result entry includes annotation lines immediately after the title/content block:

supersedes: #<id> (<title>)       — this memory supersedes another
superseded_by: #<id> (<title>)    — another memory supersedes this one
conflicts: #<id> (<title>)        — judged conflict with another memory
conflict: contested by #<id> (pending)  — pending (not yet judged)

Multiple annotation lines appear when multiple relations apply — one per related observation. Titles are retrieved via JOIN (no N+1 queries). When the related observation has been deleted, (deleted) replaces the title. Agent parsers should match by prefix — these prefixes are stable across versions (REQ-012).

Pending relations (from mem_save conflict surfacing, before mem_judge is called) produce the conflict: contested by #<id> (pending) form. Judged relations produce the enriched form with title.

mem_save

Save structured observations. The tool description teaches agents the format:

  • title: Short, searchable (e.g. "JWT auth middleware")
  • type: decision | architecture | bugfix | pattern | config | discovery | learning
  • scope: project (default) | personal | global — see Team Usage for conventions and sync caveats
  • topic_key: optional canonical topic id (e.g. architecture/auth-model) used to upsert evolving memories
  • capture_prompt: optional boolean, default true; when current prompt context is available in the same MCP process for the same project/session, Engram best-effort records it alongside the observation. If that process-local context is unavailable or prompt capture fails, mem_save still succeeds. Automated artifact saves should pass false.
  • content: Structured with **What**, **Why**, **Where**, **Learned**; required unless the legacy observation alias is provided
  • observation: backward-compatible alias for content for older/raw MCP clients; prefer content for new integrations

Exact duplicate saves are deduplicated in a rolling time window using a normalized content hash + project + scope + type + title. When topic_key is provided, mem_save upserts the latest observation in the same project + scope + topic_key, incrementing revision_count and attributing it to the latest writer session. Save responses include lifecycle metadata for the saved observation: computed state (active or needs_review) and review_after when the observation type has a review cycle. Content is redacted before the configured storage limit is applied; that limit and truncation metadata (original_bytes, limit_bytes) are UTF-8 bytes. MCP save/update responses include truncated, and warn when truncation occurs.

mem_update

Update an observation by ID. Public schema supports partial updates for title, content, type, scope, and topic_key. For legacy/raw MCP clients, a non-empty project argument is still tolerated by the handler even though it is not exposed in the schema.

mem_review

Review observation lifecycle state. Available in the agent profile (engram mcp --tools=agent).

Actions:

  • action: "list" — returns observations whose review_after has passed. Optional parameters: project and limit (default 10).
  • action: "mark_reviewed" — requires observation_id; resets that observation's local review cycle using its type decay policy. The legacy id alias is accepted for compatibility.

mark_reviewed is local-only for now: review_after is intentionally not part of sync payloads in this phase, so resetting the review cycle does not enqueue a sync mutation or propagate to other machines.

mem_pin

Pin a local observation so it appears before recent observations in memory context. Pinned state is local to this device and is not synced. Available in the agent profile (engram mcp --tools=agent).

Parameters:

  • id (required): int — observation ID to pin

Returns { "result": "Memory #N pinned", "id": N, "sync_id": ..., "pinned": true }. Idempotent: pinning an already-pinned observation succeeds without change. Errors: missing/zero id ("id is required"), or a store failure ("Failed to update pin state: ...").

mem_unpin

Unpin a local observation so it only appears in normal recency order in memory context. Pinned state is local to this device and is not synced. Available in the agent profile (engram mcp --tools=agent).

Parameters:

  • id (required): int — observation ID to unpin

Returns { "result": "Memory #N unpinned", "id": N, "sync_id": ..., "pinned": false }. Idempotent: unpinning an already-unpinned observation succeeds without change. Errors: missing/zero id ("id is required"), or a store failure ("Failed to update pin state: ...").

mem_suggest_topic_key

Suggest a stable topic_key from type + title (or content fallback). Uses family heuristics like architecture/*, bug/*, decision/*, etc. Use before mem_save when you want evolving topics to upsert into a single observation.

mem_delete

Delete an observation by ID. Uses soft-delete by default (deleted_at); optional hard-delete for permanent removal.

mem_save_prompt

Save user prompts — records what the user asked so future sessions have context about user goals. It applies the same post-redaction byte limit and truncation metadata as mem_save; mem_save_prompt warns when it truncates. When called in the same MCP process, this also feeds process-local current prompt context used by later mem_save calls with capture_prompt=true. The same MCP process lifecycle must receive the prompt context before the later save; prompt capture is best-effort and mem_save still succeeds when no context is available.

mem_context

Get recent memory context from previous sessions — shows sessions, prompts, and observations, with optional scope filtering for observations.

When project is omitted, context is scoped to the resolved current project (process override before cwd detection). This is not an all-project query. scope: personal without an explicit project retains its cross-project personal-memory behavior.

Scope values accepted by the scope parameter: project (default), personal, global. When scope: personal is passed without an explicit project override, the project filter is cleared and personal observations are returned across all projects (cross-project personal scope).

MCP mem_context uses a 16 KiB default budget for the complete tool result and caps max_bytes at 64 KiB. max_bytes must be a positive integral number; absent, mistyped, non-positive, NaN, and fractional values fall back to the 16 KiB default. It includes at most 20 pinned observations by default. When the complete result exceeds its budget, truncation is UTF-8-safe and appends a visible [truncated] marker when the marker fits. compact=true removes inline content previews from pinned and recent-observation bullets, retaining their type and title. These MCP rules are distinct from the HTTP GET /context behavior documented above.

mem_stats

Show memory system statistics — sessions, observations, prompts, projects.

mem_timeline

Progressive disclosure: after searching, drill into chronological context around a specific observation. Shows N observations before and after within the same session. The optional project filter is enforced: an observation owned by another project is not returned.

mem_get_observation

Get full untruncated content of a specific observation by ID.

mem_session_summary

Save comprehensive end-of-session summary:

## Goal
## Instructions
## Discoveries
## Accomplished
## Next Steps
## Relevant Files

mem_session_start

Register the start of a new coding session.

If the supplied session ID has already ended, the request is rejected with the structured error code session_already_ended, which includes that session ID. Choose a new session ID to continue; ended sessions cannot be reopened. New session IDs and IDs for active sessions remain accepted.

mem_session_end

Mark a session as completed with optional summary.

mem_capture_passive

Extract structured learnings from text output. Looks for ## Key Learnings: sections and saves each numbered/bulleted item as a separate observation. Duplicates are automatically skipped.

mem_merge_projects

Admin tool. Merge multiple project name variants into a single canonical name. Requires from as a comma-separated list of source project names and to as the target canonical name. All observations, sessions, and prompts from the source projects are reassigned to the canonical project.

mem_current_project

Detect the current project from the working directory. Returns project, project_source, project_path, cwd, available_projects, and warning. Never returns an error — even on ambiguous cwd it returns success with an empty project and non-empty available_projects. Recommended as the first call when starting a session.

mem_list_projects

List every project known to Engram with per-project observation_count, session_count, prompt_count, and known directories, ordered by observation count descending — the same view as engram projects list. Returns { "projects": [...], "count": N }.

Included in the agent profile; engram mcp registers all tools by default, so --tools=agent is not required to use it.

Result semantics:

  • Empty store — successful response with { "projects": [], "count": 0 }. Discovery never fails just because nothing is stored yet.
  • Store-query failure — returns a tool error (List projects failed: ...) instead of a success envelope, so the agent knows discovery failed rather than trusting an empty answer.

Use it for cross-project discovery when the working directory matches no known project, then scope mem_search/mem_context to the chosen project.

mem_doctor

Run read-only operational diagnostics. Returns the same JSON report shape as engram doctor --json, with optional project and check filters. The optional project override is validated with read-project resolution before diagnostics run.

mem_judge

Record a verdict on a pending memory conflict. When mem_save returns candidates[] and judgment_required: true, the agent inspects the candidates and calls mem_judge to mark the relation between the saved memory and a candidate.

Parameters:

  • judgment_id (required): the judgment_id returned by mem_save
  • relation (required): related | compatible | scoped | conflicts_with | supersedes | not_conflict
  • reason (optional): short text explaining the verdict
  • evidence (optional): free-form text or JSON the agent can use to justify the call (e.g., quoted excerpts from both memories)
  • confidence (optional, default 1.0): 0.0–1.0; if the value is below 0.7 the agent SHOULD ask the user before calling

Re-judging an existing relation overwrites it (deliberate revision). Two agents judging the same pair persist as separate rows — Phase 1 surfaces both; cross-actor reconciliation is Phase 2.

Search results subsequently expose annotation lines like supersedes: #<id> (<title>), superseded_by: #<id> (<title>), and conflicts: #<id> (<title>) so the recalling agent sees relevant verdicts at-a-glance. For enrolled projects with autosync enabled, judgments propagate to other machines via the cloud mutation pipeline — the annotation appears in mem_search results on any machine that has pulled the relevant mutations.

mem_compare

Records a verdict on a semantic comparison between two memories. The agent reads both memories, judges the relationship using its LLM reasoning, and calls mem_compare to persist the verdict. Unlike mem_judge (which resolves a pre-existing pending candidate surfaced by mem_save), mem_compare creates a new relation row directly — useful for proactive semantic analysis that goes beyond FTS5 lexical matching.

Available in the agent profile (engram mcp --tools=agent).

Parameters:

  • memory_id_a (required): int — observation ID of the first memory
  • memory_id_b (required): int — observation ID of the second memory
  • relation (required): string — one of conflicts_with | supersedes | scoped | related | compatible | not_conflict
  • confidence (required): float 0.0..1.0
  • reasoning (required): string — explanation of the verdict (max 200 chars)
  • model (optional): string — model name for provenance (e.g. "claude-haiku-4-5")

Behavior:

  • Persists a relation row via JudgeBySemantic with system provenance (marked_by_kind="system", marked_by_actor="engram")
  • Idempotent: the same (source_id, target_id) pair updates the existing row rather than inserting a duplicate
  • not_conflict verdicts persist as judged relations, suppressing future candidate scans without appearing in conflict-facing lists or statistics
  • Cross-project relations are rejected with an error

Memory Protocol

The Memory Protocol teaches agents when and how to use Engram's MCP tools. Without it, the agent has the tools but no behavioral guidance. Add this to your agent's prompt file (see Agent Setup for per-agent locations).

WHEN TO SAVE (mandatory)

Call mem_save IMMEDIATELY after any of these:

  • Bug fix completed
  • Architecture or design decision made
  • Non-obvious discovery about the codebase
  • Configuration change or environment setup
  • Pattern established (naming, structure, convention)
  • User preference or constraint learned

Format for mem_save:

  • title: Verb + what — short, searchable (e.g. "Fixed N+1 query in UserList", "Chose Zustand over Redux")

  • type: bugfix | decision | architecture | discovery | pattern | config | preference

  • scope: project (default) | personal | global

  • topic_key (optional, recommended for evolving decisions): stable key like architecture/auth-model

  • content:

    **What**: One sentence — what was done
    **Why**: What motivated it (user request, bug, performance, etc.)
    **Where**: Files or paths affected
    **Learned**: Gotchas, edge cases, things that surprised you (omit if none)
    

Topic update rules (mandatory)

  • Different topics must not overwrite each other (e.g. architecture vs bugfix)
  • Reuse the same topic_key to update an evolving topic instead of creating new observations
  • If unsure about the key, call mem_suggest_topic_key first and then reuse it
  • Use mem_update when you have an exact observation ID to correct

DELIVERY GUARANTEE

Memory operations are internal bookkeeping, never the user-facing answer. Complete required memory work before composing the completed-task reply; send the complete answer as the final message of the turn with no later tool calls. If memory work fails or needs follow-up, still send the answer.

WHEN TO SEARCH MEMORY

When the user asks to recall something — any variation of "remember", "recall", "what did we do", "how did we solve", "recordar", "acordate", or references to past work:

  1. First call mem_context — checks recent session history (fast, cheap)
  2. If not found, call mem_search with relevant keywords (FTS5 full-text search)
  3. If you find a match, use mem_get_observation for full untruncated content

Also search memory PROACTIVELY when:

  • Starting work on something that might have been done before
  • The user mentions a topic you have no context on — check if past sessions covered it

SESSION CLOSE PROTOCOL (mandatory)

Before ending a session or saying "done" / "listo" / "that's it", you MUST call mem_session_summary with this structure:

## Goal
[What we were working on this session]

## Instructions
[User preferences or constraints discovered — skip if none]

## Discoveries
- [Technical findings, gotchas, non-obvious learnings]

## Accomplished
- [Completed items with key details]

## Next Steps
- [What remains to be done — for the next session]

## Relevant Files
- path/to/file — [what it does or what changed]

This is NOT optional. If you skip this, the next session starts blind.

PASSIVE CAPTURE

When completing a task, include a ## Key Learnings: section at the end of your response with numbered items. Engram will automatically extract and save these as observations.

Example:

## Key Learnings:

1. bcrypt cost=12 is the right balance for our server performance
2. JWT refresh tokens need atomic rotation to prevent race conditions

You can also call mem_capture_passive(content) directly with any text that contains a learning section.

AFTER COMPACTION

If you see a message about compaction or context reset:

  1. IMMEDIATELY call mem_session_summary with the compacted summary content
  2. Then call mem_context to recover additional context from previous sessions
  3. Only THEN continue working

Do not skip step 1. Without it, everything done before compaction is lost from memory.


Project Name Normalization

Engram automatically prevents project name drift — the same project saved under different names ("engram" vs "Engram" vs " ENGRAM ") by different clients or users.

Automatic normalization

All project names are normalized on write and read: lowercase, trimmed, collapsed hyphens/underscores. Hyphens and underscores are not interchangeable, so "engram-memory" and "engram_memory" are not equivalent. If a name is changed during normalization, a warning is included in the response.

Auto-detection

MCP tools resolve project names at call time using the shared detection chain:

  1. Nearest .engram/config.json project_name within the enclosing git root, or at cwd outside git
  2. Git repository with an origin remote: initialize an absent private binding from the normalized repo name, otherwise reuse the stored binding label
  3. Git repository without an origin remote: initialize an absent private binding from the normalized root directory name, otherwise reuse the stored binding label
  4. Single git-repo child of cwd
  5. Multiple git-repo children of cwd returns ambiguous_project with available_projects
  6. Current working directory basename

engram mcp accepts a process-level default project via --project <name> / --project=<name> or ENGRAM_PROJECT=<name>. For current-project tools, this override takes precedence over cwd detection throughout the MCP process. It must be a project name, not a path; operations that cannot establish project context reject unknown overrides. Deliberately global tools retain their own omission contract, including mem_review list without a project filter.

The same precedence rule is applied by every entry point, so identity never depends on which binary wrote the record: an explicit request project (engram save --project, an MCP tool project argument) wins first, then the process override (engram mcp --project, then ENGRAM_PROJECT), then cwd detection.

Ownership on legacy sessions

A database upgraded from the schema where sessions.project was nullable still holds sessions that identify no project. Those sessions keep accepting writes: ownership is established forward rather than demanded retroactively.

  • When the write resolves a project through the chain above, its unowned parent session adopts that project in the same transaction, and the move is journaled like any other ownership change. The record and its session end up in agreement, so no record is left split from its parent.
  • Adoption is refused in one case: an unowned session that already parents a record owned by a different project. Claiming it there would split that record from its session, so the write fails with project_ownership_ambiguous (HTTP 409) and the operator resolves it explicitly.
  • A write that resolves no project at all against an unowned session fails with project_ownership_required (HTTP 409).

Both errors carry a remedy field naming the exact command to run: engram projects rescue-ownership --project <name> --session <id>. That command reaches the local store directly and needs no server token, so recovery stays available in a zero-config install. Bulk repair remains available over HTTP through POST /projects/rescue-ownership when ENGRAM_HTTP_TOKEN is configured.

Similar-project warnings

When saving to a project that doesn't exist yet, Engram checks for similar existing project names (Levenshtein distance, substring, case-insensitive matching) and warns the agent if a likely variant already exists.

Retroactive cleanup

Use engram projects consolidate to interactively merge legacy project names that are equivalent after normalization, or mem_merge_projects for agent-driven consolidation.

Use engram projects rescue-ownership --project <name> [--session <id>] [--observation <id>] [--prompt <id>] to assign ownership to legacy rows that carry none. It prints how many sessions, observations, and prompts moved, and — when anything was left behind — exactly which items and why. It works against the local store, so it needs no running server and no ENGRAM_HTTP_TOKEN.


Features

Full-Text Search (FTS5)

  • Searches across title, content, tool_name, type, and project
  • Query sanitization: wraps each word in quotes to avoid FTS5 syntax errors
  • Supports type and project filters

Timeline (Progressive Disclosure)

Three-layer pattern for token-efficient memory retrieval:

  1. mem_search — Find relevant observations
  2. mem_timeline — Drill into chronological neighborhood of a result
  3. mem_get_observation — Get full untruncated content

Privacy Tags

<private>...</private> content is stripped at TWO levels:

  1. Plugin layer (TypeScript) — Strips before data leaves the process
  2. Store layer (Go) — stripPrivateTags() runs inside AddObservation() and AddPrompt()

Example: Set up API with <private>sk-abc123</private> becomes Set up API with [REDACTED]

User Prompt Storage

Separate table captures what the USER asked (not just tool calls). Gives future sessions the "why" behind the "what". Full FTS5 search support.

Export / Import

Share memories across machines, backup, or migrate:

  • engram export — Versioned JSON backup of sessions, observations, prompts, local pin state, and memory-relation metadata
  • engram import <file> — Load an atomic backup transaction. Version 0.2.0 preserves pins and relations; legacy 0.1.0 backups without those fields remain compatible, while unsupported versions fail before mutation

Git Sync (Chunked)

Share memories through git repositories using compressed chunks with a manifest index.

  • engram sync — Exports new memories as a gzipped JSONL chunk to .engram/chunks/
  • engram sync --all — Exports ALL memories from every project
  • engram sync --import — Imports chunks listed in the manifest that haven't been imported yet
  • engram sync --status — Shows how many chunks exist locally vs remotely (filesystem mode)
  • engram sync --cloud --status --project <name> — Shows local, remote, and pending chunk counts for the specified cloud project
  • engram sync --project NAME — Filters export to a specific project
.engram/
├── manifest.json          <- index of all chunks (small, git-mergeable)
├── chunks/
│   ├── a3f8c1d2.jsonl.gz <- chunk 1 (gzipped JSONL)
│   ├── b7d2e4f1.jsonl.gz <- chunk 2
│   └── ...
└── engram.db              <- local working DB (gitignored)

Why chunks?

  • Each engram sync creates a NEW chunk — old chunks are never modified
  • No merge conflicts: each dev creates independent chunks, git just adds files
  • Chunks are content-hashed (SHA-256 prefix) — each chunk is imported only once
  • The manifest is the only file git diffs — it's small and append-only
  • Compressed: a chunk with 8 sessions + 10 observations = ~2KB

Agent-Driven Compression

Instead of a separate LLM service, the agent itself compresses observations. The agent already has the model, context, and API key.

Two levels:

  • Per-action (mem_save): Structured summaries (What/Why/Where/Learned)
  • Session summary (mem_session_summary): Comprehensive end-of-session summary (Goal/Instructions/Discoveries/Accomplished/Next Steps/Files)

No Raw Tool-Call Auto-Capture

Engram does not record a firehose of raw tool calls. Raw tool calls (edit: {file: "foo.go"}, bash: {command: "go build"}) are noisy and pollute FTS5 search. The agent's curated summaries are higher signal, more searchable, and don't bloat the database. Shell history and git provide the raw audit trail.

Since v1.15.3, mem_save can also best-effort attach the current user prompt when prompt context was already provided to the same MCP process for the same project/session (typically by mem_save_prompt) and capture_prompt is not disabled. That is not raw event capture: it stores user intent tied to a curated save, and the save still succeeds if prompt context is missing.


Terminal UI (TUI)

Interactive Bubbletea-based terminal UI. Launch with engram tui.

Screens

Screen Description
Dashboard Stats overview (sessions, observations, prompts, projects) + menu
Search FTS5 text search with text input
Search Results Browsable results list from search
Recent Observations Browse all observations, newest first
Observation Detail Full content of a single observation, scrollable
Timeline Chronological context around an observation (before/after)
Sessions Browse all sessions
Session Detail Observations within a specific session

Navigation

  • j/k or arrow keys — Navigate lists
  • Enter — Select / drill into detail
  • c — Copy observation content to clipboard (OSC 52; works in search results, recent list, detail, and session views)
  • t — View timeline for selected observation
  • s or / — Quick search from any screen
  • Esc or q — Go back / quit
  • Ctrl+C — Force quit

Visual Features

  • Catppuccin Mocha color palette
  • (active) badge — shown next to sessions and observations from active sessions, sorted to top
  • Scroll indicators — position in long lists (e.g. "showing 1-20 of 50")
  • 2-line items — each observation shows title + content preview

Running as a Service

Without a service supervisor, engram serve dies whenever the binary is replaced (e.g. on brew upgrade engram) or the host reboots, and autosync stops silently. The templates below restart it automatically. Use engram cloud status afterwards to confirm — the Local daemon: line should report running on port 7437.

Using systemd (Linux)

  1. Move binary to ~/.local/bin (ensure it's in your $PATH)
  2. Create directories: mkdir -p ~/.engram ~/.config/systemd/user
  3. Create ~/.config/systemd/user/engram.service (see below)
  4. systemctl --user daemon-reload
  5. systemctl --user enable engram
  6. systemctl --user start engram
  7. journalctl --user -u engram -f
[Unit]
Description=Engram Memory Server
After=network.target

[Service]
WorkingDirectory=%h
ExecStart=%h/.local/bin/engram serve
Restart=always
RestartSec=3
Environment=ENGRAM_DATA_DIR=%h/.engram

[Install]
WantedBy=default.target

Using launchd (macOS)

This is the recommended setup for Homebrew users on macOS. With KeepAlive=true, launchd relaunches engram serve automatically after brew upgrade engram replaces the binary, so autosync survives upgrades.

  1. Find your binary path: which engram (typically /opt/homebrew/bin/engram on Apple Silicon or /usr/local/bin/engram on Intel)
  2. Create the data dir if missing: mkdir -p ~/.engram
  3. Create ~/Library/LaunchAgents/com.gentleman-programming.engram.plist with the contents below — replace <HOME> with the absolute path of your home directory (echo $HOME) and adjust the binary path if which engram returned something different
  4. Load it: launchctl load ~/Library/LaunchAgents/com.gentleman-programming.engram.plist
  5. Verify: launchctl list | grep engram and engram cloud status (the Local daemon: line should report running on port 7437)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.gentleman-programming.engram</string>
    <key>ProgramArguments</key>
    <array>
        <string>/opt/homebrew/bin/engram</string>
        <string>serve</string>
    </array>
    <key>WorkingDirectory</key>
    <string><HOME></string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>ENGRAM_DATA_DIR</key>
        <string><HOME>/.engram</string>
        <!-- Uncomment and fill these to enable cloud autosync:
        <key>ENGRAM_CLOUD_AUTOSYNC</key>
        <string>1</string>
        <key>ENGRAM_CLOUD_SERVER</key>
        <string>https://your-cloud-host</string>
        <key>ENGRAM_CLOUD_TOKEN</key>
        <string>your-cloud-token</string>
        -->
    </dict>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string><HOME>/.engram/serve.out.log</string>
    <key>StandardErrorPath</key>
    <string><HOME>/.engram/serve.err.log</string>
</dict>
</plist>

To unload (stop and disable): launchctl unload ~/Library/LaunchAgents/com.gentleman-programming.engram.plist. To reload after editing the plist: unload, then load again.

Note on brew upgrade: launchd does not expand $HOME or ~ inside plist values, which is why the template uses literal absolute paths.

Using Windows Task Scheduler

Windows Task Scheduler is the native service equivalent on Windows. It restarts engram serve on login and after reboots, keeping autosync alive without a third-party service manager.

Setup steps:

  1. Confirm engram.exe is in your PATH: open PowerShell and run Get-Command engram.
  2. Set ENGRAM_CLOUD_TOKEN (and any other cloud vars) as a user or system environment variable in System Properties → Advanced → Environment Variables. Task Scheduler does not inherit session environment variables, so tokens set in your shell profile or in $env:... within a PowerShell session will not be visible to the scheduled task.
  3. Create the scheduled task by running the PowerShell snippet below in an elevated terminal (Run as Administrator), or import it manually through the Task Scheduler GUI.
  4. Verify: after the next login (or trigger manually), run engram cloud status — the Local daemon: line should report running on port 7437.
$action  = New-ScheduledTaskAction `
    -Execute  "powershell.exe" `
    -Argument "-ExecutionPolicy Bypass -WindowStyle Hidden -Command `"Start-Process engram -ArgumentList 'serve' -NoNewWindow`""

$trigger = New-ScheduledTaskTrigger -AtLogOn

$settings = New-ScheduledTaskSettingsSet `
    -ExecutionTimeLimit (New-TimeSpan -Hours 0) `
    -RestartCount 5 `
    -RestartInterval (New-TimeSpan -Minutes 1) `
    -StartWhenAvailable

Register-ScheduledTask `
    -TaskName    "EngramMemoryServer" `
    -Action      $action `
    -Trigger     $trigger `
    -Settings    $settings `
    -RunLevel    Limited `
    -Description "Engram persistent memory server (engram serve)"

Environment variables: ENGRAM_CLOUD_TOKEN, ENGRAM_CLOUD_SERVER, ENGRAM_CLOUD_AUTOSYNC, and ENGRAM_DATA_DIR must be set as persistent user or system environment variables (Control Panel → System → Advanced → Environment Variables) so Task Scheduler can read them. Variables you export or set with $env: in a terminal session are not visible to scheduled tasks.

Logs: To capture stdout/stderr, redirect output in the PowerShell command string, for example: ... -Command "Start-Process engram -ArgumentList 'serve' -NoNewWindow -RedirectStandardOutput '$env:USERPROFILE\.engram\serve.out.log' -RedirectStandardError '$env:USERPROFILE\.engram\serve.err.log'". Ensure the log files are opened with UTF-8 encoding (-Encoding UTF8) if you post-process them.

Stopping the task: Stop-ScheduledTask -TaskName "EngramMemoryServer" or Unregister-ScheduledTask -TaskName "EngramMemoryServer" -Confirm:$false to remove it entirely.


Design Decisions

  1. Go over TypeScript — Single binary, cross-platform, no runtime. The initial prototype was TS but was rewritten.
  2. SQLite + FTS5 over vector DB — FTS5 covers 95% of use cases. No ChromaDB/Pinecone complexity.
  3. Agent-agnostic core — Go binary is the brain, thin plugins per-agent. Not locked to any agent.
  4. Agent-driven compression — The agent already has an LLM. No separate compression service.
  5. Privacy at two layers — Strip in plugin AND store. Defense in depth.
  6. Pure Go SQLite (modernc.org/sqlite) — No CGO means true cross-platform binary distribution.
  7. No raw tool-call auto-capture — The agent saves curated summaries; mem_save may best-effort capture process-local prompt context tied to that save, but Engram does not ingest raw tool-call firehoses. Shell history and git provide the raw audit trail.
  8. TUI with Bubbletea — Interactive terminal UI following Gentleman Bubbletea patterns.

Dependencies

Go

Package Version Purpose
github.com/mark3labs/mcp-go v0.44.0 MCP protocol implementation
modernc.org/sqlite v1.45.0 Pure Go SQLite driver (no CGO)
github.com/charmbracelet/bubbletea v1.3.10 Terminal UI framework
github.com/charmbracelet/lipgloss v1.1.0 Terminal styling
github.com/charmbracelet/bubbles v1.0.0 TUI components

OpenCode Plugin

  • @opencode-ai/plugin — OpenCode plugin types and helpers
  • Runtime: Bun (built into OpenCode)

Dashboard templ regeneration

The cloud dashboard uses templ for server-side HTML components. Generated *_templ.go files are committed alongside their .templ sources. If you modify any .templ file in internal/cloud/dashboard/, you must regenerate the Go output before committing.

Prerequisite

Download the pinned templ binary:

go mod download

Regenerate

make templ
# or directly:
go tool templ generate ./internal/cloud/dashboard/...

The regenerated components_templ.go, layout_templ.go, and login_templ.go must be committed together with the .templ source changes. The test TestTemplGeneratedFilesAreCheckedIn in internal/cloud/dashboard/templ_policy_test.go will fail in CI if generated files are missing or outdated.

Important: Always use the pinned version github.com/a-h/templ v0.3.1001 (already in go.mod). Regenerating with a different version produces diff churn in generated output.


Cloud Autosync

Autosync is a background push/pull replication service that keeps your local Engram store in sync with the Engram Cloud server without blocking local writes.

Enabling Autosync

Autosync is opt-in. Set all three environment variables before starting engram serve or engram mcp:

Variable Required Description
ENGRAM_CLOUD_AUTOSYNC Yes (exact "1") Enables autosync. Any other value disables it.
ENGRAM_CLOUD_TOKEN Yes Bearer token for the cloud server.
ENGRAM_CLOUD_SERVER Yes Base URL of the cloud server (e.g. https://cloud.engram.example.com).

Example:

ENGRAM_CLOUD_AUTOSYNC=1 \
ENGRAM_CLOUD_TOKEN=your-token \
ENGRAM_CLOUD_SERVER=https://cloud.engram.example.com \
engram serve

# Or, for stdio MCP agents:
ENGRAM_CLOUD_AUTOSYNC=1 \
ENGRAM_CLOUD_TOKEN=your-token \
ENGRAM_CLOUD_SERVER=https://cloud.engram.example.com \
engram mcp

Missing ENGRAM_CLOUD_TOKEN or ENGRAM_CLOUD_SERVER logs an ERROR and disables autosync gracefully — engram serve or engram mcp still starts.

Autosync Phase Table

Phase Meaning Dashboard Status
idle Loop running, no cycle yet running
pushing Pushing local mutations to cloud running
pulling Pulling remote mutations running
healthy Last cycle succeeded healthy
push_failed Last push failed degraded
pull_failed Last pull failed degraded
backoff Too many consecutive failures; waiting degraded
disabled Paused by StopForUpgrade degraded (upgrade_paused)

Reason Code Table

reason_code appears in Manager.Status().ReasonCode and is surfaced via /sync/status:

reason_code Cause Resolution
transport_failed Network error, server 5xx, or 404 on mutation endpoints Check server health and network; if 404, see server_unsupported note below
auth_required Bearer token rejected (401) Rotate ENGRAM_CLOUD_TOKEN
policy_forbidden Project access denied (403) Check the server-side ENGRAM_CLOUD_ALLOWED_PROJECTS policy for the denied project; a managed principal's project grant may also need checking. The client does not expose allowlist contents.
internal_error Panic inside the sync cycle Check logs for stack trace
upgrade_paused Autosync paused during cloud upgrade (PhaseDisabled) Call ResumeAfterUpgrade or restart

Note: when the cloud server returns 404 on mutation endpoints, the transport logs [autosync] cloud mutation endpoint returned 404 (server_unsupported) and the transport-level ErrorCode is "server_unsupported", but the manager surfaces this as reason_code: transport_failed.

Troubleshooting

For a step-by-step recovery guide covering chunk_id does not match payload content hash, session payload directory is required, and the temporary missing-directory repair helper, see Engram Cloud Troubleshooting.

transport_failed with server_unsupported in logs: Older pre-mutation cloud server deployments may not implement POST /sync/mutations/push or GET /sync/mutations/pull, causing 404 responses from those endpoints. Deploy a server version that includes these routes before enabling ENGRAM_CLOUD_AUTOSYNC=1. Check logs for the line containing server_unsupported.

Autosync not starting: Check that ENGRAM_CLOUD_AUTOSYNC is exactly "1" (not "true" or "yes"), and that both ENGRAM_CLOUD_TOKEN and ENGRAM_CLOUD_SERVER are non-empty. The process logs an [autosync] ERROR line explaining which variable is missing.

Local writes still blocked: Autosync runs in its own goroutine and never holds locks shared with the local write path. If local writes appear blocked, investigate the SQLite store layer, not the autosync manager.


Scheduled Explicit Cloud Sync Wrappers

The wrappers under tools/ are an alternative to native autosync for hosts where you cannot keep engram serve running. For each explicitly named project, they run the native autosync order: export/push (engram sync --cloud --project <project>), then import/pull (engram sync --cloud --import --project <project>). Choose ONE mode -- native autosync (recommended) when a daemon is feasible, OR these wrappers for the no-daemon case. Do not run both at once. Cloud --all is intentionally unsupported; projects are never inferred from cwd or an env var.

Bash: tools/cloud-sync-projects.sh

./tools/cloud-sync-projects.sh my-project my-other-project
./tools/cloud-sync-projects.sh --log /var/log/engram-cloud-sync.log my-project

Exit 0 if every attempted export, import, and log operation succeeded; 1 if any attempted phase or logging operation failed; 2 on usage error. If export fails, that project's import is skipped, matching native autosync; the wrapper records the export failure and continues with later projects. If import fails, it is recorded and makes the aggregate result nonzero. Default durable log $ENGRAM_DATA_DIR/cloud-sync-projects.log (~/.engram fallback); override --log > ENGRAM_CLOUD_SYNC_LOG > default. Per-project, per-phase status lines go to both timestamped console and log; command stdout+stderr stays live on the console and is appended to the log. Nothing retried or silenced.

PowerShell: tools/cloud-sync-projects.ps1

pwsh ./tools/cloud-sync-projects.ps1 my-project my-other-project
pwsh ./tools/cloud-sync-projects.ps1 -LogPath C:\logs\engram-cloud-sync.log my-project

Requires PowerShell 7 (pwsh); 5.1 is not supported. Same export-then-import order, skipped-import behavior after an export failure, exit codes, and log defaults as Bash; override -LogPath > ENGRAM_CLOUD_SYNC_LOG > default.

Both wrapper files are included in every GoReleaser release archive under tools/; copy the one for your scheduler host from the extracted archive.

Inspecting the last failure

phase=<export|import> FAILURE project=<name> exit=<n> records the exact exit code from the failing CLI phase:

grep 'phase=.* FAILURE' "${ENGRAM_DATA_DIR:-$HOME/.engram}/cloud-sync-projects.log" | tail -n 5
# PowerShell 7 ($env:ENGRAM_DATA_DIR or $HOME/.engram fallback)
$d = if ($env:ENGRAM_DATA_DIR) { $env:ENGRAM_DATA_DIR } else { Join-Path $HOME '.engram' }
Select-String 'phase=.* FAILURE' (Join-Path $d 'cloud-sync-projects.log') | Select-Object -Last 5

Pass the failing project to Engram Cloud Troubleshooting -- the wrappers record and propagate, not interpret or retry.

Cloud Sync Audit Log

When project sync is paused and a push is rejected, Engram records an audit entry in cloud_sync_audit_log. This gives operators a persistent trail of every rejection event, visible in the admin dashboard under Admin > Audit Log.

Schema

Column Type Description
id SERIAL PK Auto-incrementing row identifier
occurred_at TIMESTAMPTZ DEFAULT NOW() Timestamp of the rejection event
contributor TEXT NOT NULL Identity of the caller (from created_by field in request, or "unknown")
project TEXT NOT NULL Project name that was paused and rejected
action TEXT NOT NULL Push type discriminator: mutation_push or chunk_push
outcome TEXT NOT NULL Rejection outcome: always rejected_project_paused in v1
entry_count INT DEFAULT 0 Number of entries in the rejected batch
reason_code TEXT Short machine-readable reason code (e.g. sync-paused)
metadata JSONB Reserved for future structured context; not populated in v1

Outcome Vocabulary

Outcome Meaning
rejected_project_paused Push was rejected because the project's sync is paused via the admin sync control

Action Discriminator

Action Meaning
mutation_push Rejection occurred on POST /sync/mutations/push
chunk_push Rejection occurred on POST /sync/push (legacy chunk push)

Pull requests (GET /sync/mutations/pull) are never gated on pause status and never emit audit entries. Paused projects continue to serve reads to enrolled contributors without restriction.

Retention and Pruning

There is no automatic retention policy in v1. Audit rows accumulate indefinitely. To prune entries older than 90 days, connect to Postgres and run:

DELETE FROM cloud_sync_audit_log
WHERE occurred_at < NOW() - INTERVAL '90 days';

Wrap in a transaction and add a LIMIT clause if the table is large:

BEGIN;
DELETE FROM cloud_sync_audit_log
WHERE id IN (
  SELECT id FROM cloud_sync_audit_log
  WHERE occurred_at < NOW() - INTERVAL '90 days'
  LIMIT 10000
);
COMMIT;

Next Steps

  • Agent Setup — connect your agent to Engram
  • Plugins — what the OpenCode and Claude Code plugins add beyond bare MCP
  • Obsidian Brain — visualize memories as a knowledge graph (beta)
  • Contributing — how to contribute