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 |
| Documentation Authority | Which doc owns each contract and what must change together |
| Documentation surface catalog | Audience, authority, status, and review routing for tracked Markdown |
| CLI Reference | General command inventory and detailed CLI links |
| 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 |
When documentation and code disagree, this table says which doc surface is canonical for each contract, where the code-level authority lives, and which sibling docs must change together with it.
Reader and status guide:
- README.md is the concise product overview; this living
DOCS.mdis the full technical reference. Maintainer and contributor guides explain how to work with the system rather than replacing the contract-specific authorities below; the Codebase Guide covers ownership and guardrails. - These living docs are maintained and checked against shipped code and tests, not presumed to be generated copies. A historical record does not supersede shipped behavior or its tests. Generation is claimed only where a source-to-copy path is verified below.
- CODEOWNERS assigns the default reviewer/owner
@Gentleman-Programmingto paths without a more specific match, including these docs; review ownership does not make every document canonical.
| Contract | Canonical doc surface | Code authority | Must change together |
|---|---|---|---|
| MCP tool inventory | DOCS.md "MCP Tools" | internal/mcp/mcp.go registrations + ProfileAgent/ProfileAdmin |
docs/ARCHITECTURE.md tool table; docs/AGENT-SETUP.md setup claims; docs/PLUGINS.md comparison table; README.md intent table (subset) |
| Tool input schemas | DOCS.md per-tool sections | internal/mcp/mcp.go (live schemas); internal/mcp/testdata/tool-contract-v1.json (test-enforced v1 compatibility baseline; change only when intentionally changed) |
(none) |
| SQLite schema | DOCS.md "Database Schema" | internal/store/store.go Store.migrate |
(none) |
| Memory Protocol | DOCS.md#memory-protocol-full-text |
(none; prose contract) | skills/memory-protocol/SKILL.md; memoryProtocolMarkdown embedded in internal/setup; plugin/*/skills/memory/SKILL.md |
| Setup instructions and per-agent paths | docs/AGENT-SETUP.md | internal/setup/agents.go + setup.go |
README.md setup table |
| Plugin contracts | docs/PLUGINS.md | plugin/* assets, internal/setup/plugins/ |
docs/AGENT-SETUP.md per-agent sections |
| HTTP API and CLI | DOCS.md HTTP API / CLI sections | internal/server/server.go (local routes); internal/cloud/cloudserver/cloudserver.go (cloud routes); cmd/engram |
docs/PLUGINS.md conflicts table (subset) |
| Package ownership boundaries | docs/CODEBASE-GUIDE.md | (none; prose contract) | (none) |
Code and tests beat docs: internal/mcp owns agent-facing tool schemas, internal/store owns the durable schema, internal/setup owns install surfaces, and plugin/* translates host events without duplicating durable policy.
For readers, Memory Protocol is the canonical living agent-facing prose contract. The table above names related surfaces to check when that behavior changes; "must change together" means review for behavioral alignment, not copy identical text into every host.
For maintainers, distinguish these ownership paths:
internal/setup/setup.gomaintainsmemoryProtocolMarkdownindependently. Setup uses that embedded text for installed agent instructions; it is not generated from DOCS.md. Compare behavior when editing either prose surface.skills/memory-protocol/SKILL.mdis a manually maintained contributor skill.plugin/claude-code/skills/memory/SKILL.mdandplugin/codex/skills/memory/SKILL.mdare host-adapted, manually maintained skills. Consider their relevant instructions alongside the canonical prose rather than assuming byte identity or automatic sync.plugin/opencode/engram.tscontains OpenCode's agent instructions. Its verified generated copy isinternal/setup/plugins/opencode/engram.ts:go generate ./internal/setup/copies source to embedded destination, andTestEmbeddedOpenCodePluginMatchesSourceByteForBytechecks equality. This generation direction applies to the OpenCode plugin copy, not the other prose and skill surfaces above.
This catalog covers the 60 tracked Markdown files. Owner GP means CODEOWNERS review routing to @Gentleman-Programming, not an individual author or content owner. Canonical identifies a contract named in the matrix above; guide explains or operates alongside contracts; instruction directs agents; policy/template governs contribution or reuse. Living means maintained against current behavior, not a guarantee that every described optional feature is enabled. Beta material describes experimental paths, not universally shipped behavior.
| Surface | Audience | Authority | Status | Owner |
|---|---|---|---|---|
| README.md | New users | Guide: overview | Living | GP |
| DOCS.md | Users, maintainers | Canonical: technical contracts above | Living | GP |
| CONTRIBUTING.md | Contributors | Policy: contribution workflow | Living | GP |
| SECURITY.md | Security reporters | Policy: disclosure | Living | GP |
| TRADEMARKS.md | Reusers | Policy: marks | Living | GP |
| AGENTS.md | Repository agents | Instruction: skill index | Living | GP |
| CHANGELOG.md | Upgraders | Guide: release record | Historical | GP |
| .github/PULL_REQUEST_TEMPLATE.md | Contributors | Template: PR review | Living | GP |
| docs/AGENT-SETUP.md | Agent users | Canonical: setup paths | Living | GP |
| docs/ARCHITECTURE.md | Maintainers | Guide: architecture | Living | GP |
| docs/BETA_TESTING.md | Beta testers | Guide: testing | Beta | GP |
| docs/CODEBASE-GUIDE.md | Contributors | Canonical: package boundaries | Living | GP |
| docs/COMPARISON.md | Evaluators | Guide: comparison | Living | GP |
| docs/DOCTOR.md | Operators | Guide: diagnostics | Living | GP |
| docs/ENGRAM-CLOUD-BRANDING.md | Cloud readers | Guide: moved-page pointer | Compatibility redirect | GP |
| docs/ENGRAM-CLOUD.md | Cloud readers | Guide: moved-page pointer | Compatibility redirect | GP |
| docs/INSTALLATION.md | Installers | Guide: installation | Living | GP |
| docs/PLUGINS.md | Plugin users | Canonical: plugin contracts | Living | GP |
| docs/RELEASE-POLICY.md | Maintainers | Policy: releases | Living | GP |
| docs/SELF-TESTING.md | Maintainers | Guide: self-tests | Living | GP |
| docs/TEAM-USAGE.md | Teams | Guide: collaboration | Living | GP |
| docs/beta/obsidian-brain.md | Beta users | Guide: Obsidian export | Beta | GP |
| docs/codebase/*.md | Maintainers | Guide: codebase maps and operations | Living | GP |
| docs/engram-cloud/README.md | Cloud readers | Guide: cloud entry point | Living | GP |
| docs/engram-cloud/branding.md | Cloud maintainers | Guide: branding | Living | GP |
| docs/engram-cloud/production-checklist.md | Cloud operators | Guide: production checks | Living | GP |
| docs/engram-cloud/quickstart.md | Cloud users | Guide: quickstart | Living | GP |
| docs/engram-cloud/troubleshooting.md | Cloud operators | Guide: troubleshooting | Living | GP |
| docs/intended-usage.md | Users | Guide: intended use | Living | GP |
| plugin/claude-code/skills/memory/SKILL.md | Claude agents | Instruction: host-adapted memory skill | Living | GP |
| plugin/codex/skills/memory/SKILL.md | Codex agents | Instruction: host-adapted memory skill | Living | GP |
| plugin/pi/README.md | Pi users | Guide: integration | Living | GP |
| skills/catalog.md | Contributor agents | Instruction: skill index | Living | GP |
| skills/*/SKILL.md | Contributor agents | Instruction: domain skills | Living | GP |
The memory protocol's canonical prose is Memory Protocol; internal/setup/setup.go independently maintains memoryProtocolMarkdown for installed generated instructions. Claude and Codex plugin skill files above are manually maintained. OpenCode's source plugin/opencode/engram.ts has a verified generated embedded copy at internal/setup/plugins/opencode/engram.ts; neither is a separate Markdown catalog entry. Historical OpenSpec/SDD changes are transient, non-shipped records, not evidence of current behavior.
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_delete_tombstones — one row per deleted entity (PK
entity,entity_key) withsession_id,project,deleted_at,hard_delete,active,last_mutation_seq, andlast_remote_mutation_seqmetadata.last_mutation_seqretains the highest historical local delete-mutation sequence; backfill emits missing delete intent for active tombstones without a recorded remote delete sequence or matching unacknowledged delete mutation, without advancing that field.last_remote_mutation_seqrecords the default cloud target's remote delete floor. Pulled session and observation upserts first pass a tombstone guard: local sync compares a known payload generation with the hard-delete time, while cloud sync checks the applicable remote delete sequence floor. Without its own floor, an active tombstone blocks the default target; for non-default targets, it blocks only when no non-default target has a remote floor. Only permitted upserts can deactivate the tombstone. - sync_delete_tombstone_remote_floors — per-non-default-cloud-target delete floors, keyed by (
target_key,entity,entity_key), withlast_mutation_seqstoring the highest recorded remote delete sequence for that target and entity. - 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
Persistent SQLite WAL is unsafe on network filesystems. Engram rejects known NFS and SMB/CIFS data directories before it opens, migrates, or changes the database files. An unknown filesystem remains compatible, but is not a proof that the directory is local.
If startup reports a network filesystem, stop all Engram processes, then copy the complete engram.db, engram.db-wal, and engram.db-shm triplet together to local storage. Set ENGRAM_DATA_DIR to the absolute path of that local directory (relative paths are rejected), start Engram, and run engram doctor. Then run the integrity check for your shell:
# POSIX shell or Git Bash
sqlite3 "$ENGRAM_DATA_DIR/engram.db" "PRAGMA integrity_check;"# PowerShell
sqlite3 (Join-Path $env:ENGRAM_DATA_DIR 'engram.db') 'PRAGMA integrity_check;'Engram does not auto-repair, quarantine, checkpoint, or fall back to rollback journaling for this condition.
The CLI groups local memory operations, project maintenance, conflict audit, and optional cloud replication. Use engram help for live usage; the inventory below points to the detailed conflict and cloud references.
engram setup [agent] Install an available agent integration (see Agent Setup)
engram serve [port] Start local HTTP API (default: 7437)
engram mcp Start stdio MCP server
engram tui Launch terminal UI
engram test [suite] Run isolated self-tests [--quick] [--json]
engram init [name] Initialize .engram/config.json [--force]
engram search <query> Search memories [--project P|--all] [--match all|any]
engram save <title> <msg> Save a memory
engram delete <obs_id> Delete an observation [--hard]
engram delete session <id> Delete an empty session
engram delete prompt <id> Delete a prompt permanently
engram delete project <name> [--hard]
Remove prompts; soft-delete observations by default; --hard deletes observations and only unreferenced sessions
engram timeline <obs_id> Chronological context [--project P|--all]
engram context [project] Recent context [--project P|--all]
engram stats Memory statistics [--project P|--all]
engram export [file] Export memories to JSON [--project P|--all]
engram import <file> Import memories from JSON
engram sync Export new memories to .engram/ [--all: every project]
engram sync --cloud --project <name>
Sync one project against the configured cloud endpoint
engram conflicts <sub> Conflict audit: list, show, stats, scan, deferred
engram doctor Read-only diagnostics [--json] [--project P] [--check CODE]
engram cloud <sub> Optional cloud configuration, enrollment, upgrade, and server
engram projects list List projects with observation/session/prompt counts
engram projects merge --from acmeapi --to acme-api --dry-run
engram projects merge --from acmeapi --to acme-api --apply
engram projects consolidate [--all] [--dry-run]
Merge similar names interactively; --all --dry-run previews without prompts
engram projects prune Prune projects with zero observations [--dry-run] [--paths-only]
engram projects rescue-ownership --project <name> [--session <id>] [--observation <id>] [--prompt <id>]
Repair legacy local ownership without a server token
engram obsidian-export Export memories to Obsidian (beta; --all for every project)
engram version Show version
For setup [agent], see Agent Setup for supported integrations rather than treating a fixed agent list as exhaustive. --all deliberately selects every project for applicable reads and local sync export (cloud sync still requires a single project); do not combine it with an explicit project. engram context [project] accepts the positional project as an alternative to --project. projects prune --paths-only limits candidates to names containing / or \.
Cloud commands include cloud status, cloud config --server <url>, cloud enroll <project>, cloud unenroll <project>, and cloud serve. For upgrades, use cloud upgrade <doctor|repair|bootstrap|remirror|status|rollback> --project <name>; remirror rebuilds cloud state from authoritative local data. Server-side repair uses cloud repair materialize-mutations --project <project> (--dry-run|--apply). Managed-admin setup uses cloud bootstrap admin --username <name> [--email <email>] [--grant-project <project>]... [--issue-token [name]]; stranded-admin token recovery uses cloud bootstrap recover-token [--name <name>] [--revoke-existing]. See Cloud CLI and managed-user bootstrap for constraints and detailed syntax.
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)POST /sync/session-authoritiesregisters a session explicitly with JSONsession_idand ownerproject. Managed principals need a project grant; legacy authenticated tokens use the configured allowlist. Grant normalization does not change the stored owner: a grant foralpha-foocan authorizealpha/foo, whose owner remainsalpha/foo. In insecure no-auth mode registration returns 401 without writing; oversized JSON returns 413. Chunks and imported sessions never bootstrap registration: an imported/offline session needs deliberate reauthorization. Matching owner replay returns 200; conflicting owner returns 409. Registration alone does not authorize prompt claims or deletes.POST /sync/prompt-pair-claimsrequires a bearer token and JSONsession_id,source_inbox_id,sync_id,owner_project, andproject(prompt storage project).owner_projectis an authorization selector, not authority: the server checks grants/legacy allowlist for both projects before looking up the independently registered session, then requires its stored owner to match the selector. Grant aliases do not rewrite stored project identity. Missing registration, owner mismatch, and authority disappearance during claim all return the same generic 404 JSON body witherror_code: session_authority_unavailable; an absent old-server route returns a 404 without that code. Matching claim replay returns 200, competing pair binding 409. Invalid JSON/unknown fields return 400, oversized bodies 413, and insecure no-auth mode 401.MutationTransport.RegisterSessionAuthorityandClaimPromptPaircan POST these JSON requests over the configured transport; non-200 responses are errors, with an absent old-server route classified asserver_unsupported. Autosync does not invoke these methods yet. Clients must explicitly register the session under its owner and claim the pair under dual authorization before relying on remote pair provenance. Delete enforcement and the client handshake/pending retry path are not implemented yet; this route alone does not make unverified deletes safe.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/users/{principalID}(admin-gated; managed-user detail)POST /dashboard/admin/users— create managed userPOST /dashboard/admin/users/{principalID}/enable— enable managed userPOST /dashboard/admin/users/{principalID}/disable— disable managed userPOST /dashboard/admin/users/{principalID}/tokens— create managed tokenPOST /dashboard/admin/tokens/{tokenID}/revoke— revoke managed tokenPOST /dashboard/admin/users/{principalID}/grants— create project grantPOST /dashboard/admin/users/{principalID}/grants/{project}/revoke— revoke project grant- Managed-user detail and form routes use the dashboard session, unlike the JSON
/admin/*API. Viewing detail allows dashboard admin sessions; mutations require managed-admin permission. Successful form mutations redirect (303, orHX-Redirectfor HTMX), except token creation, which renders the show-once token directly without redirecting. 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
Dashboard bootstrap/recovery (separate from Public and Protected routes):
GET /dashboard/bootstrap— show the first managed-admin creation form.POST /dashboard/bootstrap— create the first managed admin; rejects creation when an active admin already exists.
Both routes require legacy dashboard recovery access. In authenticated mode, this means a valid dashboard session for the configured legacy ENGRAM_CLOUD_ADMIN principal; managed admins and members cannot use these routes. In insecure mode, bypassing the dashboard session check does not supply a LegacyEnvAdmin principal, so the bootstrap handlers are not generally open.
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 /healthchecks the local store with live aggregate queries. On success it returns200with{"status":"ok","service":"engram","version":"<release version>","instance_id":"<store instance ID>"}; a failed store returns500with{"error":"health check failed"}instead of reporting healthy. - 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?, resume?}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. resumeis an optional boolean, defaultfalse. Without it, an ended session returns409withcode: "session_already_ended"; ended rows are never reopened.POST /sessions/{id}/endremains the endpoint that setsended_at.- With
resume: true, a new or live root keeps its ID. For an ended root, the store atomically renews the lowest numeric live<id>:resume:Ncontinuation, or creates the next ordinal after the maximum existing numeric suffix (starting at 2, no cap). Non-numeric suffixes and other roots are ignored. The selected continuation follows normal ownership and lease rules; conflicts return409 session_project_conflictwithout advancing further. Concurrent callers converge on one live continuation. - Success remains
201with{id, status:"created"}. A continuation response also includesresumed_from: <root id>and returns the effective ID inid. Use that acknowledged ID for subsequent session-bound operations. MCP session registration does not opt into resume mode. - 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}?expected_project=X— Update fields. Body:{title?, content?, find?, replace?, type?, project?, scope?, topic_key?}findandreplacemust be supplied together and cannot be combined withcontent. They perform a literal, case-sensitive, global replacement inside the existing observation; emptyfind, no match, or normalized-identical output leaves content unchanged.- Each replacement input and the transformed result are bounded by the configured observation content limit.
400is returned for invalid pairs, content conflicts, bounds failures, or title/content validation failures; missing observations return404.
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}?expected_project=X— Delete observation (&hard=truefor hard delete, soft delete by default)200when deleted404when observation does not exist- Both PATCH and DELETE require an explicit
expected_projectowner assertion: missing, blank, or invalid names return400; normalized owner mismatch or immutable project reassignment through PATCH returns409without changing the observation, revision, or sync queue. Personal/global scopes do not bypass ownership. The assertion and mutation run in one store transaction; project metadata remains immutable.
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. Relations normally require both endpoint observations in the resulting store. Audit rows whose normalizedjudgment_statusis exactlyorphanedmay retain missing source and/or target observations; their IDs and metadata are preserved. All other dangling relations and missing superseding relations reject and roll back the full import.
GET /stats— Current-project memory statistics. Use?project=<name>or?all_projects=trueto select scope. Store query failures return500with a generic JSON error ({"error":"stats unavailable"}), not a successful zero-count response. Store-backed project resolution failures return500with{"error":"project resolution failed","code":"project_resolution_failed"}; invalid, unknown, and ambiguous projects retain their input-error responses.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 config --project <project> --server <url> --token <token>/engram cloud config --project <project> --clear— route one project to its own cloud remote, or back to the global remote (see Per-project cloud remotes)engram 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).
The global server URL and token stay the default. Any single project can sync to a different Engram Cloud instead:
engram cloud config --project team-app --server https://team-cloud.example.com --token <token>
engram cloud config --project team-app --clear # back to the global remotecloud.json then holds an optional projects map next to the global settings:
{
"server_url": "https://cloud.example.com",
"token": "global-token",
"projects": {
"team-app": { "server_url": "https://team-cloud.example.com", "token": "team-token" }
}
}ENGRAM_CLOUD_SERVERandENGRAM_CLOUD_TOKENapply only to the global remote. A routed project uses only its ownserver_urlandtoken; the global token is never sent to another server.- When a project's effective remote changes (set, token rotation, or
--clear), its current local state is re-queued for the new remote and a catch-up pull imports what other devices already wrote there. Nothing is deleted locally or on the previous remote. If the catch-up pull fails, the routing change is kept and the command still succeeds; retry withengram sync --cloud --import --project <project>. Unenrolled projects skip both steps. - A project override without a token is saved, but autosync skips that project and explicit cloud sync fails until you re-run the command with
--token. - Running
engram serve/engram mcpprocesses readcloud.jsonat startup: restart them after changing the routing. engram cloud statuslists every project remote with its server, masked token, and remote id.
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, /sync/session-authorities, /sync/prompt-pair-claims) remain header-auth only. For cloud authenticated sync and admin requests, the Authorization parser trims outer whitespace and requires exactly two whitespace-delimited fields: a case-insensitive Bearer scheme and one credential. Tabs or multiple spaces between fields are accepted; whitespace embedded in the credential and a scheme glued to the credential are rejected. This describes field separation, not an RFC credential-character grammar; it does not describe the local ENGRAM_HTTP_TOKEN parser. 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.
These JSON routes require a managed-token admin principal; legacy admins and managed members cannot use them. Successful response shapes are:
| Method | Path | Success JSON |
|---|---|---|
GET |
/admin/users |
Array of users |
POST |
/admin/users |
User object |
POST |
/admin/users/{principalID}/enable |
{"status":"ok","principal_id":...,"enabled":true} |
POST |
/admin/users/{principalID}/disable |
{"status":"ok","principal_id":...,"enabled":false} |
GET |
/admin/users/{principalID}/tokens |
Array of token metadata; no raw token or token hash |
POST |
/admin/users/{principalID}/tokens |
{"raw_token":...,"token":...}; raw token is returned only on creation, alongside token metadata (never the hash) |
POST |
/admin/tokens/{tokenID}/revoke |
{"status":"ok","token_id":...} |
GET |
/admin/users/{principalID}/grants |
Array of grants |
POST |
/admin/users/{principalID}/grants |
Grant object |
POST |
/admin/users/{principalID}/grants/{project}/revoke |
{"status":"ok","principal_id":...,"project":...} |
User objects contain principal_id, username, email, display_name, role, enabled, and created_at; grant objects contain principal_id, project, granted_by_principal_id, and created_at. Token metadata contains id, principal_id, token_prefix, name, created_by_principal_id, and created_at, with optional usage/revocation fields. The POST payloads are JSON: user creation accepts username, email, display_name, role; token creation accepts name; token revocation accepts reason; grant creation accepts project. Enable, disable, and grant revocation use path parameters without a request payload.
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's canonical name: its config first, then existing Git binding or origin remote, then repository-root basename (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 |
A promoted child's project name and path match detection from inside that child; its source remains git_child with an advisory warning. Invalid child config or Git binding fails closed instead of promoting a directory basename. An explicit config at the parent takes precedence over child scanning. Historical memories are not migrated by detection.
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 requires id and caller-supplied expected_project. Native MCP also checks ownership against the known current/process project; the assertion does not bypass those checks, malformed/unknown process overrides, or ambiguous-project recovery protections. It no longer falls back to the stored owner for writes when cwd is ambiguous. mem_get_observation retains its read-only stored-owner fallback.
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. - An unknown non-empty
session_idnormally fails withunknown_session. Only genuine cwd ambiguity offers a short-lived MCP-localrecovery_tokenbound to that session ID, canonical context, and exact unique candidate paths. After explicit user selection,mem_save,mem_save_prompt, ormem_session_summarymay register that ID as project-owned before writing, usingproject,project_choice_reason=user_selected_after_ambiguous_project, and the token. A bare explicit project cannot register an unknown session, even when the project already exists. Recovery never resumes an ended session or changes an unrelated owner. - 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. Tokens expire after five minutes and are valid only in the issuing MCP process. Same-choice retries are allowed; changed-choice, wrong-session, changed-context/candidate, and duplicate case-normalized candidate-path requests fail closed. Tokens are recovery capabilities, not OpenCode authentication. - 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, with the returned recovery_token. A supplied session normally remains authoritative; the same narrow token-validated unknown-session bootstrap applies.
mem_search, mem_context, mem_timeline, mem_stats, mem_doctor, and mem_get_observation accept an optional project argument validated against known projects. Unknown explicit project names return a structured error with available_projects. When project is omitted, the process override takes precedence over cwd detection. For mem_get_observation, the resolved project selects response-envelope context only; retrieval remains ID-based and does not filter by observation ownership.
mem_delete requires id and caller-supplied expected_project; optional hard_delete=true permanently deletes the observation. The owner assertion is not a project-resolution override.
mem_merge_projects requires from (comma-separated, explicitly named source project names) and to (canonical target project name). Case/trim variants and matching -/_ separator variants (for example, foo-bar to foo_bar) are allowed; unrelated names and missing sources are rejected. 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, with mandatory expected_project supplied by the caller (for example, { "id": 42, "expected_project": "engram", "title": "Corrected" }). Public schema supports partial updates for title, content, find, replace, type, scope, and topic_key. find and replace are paired literal, case-sensitive global replacement inputs and cannot be combined with content; empty finds and replacements with no effective normalized change preserve content.
This intentionally breaks mutation clients that omit the owner assertion. Native MCP and the in-repository Pi adapter require it; an external gentle-engram relay must be adapted separately and is not fixed by this repository change. Never read the target observation to manufacture a missing expectation. CLI/internal maintenance store APIs retain their unguarded entry points.
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 with mandatory caller-supplied expected_project, for example { "id": 42, "expected_project": "engram", "hard_delete": true }. Uses soft-delete by default (deleted_at); optional hard-delete for permanent removal. Both modes reject invalid or mismatched assertions before changing data.
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. The optional project argument is validated against known projects and selects the project context in the response envelope; it does not filter the ID-based lookup by observation ownership. When omitted, the tool uses the process project override or cwd detection.
Parameters:
- id (required): int — observation ID to retrieve
- project (optional): string — explicit project context; unknown names return a structured error with
available_projects
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 explicitly named case/trim or corresponding -/_ separator variants into a canonical name. Requires from as a comma-separated list of source project names and to as the target canonical name. For example, foo-bar may merge into foo_bar, but unrelated names are rejected. Sources must exist; the CLI retains its stricter rules. Observations, sessions, prompts, pending sync identity, and enrollment migrate together. A no-op reports that no records moved.
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 merge --from acmeapi --to acme-api --dry-run to preview one explicitly named separator variant without mutation, then --apply to merge it. Exactly one mode and both names are required; unrelated or normalized-identical names are rejected. Preview reports observation, session, and prompt counts plus sync identity changes, even with zero rows. It is point-in-time: apply revalidates and reports actual moved row counts, which may differ. Apply's sync identity message is qualitative, not an actual-change count or a claim that the preview's sync state still holds. Sync-only merges can succeed with zero record moves. The reserved inbox project cannot be a merge destination, including for an explicitly named separator variant such as in-box; preview and apply both reject it.
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. Orphaned relation audit rows may retain missing endpoint observations; other dangling relations or missing superseding relations fail the complete transaction.engram export --helpandengram import --help— Show command-specific usage and options without an update check, configuration lookup, database migration, or store access. Export accepts an optional output filename and--project NAMEor--all; import requires a backup filename for normal operation.
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- Local sync projects hard deletes as canonical observation, prompt, and session delete mutations; child deletes precede their session and remain replay-safe through manifest history.
.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=on-failure
RestartSec=3
Environment=ENGRAM_DATA_DIR=%h/.engram
[Install]
WantedBy=default.targetRestart=on-failure restarts unexpected failures but not a clean exit when the same Engram instance already owns the port. A different or legacy port owner still causes a startup error. Existing installed units are not changed by this example: manually update ~/.config/systemd/user/engram.service and run systemctl --user daemon-reload to apply the policy.
This is the recommended setup for Homebrew users on macOS. With KeepAlive=true, launchd relaunches engram serve automatically after brew upgrade engram replaces the binary, so autosync survives upgrades.
- 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.
The templ CLI is registered as a module tool at v0.3.1001; no global PATH install is required. To download module dependencies ahead of time:
go mod downloadmake templ
# or directly:
go tool templ generate -path ./internal/cloud/dashboardThe 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 checks that generated files are present; CI additionally checks regeneration for drift.
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.
With per-project cloud remotes, autosync runs one worker per distinct remote: the global worker skips routed projects, and each routed remote syncs only its own projects with its own pull cursor and lease. Overrides without a token are skipped with a warning. Restart engram serve / engram mcp after changing the routing.
| 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