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.
| 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 |
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.
- sessions —
id(TEXT PK),project,ownership_mode,directory,started_at,ended_at,summary,runtime_lease_expires_at(local-only runtime liveness; never synced or exported) - observations —
id(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_prompts —
id(INTEGER PK AUTOINCREMENT),sync_id,session_id(FK),content,project,created_at; prompt_tombstones records deleted promptsync_id,session_id,project, anddeleted_at - prompts_fts — FTS5 virtual table synced via triggers (
content,project) - sync_chunks —
target_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 includeid(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 aprojectcolumn; 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.
- WAL mode for concurrent reads
- Busy timeout 5000ms
- Synchronous NORMAL
- Foreign keys ON
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 on127.0.0.1:7437by 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 healthGET /dashboard/login— login surface (authenticated mode), redirects to/dashboard/when already authenticatedPOST /dashboard/login— login submit (authenticated mode), redirect-only no-op in insecure modePOST /dashboard/logout— clear session cookie and redirect to loginGET /dashboard/static/*— embedded CSS/JS assets
- Protected (requires dashboard session in authenticated mode; open in insecure mode)
GET /dashboardandGET /dashboard/— dashboard overviewGET /dashboard/statsGET /dashboard/activityGET /dashboard/browserGET /dashboard/browser/observations(HX-Request: truereturns fragment; plain GET returns full page)GET /dashboard/browser/sessions(HX-Request: truereturns fragment; plain GET returns full page)GET /dashboard/browser/sessions/{sessionID}GET /dashboard/browser/prompts(HX-Request: truereturns fragment; plain GET returns full page)GET /dashboard/projectsGET /dashboard/projects/list— HTMX partial; paginated project list with "Paused" badgesGET /dashboard/projects/{project}GET /dashboard/projects/{name}/observations— HTMX partial for project detailGET /dashboard/projects/{name}/sessions— HTMX partial for project detailGET /dashboard/projects/{name}/prompts— HTMX partial for project detailGET /dashboard/contributorsGET /dashboard/contributors/list— HTMX partial; paginated contributor listGET /dashboard/contributors/{contributor}GET /dashboard/admin(also requires admin token/session)GET /dashboard/admin/projectsGET /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-listsGET /dashboard/observations/{project}/{sessionID}/{syncID}— observation detailGET /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.
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.
- 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"}
POST /sessions— Create or renew a runtime session. Body:{id, project, directory, ownership_mode?}ownership_modeacceptssharedorproject_owned; when omitted it defaults toshared.- A successful create or renewal writes a local 30-minute
runtime_lease_expires_atwithout changing the persisted session identity. Leases are local liveness evidence only: they are neither synced nor exported. - A
project_ownedregistration cannot reuse a session with a nonblank persisted project different from its requested project. It returns409with{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 orsharedregistration retains compatibility for shared sessions. - An ended session is terminal: renewal returns
409and never reopens it.POST /sessions/{id}/endremains the only endpoint that setsended_at. - An invalid non-empty
ownership_modereturns400and 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
200with[](nevernull)
- No-result responses return
GET /sessions/{id}— Get single session by IDDELETE /sessions/{id}— Delete session200when deleted404when session does not exist409when session still has observations (delete/migrate observations first)- For cloud-enrolled projects: returns
200and additionally enqueues asession/deletemutation that propagates the deletion to cloud replicas
POST /observations— Add observation. Body:{session_id, type, title, content, tool_name?, project?, scope?, topic_key?}400whentitleorcontentis 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:descGET /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
200with[](nevernull)
- No-result responses from both observation collection endpoints return
GET /observations/{id}— Get single observation by IDPATCH /observations/{id}— Update fields. Body:{title?, content?, type?, project?, scope?, topic_key?}400whentitleorcontentis 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
400for an invalid ID, and return404when the observation does not exist - Pin state is local-only for sync: these routes do not change
updated_ator enqueue sync work. Direct backups preserve pin state, but shared sync payloads continue to omit it.
- Both pin routes are idempotent, return
DELETE /observations/{id}— Delete observation (?hard=truefor hard delete, soft delete by default)200when deleted404when observation does not exist
POST /topic-keys/suggest— Suggest a stable topic key using the same heuristic asmem_suggest_topic_key. Body:{type?, title?, content?}. Returns{topic_key}.- At least one of
titleorcontentmust be non-empty; invalid JSON or missing suggestion input returns400
- At least one of
GET /review— List observations due for local review. Query:?project=X&all_projects=true&limit=NPOST /review/mark_reviewed— Reset one observation's local review cycle. Body:{observation_id}; legacy{id}is accepted.200with the refreshed observation payload when marked reviewed400whenobservation_id/idis missing or the JSON body is invalid404when the observation does not exist- Local-only: updating
review_afterdoes not enqueue a sync mutation or propagate to other machines.
GET /search— FTS5 search. Query:?q=QUERY&type=TYPE&project=PROJECT&scope=SCOPE&limit=N200with a JSON array of search results- No-result example:
GET /search?q=definitely-no-hitreturns200with[](nevernull)
GET /timeline— Chronological context. Query:?observation_id=N&before=5&after=5
POST /prompts— Save user prompt. Body:{session_id, content, project?}GET /prompts/recent— Recent prompts. Query:?project=X&all_projects=true&limit=NGET /prompts/search— Search prompts. Query:?q=QUERY&project=X&all_projects=true&limit=N- No-result responses from both prompt collection endpoints return
200with[](nevernull)
- No-result responses from both prompt collection endpoints return
DELETE /prompts/{id}— Delete prompt200when deleted400for invalid prompt id404when prompt does not exist
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=Nobservations/prompts/sessions/pinned:0(or omitted/invalid) uses that section's legacy default,>0caps it (silently clamped to a500ceiling),<0omits the section and its### ...header entirelycompact=truedrops the inline content preview fromPinned/Recent Observationsbullets, 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 to65536). 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.
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.
GET /export— Export current-project data as a versioned JSON backup- Optional
?project=<name>selects a known project;?all_projects=trueexports every project - Current format
0.2.0preserves observations (including local pin state), prompts, and complete memory-relation judgment and supersession metadata. 400for blank, malformed, or conflicting selectors
- Optional
POST /import— Import one JSON backup atomically. Current0.2.0backups and legacy0.1.0backups 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.
GET /stats— Current-project memory statistics. Use?project=<name>or?all_projects=trueto select scope.GET /doctor— Read-only operational diagnostics. Query:?project=X&check=CHECK_CODE- Returns the same diagnostic report envelope as
engram doctor --jsonand MCPmem_doctor projectandcheckare optional; omittedprojectuses current project detection- Unknown explicit projects return
404with{error, code:"unknown_project", available_projects:[...]}
- Returns the same diagnostic report envelope as
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 optionalwarning/error_hint - Ambiguous cwd is a successful discovery response:
projectis empty,project_sourceisambiguous,available_projectslists the candidates, anderror_hintexplains why no project was selected. - Other current-project-scoped HTTP routes return
404for an unknown explicit project,409with{error, code:"ambiguous_project", available_projects, project_source, project_path}for an ambiguous cwd, and400for 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.jsonwith the intended project name rather than relying on a renamed remote. Global local/cloudproject_idpropagation and alias migration remain deferred.
- Always returns a success envelope with
POST /projects/rescue-ownership— Bulk-assign ownership to explicitly selected historical records that carry none.POST /projects/migrateis 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, matchingAuthorization: Bearer <token>,target_project,confirmed:true, and at least one positive observation/prompt ID or non-blank session ID are required. Missing server token returns503; missing or wrong credentials return401; malformed or invalid requests return400. - 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. 200returns{status, complete, blocked, target_project, rescued_observations, rescued_sessions, rescued_prompts, conflicting_records, skipped_records, journaled_local, reconciliation_status}. Owned records are never reassigned.statusisrescuedwhencompleteistrueand everything selected now belongs totarget_project, orpartially_rescuedwhen something was left behind.blockedthen names each item exactly —{kind, id, reason, owned_by}withkindone ofsession/observation/promptandreasonone ofowned_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_localmeans 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.
- A configured
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.
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"
}
]
}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:
200when judged400for invalid JSON, missing required fields, unknown relation, or invalid relation state
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:
200when accepted400for invalid JSON, missing required fields, invalid relation, invalid confidence, or cross-project pairs404when either observation ID does not exist
Get full detail for one relation row, including source and target observation snippets.
200with full relation +source_snippet+target_snippet404with a JSONerrorcontaining the not-found message whenrelation_iddoes not exist400with JSON error body whenrelation_idis not a valid integer
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
}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 IDcursor— optionalnext_cursorfrom a completed previous page; omit to start the first pageapply: false(default) — dry-run for the non-semantic lexical scan; reports candidates without inserting pending rowsapply: true— non-semantic lexical scan inserts new pending relation rows up tomax_insertcap (default 100)semantic: true— after FTS5 lexical scan, run LLM-judge semantic detection on the candidate pairs returned byFindCandidates. It does not discover totally lexically unrelated pairs on its own. RequiresENGRAM_AGENT_CLIto be set on the server toclaudeoropencode.- Semantic dry-runs evaluate verdicts without persisting them;
not_conflictresults are reported as skipped. Applied semantic scans persist every valid verdict, includingnot_conflict. concurrency— worker pool size for parallel LLM calls whensemantic: true(default 5, range 1–20)timeout_per_call_seconds— per-LLM-call timeout in seconds whensemantic: 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
projectresolves the current project;all_projects:trueexplicitly scans every project - With
semantic: true,concurrencyoutside [1, 20] ortimeout_per_call_secondsoutside [1, 600] returns400
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"
}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"
}
]
}Call ReplayDeferred() synchronously. Returns counts of rows processed.
Response:
{
"retried": 4,
"succeeded": 3,
"failed": 0,
"dead": 1
}GET /sync/status— Runtime sync-state status for the local node (engram serveonly).- 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=truereturns HTTP 400 withcode: "unsupported_project_scope".- Response fields when provider is injected:
enabledphaselast_errorconsecutive_failuresbackoff_untillast_sync_atreason_codereason_messagedeferred_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 retriedupgrade(nested object)stagereason_codereason_message
enabledsemantics:truewhen 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.falsewhen 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=falseresponse if no provider is injected.
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) |
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-runand--applyare 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 printednext_cursor; no automatic follow-up page is run.--semantic: enable LLM-judge semantic detection on FTS5 candidate pairs returned byFindCandidates. It can improve verdict quality for candidates that share lexical terms, but it does not discover totally lexically unrelated pairs on its own. RequiresENGRAM_AGENT_CLI=claudeorENGRAM_AGENT_CLI=opencode.- With
--semantic,--dry-runevaluates verdicts without persistence and reportsnot_conflictas skipped.--applypersists every valid verdict, includingnot_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: callReplayDeferred()and print retried/succeeded/failed/dead counts.
engram cloud status— show current cloud config state plus auth/sync readiness without mutating local state. When cloud is configured, also probes the localengram servedaemon at127.0.0.1:7437(respectsENGRAM_PORT) and prints aLocal daemon:line (running/not running/unreachable) so you can detect a silently dead autosync. The probe currently uses the TCP daemon endpoint, soENGRAM_SOCKETsocket-only mode can report the daemon as not running. Exit code is unaffected; the line is informationalengram cloud enroll <project>— enroll one project for cloud replicationengram cloud config --server <url>— persist cloud server URL to~/.engram/cloud.jsonengram cloud serve— run cloud backend API + dashboard (/dashboard) using Postgres config from envengram 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 flowengram cloud upgrade status --project <project>— show upgrade stage/class/reasonengram cloud upgrade rollback --project <project>— restore pre-upgrade local snapshot beforebootstrap_verified; blocked afterwardsengram cloud repair materialize-mutations --project <project> (--dry-run|--apply)— explicit server-side Postgres repair that backfills existingcloud_mutationsinto compatiblecloud_chunkswithout deleting remote dataengram 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.0so 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). |
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--usernameis required;--emailis 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 requiresENGRAM_CLOUD_TOKEN_PEPPERto be set to a dedicated secret (distinct fromENGRAM_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.cliaudit event. - Every bootstrap attempt (accepted or denied) writes a
bootstrap.cliaudit event tocloud_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
cloudstoremethods 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 replacementIt 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-existingThis 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_PEPPERto enable managed-token authentication. A token issued byengram 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_PEPPERis not set, managed-token authentication is simply disabled: the server still starts normally, andENGRAM_CLOUD_TOKEN/ENGRAM_CLOUD_ADMINcontinue 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). LegacyENGRAM_CLOUD_TOKENkeeps its existingENGRAM_CLOUD_ALLOWED_PROJECTSallowlist 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_TOKENcontinues to use its existing sync allowlist, andENGRAM_CLOUD_ADMINcontinues to provide dashboard read access, project sync controls, audit logs, and the first-admin dashboard bootstrap entry point whether or notENGRAM_CLOUD_TOKEN_PEPPERis configured. The CLI recovery command remains limited to its documented stranded-admin state. Use a managed admin token for managed-user administration.
The managed-admin API returns snake_case JSON keys for user and grant objects:
POST /admin/usersreturns one user object andGET /admin/usersreturns an array of user objects. Each object containsprincipal_id,username,email,display_name,role,enabled, andcreated_at.POST /admin/users/{principalID}/grantsreturns one grant object andGET /admin/users/{principalID}/grantsreturns an array of grant objects. Each object containsprincipal_id,project,granted_by_principal_id, andcreated_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-projectWhen 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:
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> --applyThe 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.
# 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 designDeterministic reason codes shared across store/CLI/server:
blocked_unenrolledauth_requiredcloud_config_errorpolicy_forbidden— check the server-sideENGRAM_CLOUD_ALLOWED_PROJECTSpolicy for the denied project; a managed principal's project grant may also need checking. The client does not expose allowlist contents.pausedtransport_failed
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.
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.
| 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.
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.
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_projectreturns detection fields directly (project,project_source,project_path,cwd,available_projects, optionalwarning/error_hint) and does not wrap them inresult.mem_doctorreturns the same JSON report shape asengram doctor --json; it uses read-project resolution before running diagnostics but does not wrap the report in the common MCP envelope.
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
projectnames fail loudly instead of silently falling back. - Valid-looking explicit
projectnames 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
projectfails loudly and does not create a new bucket. - If a non-empty
session_idis supplied and no session exists,mem_savefails with a structured error and does not write. - If both explicit
projectandsession_idare supplied, they must resolve to the same normalized project ormem_savefails with a structured error and does not write. - An explicit
session_idis 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, thenstarted_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 changesended_at. Directory is not session identity; callers with concurrent sessions must supplysession_id, end other active matching sessions, or save independently withengram 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_projectis 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,
projectmust exactly match one of the previously returnedavailable_projects; invented or normalized guesses are rejected. - Exact ambiguous-project choices can still fail with
project_name_collisionwhen 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 withproject_name_collisionwhen the raw explicit name collapses into an existing config-backed, session-backed, or store-backed project bucket, such asfoo--barcolliding withfoo-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.
mem_search, mem_context, mem_timeline, mem_stats, mem_doctor — project 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.
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.
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.
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.
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_savestill succeeds. Automated artifact saves should passfalse. - content: Structured with
**What**,**Why**,**Where**,**Learned**; required unless the legacyobservationalias is provided - observation: backward-compatible alias for
contentfor older/raw MCP clients; prefercontentfor 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.
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.
Review observation lifecycle state. Available in the agent profile (engram mcp --tools=agent).
Actions:
action: "list"— returns observations whosereview_afterhas passed. Optional parameters:projectandlimit(default 10).action: "mark_reviewed"— requiresobservation_id; resets that observation's local review cycle using its type decay policy. The legacyidalias 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.
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: ...").
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: ...").
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.
Delete an observation by ID. Uses soft-delete by default (deleted_at); optional hard-delete for permanent removal.
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.
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.
Show memory system statistics — sessions, observations, prompts, projects.
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.
Get full untruncated content of a specific observation by ID.
Save comprehensive end-of-session summary:
## Goal
## Instructions
## Discoveries
## Accomplished
## Next Steps
## Relevant Files
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.
Mark a session as completed with optional summary.
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.
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.
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.
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.
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.
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_idreturned bymem_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.
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
JudgeBySemanticwith 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_conflictverdicts persist as judged relations, suppressing future candidate scans without appearing in conflict-facing lists or statistics- Cross-project relations are rejected with an error
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).
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)
- Different topics must not overwrite each other (e.g. architecture vs bugfix)
- Reuse the same
topic_keyto update an evolving topic instead of creating new observations - If unsure about the key, call
mem_suggest_topic_keyfirst and then reuse it - Use
mem_updatewhen you have an exact observation ID to correct
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 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:
- First call
mem_context— checks recent session history (fast, cheap) - If not found, call
mem_searchwith relevant keywords (FTS5 full-text search) - If you find a match, use
mem_get_observationfor 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
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.
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.
If you see a message about compaction or context reset:
- IMMEDIATELY call
mem_session_summarywith the compacted summary content - Then call
mem_contextto recover additional context from previous sessions - Only THEN continue working
Do not skip step 1. Without it, everything done before compaction is lost from memory.
Engram automatically prevents project name drift — the same project saved under different names ("engram" vs "Engram" vs " ENGRAM ") by different clients or users.
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.
MCP tools resolve project names at call time using the shared detection chain:
- Nearest
.engram/config.jsonproject_namewithin the enclosing git root, or at cwd outside git - Git repository with an
originremote: initialize an absent private binding from the normalized repo name, otherwise reuse the stored binding label - Git repository without an
originremote: initialize an absent private binding from the normalized root directory name, otherwise reuse the stored binding label - Single git-repo child of cwd
- Multiple git-repo children of cwd returns
ambiguous_projectwithavailable_projects - 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.
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(HTTP409) and the operator resolves it explicitly. - A write that resolves no project at all against an unowned session fails with
project_ownership_required(HTTP409).
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.
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.
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.
- 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
Three-layer pattern for token-efficient memory retrieval:
mem_search— Find relevant observationsmem_timeline— Drill into chronological neighborhood of a resultmem_get_observation— Get full untruncated content
<private>...</private> content is stripped at TWO levels:
- Plugin layer (TypeScript) — Strips before data leaves the process
- Store layer (Go) —
stripPrivateTags()runs insideAddObservation()andAddPrompt()
Example: Set up API with <private>sk-abc123</private> becomes Set up API with [REDACTED]
Separate table captures what the USER asked (not just tool calls). Gives future sessions the "why" behind the "what". Full FTS5 search support.
Share memories across machines, backup, or migrate:
engram export— Versioned JSON backup of sessions, observations, prompts, local pin state, and memory-relation metadataengram import <file>— Load an atomic backup transaction. Version0.2.0preserves pins and relations; legacy0.1.0backups without those fields remain compatible, while unsupported versions fail before mutation
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 projectengram sync --import— Imports chunks listed in the manifest that haven't been imported yetengram 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 projectengram 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 synccreates 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
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)
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.
Interactive Bubbletea-based terminal UI. Launch with engram tui.
| 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 |
j/kor arrow keys — Navigate listsEnter— Select / drill into detailc— Copy observation content to clipboard (OSC 52; works in search results, recent list, detail, and session views)t— View timeline for selected observationsor/— Quick search from any screenEscorq— Go back / quitCtrl+C— Force quit
- 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
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.
- Move binary to
~/.local/bin(ensure it's in your$PATH) - Create directories:
mkdir -p ~/.engram ~/.config/systemd/user - Create
~/.config/systemd/user/engram.service(see below) systemctl --user daemon-reloadsystemctl --user enable engramsystemctl --user start engramjournalctl --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.targetThis 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.
- Find your binary path:
which engram(typically/opt/homebrew/bin/engramon Apple Silicon or/usr/local/bin/engramon Intel) - Create the data dir if missing:
mkdir -p ~/.engram - Create
~/Library/LaunchAgents/com.gentleman-programming.engram.plistwith the contents below — replace<HOME>with the absolute path of your home directory (echo $HOME) and adjust the binary path ifwhich engramreturned something different - Load it:
launchctl load ~/Library/LaunchAgents/com.gentleman-programming.engram.plist - Verify:
launchctl list | grep engramandengram cloud status(theLocal daemon:line should reportrunning 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$HOMEor~inside plist values, which is why the template uses literal absolute paths.
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:
- Confirm
engram.exeis in yourPATH: open PowerShell and runGet-Command engram. - 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. - 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.
- Verify: after the next login (or trigger manually), run
engram cloud status— theLocal daemon:line should reportrunning 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, andENGRAM_DATA_DIRmust be set as persistent user or system environment variables (Control Panel → System → Advanced → Environment Variables) so Task Scheduler can read them. Variables youexportor 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"orUnregister-ScheduledTask -TaskName "EngramMemoryServer" -Confirm:$falseto remove it entirely.
- Go over TypeScript — Single binary, cross-platform, no runtime. The initial prototype was TS but was rewritten.
- SQLite + FTS5 over vector DB — FTS5 covers 95% of use cases. No ChromaDB/Pinecone complexity.
- Agent-agnostic core — Go binary is the brain, thin plugins per-agent. Not locked to any agent.
- Agent-driven compression — The agent already has an LLM. No separate compression service.
- Privacy at two layers — Strip in plugin AND store. Defense in depth.
- Pure Go SQLite (modernc.org/sqlite) — No CGO means true cross-platform binary distribution.
- No raw tool-call auto-capture — The agent saves curated summaries;
mem_savemay 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. - TUI with Bubbletea — Interactive terminal UI following Gentleman Bubbletea patterns.
| 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-ai/plugin— OpenCode plugin types and helpers- Runtime: Bun (built into OpenCode)
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.
Download the pinned templ binary:
go mod downloadmake 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.
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.
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 mcpMissing ENGRAM_CLOUD_TOKEN or ENGRAM_CLOUD_SERVER logs an ERROR and disables autosync gracefully — engram serve or engram mcp still starts.
| 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 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.
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.
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.
./tools/cloud-sync-projects.sh my-project my-other-project
./tools/cloud-sync-projects.sh --log /var/log/engram-cloud-sync.log my-projectExit 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.
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-projectRequires 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.
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 5Pass the failing project to Engram Cloud Troubleshooting -- the wrappers record and propagate, not interpret or retry.
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.
| 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 | Meaning |
|---|---|
rejected_project_paused |
Push was rejected because the project's sync is paused via the admin sync control |
| 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.
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;- 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