Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/lnardev/opencode-config-agent/llms.txt

Use this file to discover all available pages before exploring further.

The Engram plugin is a thin adapter layer that bridges OpenCode’s internal event system to the engram Go binary — a local HTTP server backed by a SQLite database. Every user prompt, tool call, and session summary is silently captured and persisted, making it available to future sessions and surviving context compaction. The plugin handles binary auto-start, project name extraction, session lifecycle management, and system prompt injection, so there is nothing for you to configure manually beyond having the engram binary installed.

Architecture

OpenCode events


Engram plugin (engram.ts)
    │  HTTP calls to http://127.0.0.1:7437

engram serve (Go binary)


SQLite database
The plugin communicates with the binary exclusively over HTTP — all persistence logic lives in the Go binary. If the HTTP call fails (binary not running, network error), the plugin silently returns null and continues without disrupting the agent session.

Configuration

The plugin reads two environment variables at startup:
VariableDefaultPurpose
ENGRAM_PORT7437HTTP port the engram binary listens on
ENGRAM_BINResolved in order: ENGRAM_BIN env var → Bun.which("engram")/opt/homebrew/Cellar/engram/1.10.10/bin/engramPath to the engram binary
# Override port (e.g. if 7437 is in use)
export ENGRAM_PORT=7438

# Override binary path (e.g. custom install location)
export ENGRAM_BIN=/usr/local/bin/engram

What the Plugin Does

1. Auto-Starts the Engram Binary

At plugin initialization, the plugin checks whether engram serve is already running by hitting the /health endpoint with a 500ms timeout. If the binary is not running, it spawns it automatically:
Bun.spawn([ENGRAM_BIN, "serve"], {
  stdout: "ignore",
  stderr: "ignore",
  stdin: "ignore",
})
You never need to run engram serve manually — the plugin handles it.

2. Project Name Extraction

The plugin derives the project name using a three-step fallback chain:
1

Git remote origin URL

Runs git -C {directory} remote get-url origin and extracts the repository name from the URL (stripping .git suffix and taking the last path segment).
2

Git root directory name

Runs git -C {directory} rev-parse --show-toplevel and uses the basename. This handles worktrees correctly.
3

CWD basename

Falls back to directory.split("/").pop() — always succeeds.

3. Session Management

Sessions are created in engram on-demand via ensureSession(). This function is idempotent — it calls POST /sessions which uses INSERT OR IGNORE internally, so calling it multiple times for the same session ID is safe.
Sub-agent session suppression: Sessions created by the task tool (or delegate) have a parentID set, or their title ends with " subagent)". These sessions are tracked in a subAgentSessions set and never registered in engram. This prevents the session-inflation bug (issue #116) where 170 engram sessions were created for a single conversation.

4. Prompt Capture

Every user message longer than 10 characters is sent to the /prompts endpoint before the LLM sees it:
await engramFetch("/prompts", {
  method: "POST",
  body: {
    session_id: sessionId,
    content: stripPrivateTags(truncate(finalContent, 2000)),
    project,
  },
})
The chat.message hook handles this. Content is truncated to 2000 characters and has private tags stripped before transmission.

5. Tool Call Counting

The tool.execute.after hook fires after every tool execution. The plugin increments a per-session counter for all tools except Engram’s own tools (listed in ENGRAM_TOOLS). This gives meaningful session statistics without inflating counts with memory operations.

6. Passive Learning from Task Tool

When the Task tool completes and produces output longer than 50 characters, the plugin automatically sends that output to the /observations/passive endpoint:
if (input.tool === "Task" && output && sessionId) {
  await engramFetch("/observations/passive", {
    method: "POST",
    body: {
      session_id: sessionId,
      content: stripPrivateTags(text),
      project,
      source: "task-complete",
    },
  })
}
The engram binary’s passive capture pipeline extracts learnings from this output automatically — no explicit mem_save call required.

7. System Prompt Injection

On every message, MEMORY_INSTRUCTIONS are appended to the last entry of output.system. This is intentionally an append — not a push — to avoid creating multiple system blocks:
if (output.system.length > 0) {
  output.system[output.system.length - 1] += "\n\n" + MEMORY_INSTRUCTIONS
} else {
  output.system.push(MEMORY_INSTRUCTIONS)
}
Appending to the last system entry (rather than adding a new system message) ensures compatibility with models like Qwen3.5 and Mistral/Ministral that only accept a single system block in their Jinja chat templates.
The injected instructions tell the agent:
  • When to call mem_save (mandatory after bug fixes, decisions, discoveries, etc.)
  • When and how to search memory (mem_context first, then mem_search)
  • The session close protocol: always call mem_session_summary before ending
  • What to do after compaction: save the compacted summary to engram first, then recover context

8. Compaction Hook

When OpenCode compacts a session, the experimental.session.compacting hook fires. The plugin:
  1. Ensures the session exists in engram
  2. Fetches recent context from /context?project={project} and injects it into output.context
  3. Appends a critical instruction telling the compressor to embed a FIRST ACTION REQUIRED directive in the compacted summary — instructing the new agent to call mem_session_summary immediately on startup
output.context.push(
  `CRITICAL INSTRUCTION FOR COMPACTED SUMMARY:\n` +
  `The agent has access to Engram persistent memory via MCP tools.\n` +
  `You MUST include the following instruction at the TOP of the compacted summary:\n\n` +
  `"FIRST ACTION REQUIRED: Call mem_session_summary with the content of this ` +
  `compacted summary. Use project: '${project}'. This preserves what was ` +
  `accomplished before compaction. Do this BEFORE any other work."\n\n` +
  `This is NOT optional. Without this, everything done before compaction is lost from memory.`
)

9. Auto-Import from Git

If a .engram/manifest.json file exists in the project directory, the plugin runs engram sync --import at startup. This loads any git-synced memory chunks into the local SQLite database — useful when cloning a repository that includes shared team memories.
const manifestFile = `${ctx.directory}/.engram/manifest.json`
const file = Bun.file(manifestFile)
if (await file.exists()) {
  Bun.spawn([ENGRAM_BIN, "sync", "--import"], { cwd: ctx.directory, ... })
}

10. Project Migration

If the project name derived at startup differs from the previous basename (e.g., after the git remote detection fix correctly identifies the repo name), the plugin calls /projects/migrate to move all stored data to the new project name:
if (oldProject !== project) {
  await engramFetch("/projects/migrate", {
    method: "POST",
    body: { old_project: oldProject, new_project: project },
  })
}

Private Tag Stripping

Any content wrapped in <private>...</private> tags is stripped before being sent to engram. The replacement text is [REDACTED].
function stripPrivateTags(str: string): string {
  return str.replace(/<private>[\s\S]*?<\/private>/gi, "[REDACTED]").trim()
}
This stripping happens in the plugin (TypeScript) before the HTTP call, so sensitive content never hits the wire. As a second layer of safety, the engram Go binary also strips private tags on its end.

Engram Tools

The following tools are provided by the engram MCP server and are excluded from tool-call counting:
mem_save          — Save a new observation/memory
mem_update        — Update an existing observation by ID
mem_delete        — Delete an observation by ID
mem_search        — Full-text search across all memories (FTS5)
mem_get_observation — Retrieve full (untruncated) observation content

Sub-Agent Session Suppression

// Sub-agent sessions are detected by:
const isSubAgent = !!parentID || title.endsWith(" subagent)")

if (sessionId && isSubAgent) {
  subAgentSessions.add(sessionId) // suppress all engram calls for this session
}
Sub-agent sessions are added to subAgentSessions at session.created time. All subsequent hooks (chat.message, tool.execute.after, ensureSession) check this set and silently skip sub-agent sessions.

Dependency

The Engram plugin is distributed as part of this config. The @opencode-ai/plugin package (used for the Plugin type) is declared in package.json:
{
  "dependencies": {
    "@opencode-ai/plugin": "1.4.6"
  }
}
The engram binary must be installed separately — see the MCP Servers page for installation details.

Build docs developers (and LLMs) love