Generated banner for forgetful
Generated logo for forgetful

forgetful

Memory server

Created by Scott

Published in ScottRBK/forgetful. Publisher page

339 GitHub stars

What it is

Opensource Memory for Agents
— the description by Scott

AutoGineer overview, from the brain’s own files

Forgetful is a storage and retrieval tool designed as a Model Context Protocol (MCP) server. It provides AI agents with persistent, semantically searchable memory, enabling them to maintain context across sessions and different agentic applications. By imposing the Zettelkasten principle, Forgetful requires memories to be atomic, which facilitates the automatic construction of a knowledge graph through semantic embedding and linking. It supports flexible storage backends including SQLite for local development and PostgreSQL for production, and uses local embedding models to ensure privacy and performance. Beyond simple memory, it supports entities, projects, and procedural skills, allowing agents to coordinate tasks and maintain complex relationships between data points.

How it works

Memory model: Hybrid

AutoGineer overview, from the brain’s own files

Forgetful functions as an MCP server that exposes meta-tools to connected AI agents. When an agent records a memory, it must provide a title, content, and context, which the server then converts into semantic embeddings using local models. The system automatically links new memories to existing ones based on similarity scores, building a knowledge graph. It manages the agent's context window by enforcing a token budget and ranking retrieved information by importance. The system also supports entities and relationships, allowing for structured data management alongside unstructured memory notes.

Folders
FolderRoleFiles
.githubagent-config10
appsource-code153
docsdocs46
examplesexamples1
skillsagent-instructions13
teststests182
alembicfolder18
benchmarksfolder6
dockerfolder5
nixfolder1
test_harnessfolder12

Requirements

Runtimes: Python >=3.12

Databases: PostgreSQL, Qdrant, SQLite

API keys (by name): FORGETFUL_TOKEN

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Google Antigravity, Kiro, OpenCode, Amp, Roo Code, Cline, JetBrains Junie, Devin (ex-Windsurf)

Set it up

  1. This brain runs code automatically: 2 scripts. Review app/bootstrap.py, pyproject.toml before opening the folder in Claude Code. No step below turns a hook on for you.

    • app/bootstrap.py: python app/bootstrap.py
    • pyproject.toml: hatch build hook vcs (hatch_build.py)
  2. Not scanned by AutoGineerThe author’s README also describes installing it with Docker Compose. Read its install section: AutoGineer did not scan that path. https://github.com/ScottRBK/forgetful/blob/9f85ddcf2ca1ae0264358e0b4b50860948ec1e03/README.md#option-3-docker-deployment-productionscale
  3. Not scanned by AutoGineerThe author’s README also describes installing it from a clone of its repository. Read its install section: AutoGineer did not scan that path. https://github.com/ScottRBK/forgetful/blob/9f85ddcf2ca1ae0264358e0b4b50860948ec1e03/README.md#quick-start
  4. Not scanned by AutoGineerIts dependencies are installed by pip or uv when you run it and were not scanned by AutoGineer (28 declared). https://pypi.org/project/forgetful-ai/
  5. Created by Scott. Published in ScottRBK/forgetful. Licence: MIT License.

Usage

AutoGineer overview, from the brain’s own files

  • Connect to AI agents via MCP (stdio or HTTP).
  • Store atomic memories with context and tags.
  • Query memories using natural language with semantic search.
  • Manage projects and link memories to specific work streams.
  • Track entities and their relationships for team or system context.
  • Execute procedural skills stored as step-by-step instructions.
“Create a memory about a technical decision.”
The agent saves an atomic note linked to a project, which is then semantically indexed and linked to related past decisions.
“Find information about a specific team member's role.”
The agent retrieves the entity record for the person and any associated memories or relationship metadata.

Documentation by Scott

<h1 align="center">Forgetful</h1>

<p align="center"> <img src="https://img.shields.io/badge/python-3.12%2B-blue" alt="Python"> <img src="https://img.shields.io/badge/license-MIT-green" alt="License"> <img src="https://img.shields.io/badge/MCP-server-purple" alt="MCP"> <a href="https://github.com/jlowin/fastmcp"> <img src="https://img.shields.io/badge/FastMCP-powered-orange" alt="FastMCP"> </a> <a href="https://github.com/qdrant/fastembed"> <img src="https://img.shields.io/badge/FastEmbed-powered-orange" alt="FastEmbed"> </a> <a href="https://discord.gg/ngaUjKWkFJ"> <img src="https://img.shields.io/badge/Discord-Join%20Us-7289da?logo=discord&amp;logoColor=white" alt="Discord"> </a> </p>

Forgetful is a storage and retrieval tool for AI Agents. Designed as a Model Context Protocol (MCP) server built using the FastMCP framework. Once connected to this service, MCP clients such as Coding Agents, Chat Bots or your own custom built Agents can store and retrieve information from the same knowledge base.

Image: Banner


Table of Contents

  • Overview

  • Features

  • Quick Start

  • CLI

  • Usage Examples

  • How It Works

  • Configuration

  • Documentation

  • Contributing

  • License


Overview

A lot of us are using AI Agents now, especially in the realm of software development. The pace at which work and decisions are made can make it difficult for you to keep up from a notes and context persistence perspective.

So if you are following something like the BMAD Method for example and you want to take your brain storming session you've just had with Claude on your desktop/mobile and use it for the basis of your next Claude Code session, then having a shared knowledge base across the two agents can help with this.

This is just one example use case to illustrate the point, more and more agentic applications are going to surface and the use cases for sharing data across them is going to increase.

Knowledge bases are going to become a key infrastructure component for your interactions with AIs. There are many excellent knowledge base solutions available (many for free on github) and I would encourage you to check them out and find one that works for you (even if Forgetful doesn't) as I found from personal experience that interactions with my agents got easier and more rewarding once they knew more about me, my work and previous interactions that I had had with them or other AI systems.

What makes Forgetful different from other Memory based MCP services is that it is a rather opinionated view on how AI Agents such store and retrieve data.

Forgetful imposes the Zettelkasten principle when clients wish to record memories, that is each memory must be atomic (one concept per note). Along with the note (title and content), we also ask the client / agent to provide context around what it was doing when creating the note, along with keywords and tags. With this information we create semantic embeddings and store these to aid with later retrieval and in addition to this we also automatically link the memory to existing memories that have a particular similarity score, allowing for the automatic construction of a knowledge graph.

In this sense Forgetful becomes a little bit like Obsidian for AI Agents, where the auto linking nudges them in building up a graph of the knowledge.

We find, as do others (A-MEM: Agentic Memory or LLM Agents), all this helps in ensuring that when the agent requires relevant information from the memory system later, the correct information is returned.

In addition to just memories, Forgetful also has the concept of entities (think organisation, people, products), projects, documents, code artifacts, skills (procedural knowledge following the Agent Skills standard), and plans with tasks for multi-agent coordination, all of which can be associated with one or more memories.

Image: Architecture

Features

  • Configure either STDIO or HTTP transport mechanism (or stand up two services to support both)

  • Multiple Authentication supported, flows see FastMCP docs for full list

  • Meta Tool Discovery, only three tools exposed to client application to preserve context window.

  • Flexible Storage– SQLite (default, zero-config) or PostgreSQL (for scale and production deployments)

  • Stores memories as vectors and allowing memories to be retrieved from natural language queries from AI.

  • Cross Encoder reranking to improve recall and precision of memory retrieval.

  • Flexible ranking (embedding and cross encoder) providers, run everything locally without calls to the cloud thanks to FastEmbed

  • Automatic linking of semantically similar memories, automating the creation of the knowledge graph.

  • Plans and Tasks for multi-agent coordination -- structure work into plans with tasks that have acceptance criteria, state management with optimistic locking, and dependency tracking with cycle detection.

  • Skills for procedural memory -- store step-by-step instructions and agent capabilities with semantic search, import/export in Agent Skills SKILL.md format, and cross-referencing with memories.

For the complete roadmap, see Features Roadmap.


Quick Start

# Run directly with uvx (no installation needed)
uvx forgetful-ai

# Or install globally
uv tool install forgetful-ai
forgetful

Data stored in platform-appropriate locations (~/.local/share/forgetful on Linux/Mac, AppData on Windows).

By default, runs with stdio transport for MCP clients. For HTTP:

uvx forgetful-ai --transport http --port 8020
Option 2: From Source
git clone https://github.com/ScottRBK/forgetful.git
cd forgetful

# Install dependencies with uv
uv sync

# Run the server (uses SQLite by default)
uv run main.py

The server starts with stdio transport. For HTTP: uv run main.py --transport http

Option 3: Docker Deployment (Production/Scale)

Forgetful provides two Docker deployment options:

SQLite with Docker (Simpler, Single-Container)

See docker-compose.sqlite.yml

cd docker
cp .env.example .env
# Edit .env: Set DATABASE=SQLite and SQLITE_PATH=data/forgetful.db
docker compose -f docker-compose.sqlite.yml up -d

The SQLite database persists in the ./data directory on the host.

See docker-compose.postgres.yml and .env.example

cd docker
cp .env.example .env
# Edit .env: Set DATABASE=Postgres and configure POSTGRES_* settings
docker compose -f docker-compose.postgres.yml up -d

Note: If no .env file exists, the application uses defaults from app/config/settings.py. For all configuration options, see Configuration Guide.

Connecting to An Agent

For detailed connection guides (Claude Code, Claude Desktop, other clients that support MCP), see Connectivity Guide.

Add Forgetful to your MCP client configuration:

stdio transport (recommended for local use):

{
  "mcpServers": {
    "forgetful": {
      "type": "stdio",
      "command": "uvx",
      "args": ["forgetful-ai"]
    }
  }
}

HTTP transport (for Docker/remote):

{
  "mcpServers": {
    "forgetful": {
      "type": "http",
      "url": "http://localhost:8020/mcp"
    }
  }
}

CLI

The forgetful command is also a full terminal client over the same tool registry the MCP meta-tools use - against your local database by default, or a remote deployment after auth login.

uv tool install forgetful-ai

# Curated verbs for daily use
forgetful memory save "Set generateResolvConf false to fix WSL2 DNS" \
    --title "WSL2 DNS fix" --importance 7
forgetful memory search "wsl dns" -c "wsl networking" -n 5
forgetful memory get 812
forgetful memory recent -n 10 -p my-project
forgetful project list

# Generic passthrough to any available tool
forgetful tools list --category memory
forgetful tools info query_memory
forgetful call create_project --args '{"name": "Homelab", "description": "...", "project_type": "personal"}'

# Remote deployment (browser OAuth; saves FORGETFUL_SERVER to ~/.config/forgetful/.env)
forgetful auth login --server https://forgetful.example.com
forgetful auth status
forgetful memory search "wsl dns" -c "wsl networking"          # now runs remotely
forgetful memory search "wsl dns" -c "wsl networking" --local  # force local mode per invocation

# Scripting: --json emits machine-readable output
forgetful memory search "wsl dns" -c "wsl networking" --json | jq '.primary_memories[0].id'

forgetful serve is the canonical way to run the MCP server (forgetful serve --transport http --port 8020); the bare forgetful / uvx forgetful-ai invocation and the legacy --transport/--re-embed flags keep working indefinitely, so existing MCP client configurations are unaffected. Headless environments can set FORGETFUL_TOKEN (bearer) instead of the OAuth flow. See the Configuration Guide for precedence rules, or walk through the forgetful-cli-setup skill for install, auth, and verification steps end-to-end.


Usage Examples

Forgetful exposes only 3 meta-tools to MCP clients. The tools available through execute_forgetful_tool depend on the enabled feature flags. Use discover_forgetful_tools for the current runtime catalog.

Example 1: Project-Scoped Memory

Create a memory linked to a project for better organization and scoped retrieval.

# Create project for organizing related knowledge
project = execute_forgetful_tool(
    "create_project",
    {
        "name": "E-Commerce Platform Redesign",
        "project_type": "work",
        "status": "active"
    }
)

# Create memory linked to project
memory = execute_forgetful_tool(
    "create_memory",
    {
        "title": "Payment gateway: Stripe chosen over PayPal",
        "content": "Selected Stripe for its API, fees, and fraud detection.",
        "context": "Choosing the payment provider for the redesign",
        "keywords": ["payment", "stripe", "paypal"],
        "tags": ["payment", "decision"],
        "importance": 9,
        "project_ids": [project["id"]]
    }
)

# Later, query within project scope
results = execute_forgetful_tool(
    "query_memory",
    {
        "query": "payment processing implementation",
        "query_context": "Implementing payments for the redesign",
        "project_ids": [project["id"]]
    }
)
# Returns: Stripe decision + auto-linked related memories
Example 2: Knowledge Graph with Entities

Track people, organizations, and relationships - perfect for team and infrastructure management.

# New engineer joins your company
new_hire = execute_forgetful_tool(
    "create_entity",
    {
        "name": "Jordan Taylor",
        "entity_type": "Individual",
        "description": "Backend Engineer - Payments Team",
        "tags": ["engineering", "backend", "payments"]
    }
)

# Get company entity (create if needed)
company = execute_forgetful_tool(
    "create_entity",
    {
        "name": "TechFlow Systems",
        "entity_type": "Organization",
        "description": "SaaS platform company"
    }
)

# Create employment relationship
execute_forgetful_tool(
    "create_entity_relationship",
    {
        "from_entity_id": new_hire["id"],
        "to_entity_id": company["id"],
        "relationship_type": "works_for",
        "metadata": {
            "role": "Backend Engineer II",
            "department": "Payments",
            "start_date": "2025-01-20"
        }
    }
)

# Create memory about hiring
hire_memory = execute_forgetful_tool(
    "create_memory",
    {
        "title": "Jordan Taylor hired - payments focus",
        "content": "Jordan joins to build the Stripe integration and handle PCI compliance.",
        "context": "Recording ownership and experience for the payments work",
        "keywords": ["jordan", "stripe", "payments", "pci"],
        "tags": ["team", "hiring", "payments"],
        "importance": 7
    }
)

# Link person to memory
execute_forgetful_tool(
    "link_entity_to_memory",
    {
        "entity_id": new_hire["id"],
        "memory_id": hire_memory["id"]
    }
)

# Query Jordan's related knowledge
results = execute_forgetful_tool(
    "query_memory",
    {
        "query": "Jordan payment implementation",
        "query_context": "Finding ownership and experience for payments work"
    }
)
# Returns: Hiring memory + linked entity + relationship context
Tool Categories

The core catalog covers users, memories, projects, entities, code artifacts, and documents. Skills, files, plans, and tasks appear when their feature flags are enabled.

For complete documentation with extensive examples, see Complete Tool Reference.


How It Works

Atomic Memory Principle

Inspired by Zettelkasten, each memory stores one concept in ~300-400 words:

  • Easily titled – Forces clarity (200 char limit)

  • Self-contained – Understandable without external context

  • Linkable – Small units enable precise knowledge graphs

For detailed content, use Documents and extract 3-7 atomic memories that link to the parent document.

Automatic Knowledge Graph

When you create a memory:

  1. Embedding generated – FastEmbed converts content to 384-dimensional vector

  2. Similarity search – Finds related memories (similarity ≥0.7 by default)

  3. Auto-linking – Creates bidirectional links to top 3-5 matches (configurable)

  4. Graph traversal – Queries return primary results + 1-hop linked memories

Entities and Knowledge Graphs

Entities represent concrete, real-world things (people, organizations, teams, devices) that can be linked to memories:

  • Typed entities – Organizations, Individuals, Teams, Devices, Systems, or custom types

  • Relationships – Directional connections (e.g., "Person works_at Organization") with strength and metadata

  • Memory linking – Associate entities with relevant memories for context

  • Knowledge graph – Build networks showing how entities relate to each other and your knowledge base

Use entities for concrete things (Sarah Chen, TechFlow Systems, Cache Server 01) and memories for abstract concepts (architectural patterns, decisions, learnings).

Token Budget Management

Prevents context window overflow:

  • Configurable budget (default 8K tokens)

  • Search finds relevant memories; primary results are then sorted by importance (high first)

  • Equal-importance primary results keep their search order, even when nothing is truncated

  • Truncates gracefully if over budget

  • Respects max memory count (default 20)

This keeps query results within the LLM's context budget.

For vector search and optional cross-encoder reranking, see Search Documentation.


Configuration

No configuration required – Forgetful uses sensible defaults out of the box.

Key Settings (Optional)
  • MEMORY_TOKEN_BUDGET – Max tokens for query results (default: 8000)

  • EMBEDDING_MODEL – Embedding model (default: BAAI/bge-small-en-v1.5)

  • MEMORY_NUM_AUTO_LINK – Auto-link count (default: 3, set 0 to disable)

  • MEMORY_SIMILARITY_THRESHOLD – Minimum cosine similarity for auto-links (default: 0.7)

  • SERVER_PORT – HTTP server port (default: 8020)

  • MAX_GRAPH_LIMIT – Upper bound for /api/v1/graph ?limit and /api/v1/graph/subgraph ?max_nodes (default: 2000)

For all 40+ environment variables with detailed explanations, see Configuration Guide.


Documentation

Guides
External Resources

Contributing

We welcome contributions! Forgetful uses integration + E2E testing with Docker Compose orchestration.

See the contributor guide for:

  • Testing workflows (integration tests, E2E tests, GitHub Actions)

  • Development setup (local vs Docker)

  • CI/CD pipeline details

  • Release process


License

MIT License - see LICENSE for details.

docs/api_reference.md

REST API Reference

Forgetful exposes a REST API for web UI integration and external access.

Base URL: http://localhost:8020/api/v1

Health and capability discovery use the application base URL, outside /api/v1.

Authentication

The REST API uses the same authentication provider as MCP routes. This means:

  • If you've configured OAuth2/OIDC for MCP, the same tokens work for REST

  • If auth is disabled (FASTMCP_SERVER_AUTH not set), REST endpoints use a default user

  • All FastMCP auth providers are supported (JWT, OAuth2, GitHub, Google, Azure, etc.)

When Auth is Enabled

Include a Bearer token in the Authorization header:

Authorization: Bearer <your-oauth-token>

The token is validated using mcp.auth.verify_token() - the same mechanism used for MCP tool authentication. User identity is extracted from the token's sub claim.

When Auth is Disabled (Default)

If FASTMCP_SERVER_AUTH is not set, domain requests use a default user. No Authorization header is required. Public capability discovery does not resolve or create a user.

Configuration

Authentication is configured via FastMCP environment variables:

# Example: GitHub OAuth
FASTMCP_SERVER_AUTH=github
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret

# Example: Bearer token (simple API key)
FASTMCP_SERVER_AUTH=bearer
FASTMCP_SERVER_AUTH_SECRET=your-secret-key

See FastMCP Auth Documentation for all supported providers and configuration options.

Common Response Codes

CodeDescription
200Success
201Created
400Bad Request (validation error, CyclicDependencyError)
401Unauthorized (missing/invalid token)
404Not Found (also returned when a referenced project_id does not exist or belongs to another user)
409Conflict (ConflictError -- version mismatch in optimistic concurrency)
422Unprocessable Entity (InvalidStateTransitionError, DependencyNotMetError)
500Internal Server Error

Health

GET /health

Health check endpoint (no authentication required).

Response:

{
  "status": "healthy",
  "timestamp": "2024-12-05T10:00:00Z",
  "service": "forgetful",
  "version": "0.3.0"
}

Capability discovery

GET /.well-known

Public JSON metadata describing which optional features the instance has enabled. No credentials are required, even when authentication is enabled. All callers receive the same schema version and capability booleans; the response does not expose raw configuration or user data.

The path is relative to the application's deployment base, not /api/v1 or the MCP endpoint:

  • Root deployment: https://host/.well-known.

  • Deployment at https://host/forgetful/: https://host/forgetful/.well-known.

Preserve the deployment prefix when building this URL. /forgetful/ is an example prefix, not a hardcoded route. This project-specific endpoint is separate from OAuth's standard well-known routes.

Response (example):

{
  "schema_version": 1,
  "capabilities": {
    "planning": true,
    "skills": false,
    "files": true,
    "activity_stream": false
  }
}

Each value maps directly to the settings singleton:

CapabilitySettingMeaning
planningPLANNING_ENABLEDPlans and tasks are enabled together.
skillsSKILLS_ENABLEDSkill operations are enabled.
filesFILES_ENABLEDFile operations are enabled.
activity_streamACTIVITY_ENABLEDActivity tracking and live streaming are enabled.

activity_stream: false does not disable queries of previously stored activity history.

Discovery remains available when all optional features are disabled. Each request reads settings without a server-side response cache, database queries, model calls, or user creation. Clients own caching and refresh policy. These booleans describe configuration, not service health, the caller's permissions, or a guarantee that a feature request will succeed. Feature routes keep their normal authentication and authorization requirements.

For schema version 1, clients should ignore additional capability keys. Only an explicit false means disabled; a missing key, unsupported schema version, or unavailable document means unknown. Older servers may not provide this endpoint. Clients may fall back to probing a feature route, but must not treat authentication errors, routing errors, or temporary outages as a disabled feature.


Memories

GET /api/v1/memories

List memories with pagination, sorting, and filtering.

Query Parameters:

ParameterTypeDefaultDescription
limitint20Results per page (1-100)
offsetint0Skip N results
sort_bystringcreated_atSort field: created_at, updated_at, importance
sort_orderstringdescSort direction: asc, desc
project_idint-Filter by project
importance_minint-Minimum importance (1-10)
tagsstring-Comma-separated tags
include_obsoleteboolfalseInclude obsolete memories

Response:

{
  "memories": [
    {
      "id": 1,
      "title": "Memory title",
      "content": "Memory content...",
      "importance": 7,
      "tags": ["tag1", "tag2"],
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z",
      "linked_memory_ids": [2, 3],
      "project_ids": [1],
      "source_repo": "owner/repo",
      "source_files": ["path/to/file.py"],
      "source_url": "https://example.com/source",
      "confidence": 0.85,
      "encoding_agent": "claude-sonnet-4",
      "encoding_version": "1.0.0",
      "access_count": 0,
      "last_accessed_at": null
    }
  ],
  "total": 42,
  "limit": 20,
  "offset": 0
}
GET /api/v1/memories/{id}

Get a single memory by ID.

Response: Memory object (see above)

The object includes server-managed access_count (int, default 0) and last_accessed_at (ISO datetime or null). Callers cannot set them on POST/PUT; incrementing them does not change updated_at. On upstream, counters increment only when memory.read / memory.queried events fire (get_memory / query_memory), which requires ACTIVITY_TRACK_READS=true and a wired EventBus (default off → counters stay 0). REST listing does not increment them.

POST /api/v1/memories

Create a new memory.

A configured Memory Plugin can allow, warn, or reject before persistence, and advise after a successful save. Successful create, update, and DELETE responses add plugin_warnings only when nonempty, for example:

{"plugin_warnings": [{"outcome": "warn", "message": "Check the source.", "code": "source"}]}

This field augments the normal response; existing lists (including obsolete_matches) remain. Warnings are not stored or returned by reads, lists, queries, or events. In enforce mode, rejection returns 400 with {"error": "<plugin message>"}; before-handler failure/timeout returns 503 with {"error": "Memory Write Plugin unavailable."}. In warn mode both proceed with warnings. After-handler failures preserve success and add a generic warning with code MEMORY_PLUGIN_AFTER_SAVE_UNAVAILABLE. Near-duplicate warnings use create's existing similarity results after persistence; they do not prevent duplicates. External cancellation or network failure can still lose a response after a write; verify state before retrying.

Request Body:

{
  "title": "Memory title",
  "content": "Memory content...",
  "context": "Why this is being stored",
  "keywords": ["keyword1", "keyword2"],
  "tags": ["tag1"],
  "importance": 7,
  "project_ids": [1],
  "source_repo": "owner/repo",
  "source_files": ["path/to/file.py"],
  "source_url": "https://example.com/source",
  "confidence": 0.85,
  "encoding_agent": "claude-sonnet-4",
  "encoding_version": "1.0.0"
}

Provenance Fields (all optional):

FieldTypeDescription
source_repostringRepository source (e.g., 'owner/repo', max 200 chars)
source_filesstring[]List of file paths that informed this memory
source_urlstringURL to original source material (max 2048 chars)
confidencefloatEncoding confidence score (0.0-1.0)
encoding_agentstringAgent/process that created this memory (max 100 chars)
encoding_versionstringVersion of encoding process/prompt (max 50 chars)

Response (201):

{
  "id": 1,
  "title": "Memory title",
  "linked_memory_ids": [],
  "project_ids": [1],
  "code_artifact_ids": [],
  "document_ids": [],
  "similar_memories": []
}
PUT /api/v1/memories/{id}

Update an existing memory.

Request Body: (all fields optional)

{
  "title": "Updated title",
  "content": "Updated content",
  "importance": 8,
  "tags": ["updated-tag"],
  "source_repo": "owner/repo",
  "source_files": ["path/to/file.py"],
  "source_url": "https://example.com/source",
  "confidence": 0.9,
  "encoding_agent": "manual-review",
  "encoding_version": "1.0.0"
}

Provenance fields can be added or updated after memory creation. See POST /api/v1/memories for field descriptions.

Response: Flat updated memory object, with plugin_warnings only when nonempty (see create above). No-op updates skip both phases. Before rejection returns 400; unavailable returns 503. An invalid merged candidate returns 400 through validation handling.

DELETE /api/v1/memories/{id}

Mark a memory as obsolete (soft delete). Registered obsolete handlers run in both phases. Before rejection/unavailability uses the same 400/503 responses as create. Successful responses include plugin_warnings only when nonempty; after-handler failures remain successful.

When supplied, reason must be a string and superseded_by must be an integer or null. Omitted fields default to "Marked obsolete via API" and null; omitting the body uses both defaults. Malformed JSON, a non-object body, or invalid field types return 400 before changing the memory, whether or not a plugin is configured.

Request Body: (optional)

{
  "reason": "No longer relevant",
  "superseded_by": 42
}

Response:

{
  "success": true
}
POST /api/v1/memories/search

Semantic search across memories.

Request Body:

{
  "query": "search query",
  "query_context": "why searching",
  "k": 10,
  "include_links": true,
  "importance_threshold": 5,
  "project_ids": [1, 2]
}

Response:

{
  "memories": [...],
  "total": 10
}
POST /api/v1/memories/{id}/links

Link memories together (bidirectional).

Request Body:

{
  "related_ids": [2, 3, 4]
}

Response:

{
  "linked_ids": [2, 3, 4]
}

Get memories linked to this memory.

Query Parameters:

  • limit (int, default 20): Max linked memories to return

Response:

{
  "memory_id": 1,
  "linked_memories": [...]
}

Remove a link between two memories (bidirectional).

Response:

{
  "success": true
}

Projects

GET /api/v1/projects

List all projects.

Query Parameters:

ParameterTypeDescription
statusstringFilter by status: active, archived, completed
repo_namestringMatch equivalent repository addresses or literal identifiers

Repository lookup recognises equivalent HTTPS, SSH, and host-qualified addresses. For example, github.com/ScottRBK/forgetful, https://github.com/ScottRBK/forgetful.git, and [e-mail address on the upstream page](https://github.com/ScottRBK/forgetful/blob/9f85ddcf2ca1ae0264358e0b4b50860948ec1e03/docs/api_reference.md):ScottRBK/forgetful.git match each other. A trailing slash is optional. Hostnames are case-insensitive; repository paths and explicit ports remain significant. Hostless paths such as group/subgroup/repo do not imply GitHub or another host. Unsupported or ambiguous forms use literal matching. Lookup returns every matching project and can be combined with the status filter; it does not perform partial-name or fuzzy search.

Response: ProjectSummary items (lightweight; no description or notes).

{
  "projects": [
    {
      "id": 1,
      "name": "Project Name",
      "project_type": "development",
      "status": "active",
      "repo_name": "owner/repo",
      "last_encoding_point": "5d41402abc4b2a76b9719d911017c592",
      "memory_count": 12,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}
GET /api/v1/projects/{id}

Get a single project by ID.

Response: Full Project object (includes description, notes, provenance fields, memory_count, and last_encoding_point when set). Shape differs from list ProjectSummary items.

POST /api/v1/projects

Create a new project.

repo_name is an optional Git repository identifier or address (maximum 255 characters). Nested paths, host-qualified identifiers, HTTPS/SSH addresses, and opaque values are accepted. Surrounding whitespace is trimmed; the supplied format is otherwise preserved in responses. An empty string clears the repository when updating a project. Invalid project fields return HTTP 400 with JSON field locations and error messages.

Request Body:

{
  "name": "Project Name",
  "description": "Project description",
  "project_type": "development",
  "repo_name": "owner/repo",
  "last_encoding_point": "5d41402abc4b2a76b9719d911017c592"
}

Project Types: personal, work, learning, development, infrastructure, template, product, documentation, open-source

last_encoding_point: Opaque, caller-managed checkpoint through which memories were last encoded for this project (e.g. a Git commit SHA). The server never infers or advances it; the encoding client updates it only after a successful run.

PUT /api/v1/projects/{id}

Update an existing project.

Request Body: (all fields optional)

{
  "name": "Updated Name",
  "description": "Updated description",
  "status": "completed",
  "last_encoding_point": "7d793037a0760186574b0282f2ebc2eb"
}

Omit last_encoding_point to leave the stored value unchanged; send an empty string to clear it.

DELETE /api/v1/projects/{id}

Delete a project (preserves associated memories).


Entities

GET /api/v1/entities

List all entities.

Query Parameters:

ParameterTypeDescription
entity_typestringFilter by type: Individual, Organization, Team, Device, System, Other

Response:

{
  "entities": [
    {
      "id": 1,
      "name": "Entity Name",
      "entity_type": "Individual",
      "custom_type": null,
      "notes": "Some notes",
      "created_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 10
}
GET /api/v1/entities/{id}

Get a single entity by ID.

POST /api/v1/entities

Create a new entity.

Request Body:

{
  "name": "Entity Name",
  "entity_type": "Individual",
  "custom_type": "Custom Type (if entity_type is Other)",
  "notes": "Optional notes"
}
PUT /api/v1/entities/{id}

Update an existing entity.

DELETE /api/v1/entities/{id}

Delete an entity. Successful responses keep {"success": true} and add plugin_warnings only when nonempty when an Entity Plugin is registered. Before-save rejections return 400; unavailable before-save returns 503 with Entity Write Plugin unavailable.

Entity create (201) and update responses stay flat entity objects with plugin_warnings only when nonempty, matching memory write responses.

POST /api/v1/entities/search

Search entities by name.

Request Body:

{
  "query": "search term",
  "entity_type": "Individual",
  "limit": 10
}
POST /api/v1/entities/{id}/memories

Link an entity to a memory.

Request Body:

{
  "memory_id": 1
}
DELETE /api/v1/entities/{id}/memories/{memory_id}

Remove link between entity and memory.

GET /api/v1/entities/{id}/relationships

Get relationships for an entity.

Response:

{
  "relationships": [
    {
      "id": 1,
      "source_entity_id": 1,
      "target_entity_id": 2,
      "relationship_type": "manages",
      "description": "Team lead"
    }
  ],
  "total": 3
}
POST /api/v1/entities/{id}/relationships

Create a relationship between entities.

Request Body:

{
  "target_entity_id": 2,
  "relationship_type": "manages",
  "description": "Optional description"
}
PUT /api/v1/entities/relationships/{id}

Update an entity relationship.

DELETE /api/v1/entities/relationships/{id}

Delete an entity relationship.


Documents

GET /api/v1/documents

List all documents.

Query Parameters:

ParameterTypeDescription
project_idintFilter by project
document_typestringFilter by type
tagsstringComma-separated tags

Response:

{
  "documents": [
    {
      "id": 1,
      "title": "Document Title",
      "description": "Brief description",
      "content": "Full document content...",
      "document_type": "analysis",
      "tags": ["tag1"],
      "created_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}

Document Types: analysis, guide, specification, report, note, reference

GET /api/v1/documents/{id}

Get a single document by ID.

POST /api/v1/documents

Create a new document.

Request Body:

{
  "title": "Document Title",
  "description": "Brief description",
  "content": "Full document content...",
  "document_type": "analysis",
  "tags": ["tag1"]
}
PUT /api/v1/documents/{id}

Update an existing document.

DELETE /api/v1/documents/{id}

Delete a document.


Code Artifacts

GET /api/v1/code-artifacts

List all code artifacts.

Query Parameters:

ParameterTypeDescription
project_idintFilter by project
languagestringFilter by programming language
tagsstringComma-separated tags

Response:

{
  "code_artifacts": [
    {
      "id": 1,
      "title": "Artifact Title",
      "description": "What this code does",
      "code": "def hello():\n    return 'Hello'",
      "language": "python",
      "tags": ["utility"],
      "created_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 10
}
GET /api/v1/code-artifacts/{id}

Get a single code artifact by ID.

POST /api/v1/code-artifacts

Create a new code artifact.

Request Body:

{
  "title": "Artifact Title",
  "description": "What this code does",
  "code": "def hello():\n    return 'Hello'",
  "language": "python",
  "tags": ["utility"]
}
PUT /api/v1/code-artifacts/{id}

Update an existing code artifact.

DELETE /api/v1/code-artifacts/{id}

Delete a code artifact.


Skills

Requires: SKILLS_ENABLED=true

GET /api/v1/skills

List all skills.

Query Parameters:

ParameterTypeDescription
project_idintFilter by project
tagsstringComma-separated tags (OR logic)
importance_thresholdintMinimum importance level

Response:

{
  "skills": [
    {
      "id": 1,
      "name": "code-review",
      "description": "Systematic code review process",
      "license": "MIT",
      "tags": ["development", "review"],
      "importance": 8,
      "project_id": null,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}

Semantic search for skills by description similarity.

Query Parameters:

ParameterTypeDefaultDescription
querystring(required)Search query
kint5Number of results
project_idint-Filter by project
GET /api/v1/skills/{id}

Get a single skill by ID (includes full content).

POST /api/v1/skills

Create a new skill.

Request Body:

{
  "name": "code-review",
  "description": "Systematic code review process",
  "content": "# Code Review\n\n## Steps\n1. Check for...",
  "license": "MIT",
  "compatibility": "Requires Python 3.12+",
  "allowed_tools": ["Read", "Grep"],
  "metadata": {"author": "team"},
  "tags": ["development", "review"],
  "importance": 8,
  "project_id": 1
}
PUT /api/v1/skills/{id}

Update an existing skill (PATCH semantics).

DELETE /api/v1/skills/{id}

Delete a skill.

POST /api/v1/skills/import

Import a skill from Agent Skills markdown format.

Request Body:

{
  "skill_md": "---\nname: code-review\ndescription: ...\n---\n\n# Content...",
  "project_id": 1,
  "importance": 8
}
GET /api/v1/skills/{id}/export

Export a skill to Agent Skills markdown format (SKILL.md).

Response:

{
  "skill_md": "---\nname: code-review\ndescription: ...\n---\n\n# Content..."
}

Graph

GET /api/v1/graph

Get full knowledge graph for visualization. Returns all nodes and edges for the user.

Query Parameters:

ParameterTypeDefaultDescription
node_typesstringmemory,entity,project,document,code_artifactComma-separated list of node types to include
project_idint-Filter to specific project
include_entitiesbooltrueInclude entity nodes (deprecated, use node_types)
limitint100Max memories to include (max 500)
offsetint0Number of memories to skip for pagination
sort_bystringcreated_atSort field: created_at, updated_at, or importance
sort_orderstringdescSort direction: asc or desc

Node Types:

  • memory - Knowledge memories

  • entity - People, organizations, devices

  • project - Project contexts

  • document - Long-form documents

  • code_artifact - Code snippets

Edge Types:

  • memory_link - Memory-to-memory connections

  • entity_memory - Entity linked to memory

  • entity_relationship - Entity-to-entity relationship

  • memory_project - Memory linked to project

  • document_project - Document belongs to project

  • code_artifact_project - Code artifact belongs to project

  • memory_document - Memory linked to document

  • memory_code_artifact - Memory linked to code artifact

Response:

{
  "nodes": [
    {
      "id": "memory_1",
      "type": "memory",
      "label": "Memory Title",
      "data": {
        "id": 1,
        "title": "Memory Title",
        "importance": 7,
        "tags": ["tag1"],
        "created_at": "2024-12-05T10:00:00Z"
      }
    },
    {
      "id": "entity_1",
      "type": "entity",
      "label": "Entity Name",
      "data": {
        "id": 1,
        "name": "Entity Name",
        "entity_type": "Individual"
      }
    },
    {
      "id": "project_1",
      "type": "project",
      "label": "Project Name",
      "data": {
        "id": 1,
        "name": "Project Name",
        "project_type": "development",
        "status": "active"
      }
    }
  ],
  "edges": [
    {
      "id": "memory_1_memory_2",
      "source": "memory_1",
      "target": "memory_2",
      "type": "memory_link"
    },
    {
      "id": "memory_1_project_1",
      "source": "memory_1",
      "target": "project_1",
      "type": "memory_project"
    }
  ],
  "meta": {
    "memory_count": 50,
    "total_memory_count": 1243,
    "offset": 0,
    "limit": 100,
    "has_more": true,
    "entity_count": 10,
    "project_count": 3,
    "document_count": 5,
    "code_artifact_count": 8,
    "edge_count": 75,
    "memory_link_count": 25,
    "entity_relationship_count": 5,
    "entity_memory_count": 15,
    "memory_project_count": 12,
    "document_project_count": 5,
    "code_artifact_project_count": 8,
    "memory_document_count": 3,
    "memory_code_artifact_count": 2
  }
}

Pagination Examples:

# Get first 50 memories sorted by importance
GET /api/v1/graph?limit=50&sort_by=importance&sort_order=desc

# Get next page of memories
GET /api/v1/graph?limit=50&offset=50&sort_by=importance&sort_order=desc

# Filter to project and sort by creation date (oldest first)
GET /api/v1/graph?project_id=4&sort_by=created_at&sort_order=asc
GET /api/v1/graph/subgraph

Get subgraph centered on a specific node using efficient CTE traversal. This is the recommended endpoint for graph visualization with depth-limited traversal.

Query Parameters:

ParameterTypeDefaultDescription
node_idstringrequiredCenter node (e.g., memory_1, entity_5, project_3, document_2, code_artifact_4)
depthint2Traversal depth (1-3, clamped)
node_typesstringmemory,entity,project,document,code_artifactComma-separated list of types to traverse
max_nodesint200Maximum nodes to return (max 500)

Response:

{
  "nodes": [
    {
      "id": "memory_1",
      "type": "memory",
      "depth": 0,
      "label": "Center Memory",
      "data": {...}
    },
    {
      "id": "memory_2",
      "type": "memory",
      "depth": 1,
      "label": "Linked Memory",
      "data": {...}
    }
  ],
  "edges": [...],
  "meta": {
    "center_node_id": "memory_1",
    "depth": 2,
    "node_types": ["memory", "entity"],
    "max_nodes": 200,
    "memory_count": 5,
    "entity_count": 2,
    "project_count": 0,
    "document_count": 0,
    "code_artifact_count": 0,
    "edge_count": 6,
    "memory_link_count": 3,
    "entity_relationship_count": 1,
    "entity_memory_count": 2,
    "memory_project_count": 0,
    "document_project_count": 0,
    "code_artifact_project_count": 0,
    "memory_document_count": 0,
    "memory_code_artifact_count": 0,
    "truncated": false
  }
}
GET /api/v1/graph/memory/{id}

Deprecated: Use /api/v1/graph/subgraph?node_id=memory_{id} instead.

Get subgraph centered on a specific memory.

Query Parameters:

ParameterTypeDefaultDescription
depthint1Link traversal depth (1-3)

Response:

{
  "nodes": [...],
  "edges": [...],
  "center_memory_id": 1,
  "meta": {
    "memory_count": 5,
    "edge_count": 4,
    "depth": 1
  }
}

Activity

Experimental Feature

Activity logging is an experimental feature. The async event-driven architecture may cause issues with SQLite backends due to connection pooling conflicts. If you are using SQLite and do not intend to use activity tracking, leave it disabled by not configuring the activity-related settings. PostgreSQL users can enable this feature but please be advised it is still experimental

Known limitations:

  • SQLite in-memory mode (testing): Events may conflict with concurrent database operations

The Activity API provides read access to the activity log, which tracks all entity lifecycle events (created, updated, deleted) and optionally read/query operations.

Configuration

Activity tracking is controlled by these settings:

SettingTypeDefaultDescription
ACTIVITY_RETENTION_DAYSintNoneDays to keep events (None = forever). Cleanup happens lazily on API access.
ACTIVITY_TRACK_READSboolfalseTrack read/query operations (opt-in, can be high volume)
GET /api/v1/activity

List activity events with filtering and pagination.

Query Parameters:

ParameterTypeDefaultDescription
entity_typestring-Filter by type: memory, project, document, code_artifact, entity, link
actionstring-Filter by action: created, updated, deleted, read, queried
entity_idint-Filter by specific entity ID
actorstring-Filter by actor: user, system, llm-maintenance
sincedatetime-Only events after this timestamp (ISO 8601)
untildatetime-Only events before this timestamp (ISO 8601)
limitint50Results per page (1-100)
offsetint0Skip N results

Response:

{
  "events": [
    {
      "id": 123,
      "entity_type": "memory",
      "entity_id": 1,
      "action": "updated",
      "changes": {
        "title": {"old": "Old Title", "new": "New Title"},
        "importance": {"old": 5, "new": 8}
      },
      "snapshot": {
        "id": 1,
        "title": "New Title",
        "content": "...",
        "importance": 8
      },
      "actor": "user",
      "actor_id": null,
      "metadata": null,
      "created_at": "2026-01-06T12:00:00Z"
    }
  ],
  "total": 42,
  "limit": 50,
  "offset": 0
}
GET /api/v1/activity/{entity_type}/{entity_id}

Get activity history for a specific entity.

Path Parameters:

ParameterTypeDescription
entity_typestringEntity type: memory, project, document, code_artifact, entity
entity_idintEntity ID

Query Parameters:

ParameterTypeDefaultDescription
limitint50Results per page (1-100)
offsetint0Skip N results

Response: Same format as GET /api/v1/activity

Event Types

Entity Types:

  • memory - Knowledge memories

  • project - Project containers

  • document - Long-form documents

  • code_artifact - Code snippets

  • entity - People, organizations, devices

  • link - Memory-to-memory connections

  • entity_memory_link - Entity-to-memory connections

  • entity_project_link - Entity-to-project connections

  • entity_relationship - Entity-to-entity relationships

Action Types:

  • created - Entity was created

  • updated - Entity was modified (includes changes diff)

  • deleted - Entity was soft-deleted (obsolete)

  • read - Entity was read (if ACTIVITY_TRACK_READS=true)

  • queried - Search was performed (if ACTIVITY_TRACK_READS=true)

Actor Types:

  • user - Human user action

  • system - Automated system action

  • llm-maintenance - LLM-based maintenance task (future)

Changes Format

For updated events, the changes field contains a diff:

{
  "changes": {
    "field_name": {
      "old": "previous value",
      "new": "current value"
    }
  }
}

Only modified fields are included. The snapshot field contains the full entity state after the change.

Link events (entity_type: "link") use entity_id: 0 and store source/target in metadata:

{
  "entity_type": "link",
  "entity_id": 0,
  "action": "created",
  "snapshot": {"source_id": 1, "target_id": 2},
  "metadata": {"source_id": 1, "target_id": 2}
}
GET /api/v1/activity/stream

Stream activity events in real-time via Server-Sent Events (SSE).

Events are filtered to only those belonging to the authenticated user. Each event includes a sequence number for gap detection and client recovery.

Query Parameters:

ParameterTypeDescription
entity_typestringFilter by type (optional)
actionstringFilter by action (optional)

Response: SSE stream with text/event-stream content type.

Event Format:

event: activity
data: {"seq": 1, "entity_type": "memory", "action": "created", "entity_id": 42, ...}

event: activity
data: {"seq": 2, "entity_type": "memory", "action": "updated", "entity_id": 42, ...}

Sequence Numbers:

Each event includes a monotonically increasing seq field per user. Clients should track this to detect gaps (e.g., receiving seq 45 when last seen was 42 indicates missed events).

Gap Recovery:

On gap detection, fetch missed events via the REST API:

GET /api/v1/activity?since=<last_seen_timestamp>&limit=100

Configuration:

SettingTypeDefaultDescription
SSE_MAX_QUEUE_SIZEint1000Max events per subscriber queue (backpressure)

When the queue is full, new events are dropped with a warning log. Clients can detect this via sequence gaps and resync via REST.

Example Usage (JavaScript):

const events = new EventSource('/api/v1/activity/stream');
let lastSeq = 0;

events.addEventListener('activity', (e) => {
  const event = JSON.parse(e.data);

  // Gap detection
  if (lastSeq > 0 && event.seq > lastSeq + 1) {
    console.warn(`Gap detected: ${lastSeq} -> ${event.seq}`);
    // Fetch missed events via REST API
  }

  lastSeq = event.seq;
  handleEvent(event);
});

Error Responses:

CodeDescription
400Invalid filter parameter
401Unauthorized
503Activity streaming not enabled (ACTIVITY_ENABLED=false)

Plans

GET /api/v1/plans

List plans with optional filtering.

Query Parameters:

ParameterTypeDescription
project_idintFilter by project
external_refstringExact, user-scoped reference lookup
statusstringFilter by status: draft, active, completed, archived

Response:

{
  "plans": [
    {
      "id": 1,
      "title": "Plan Title",
      "project_id": 1,
      "status": "draft",
      "external_ref": "github:ScottRBK/factory#42",
      "task_count": 3,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}
GET /api/v1/plans/{plan_id}

Get a single plan by ID.

Response:

{
  "id": 1,
  "title": "Plan Title",
  "project_id": 1,
  "goal": "Implement feature X",
  "context": "Background context for the plan",
  "status": "draft",
  "external_ref": "github:ScottRBK/factory#42",
  "task_count": 3,
  "created_at": "2024-12-05T10:00:00Z",
  "updated_at": "2024-12-05T10:00:00Z"
}
POST /api/v1/plans

Create a new plan.

Request Body:

{
  "title": "Plan Title",
  "project_id": 1,
  "goal": "Optional goal description",
  "context": "Optional background context",
  "status": "draft"
}
FieldTypeRequiredDescription
titlestringyesPlan title
project_idintyesParent project ID
goalstringnoGoal or objective
contextstringnoBackground context
external_refstringnoOpaque reference unique across your plans; default null
statusstringnoInitial status (default: draft). Values: draft, active, completed, archived

Response (201): Full plan object (see GET response above)

Reference rules: external_ref is trimmed, case-sensitive, and must contain 1–255 characters when supplied. Multiple null references are allowed. Uniqueness covers every project and status owned by the authenticated user; other users may reuse the same value. source_url remains independent provenance, and existing plans receive no automatic reference backfill.

Errors: 400 for invalid references; 404 when project_id is not one of your projects; 409 when a reference already belongs to one of your plans. Concurrent duplicate requests cannot both succeed. After a conflict, look up the reference alone, without project or status filters, to include completed and archived plans. URL-encode the query value (especially #, which otherwise starts a URL fragment):

GET /api/v1/plans?external_ref=github%3AScottRBK%2Ffactory%2342

The response contains zero or one summary, including external_ref. Blank or overlength lookup values return 400. Additional filters are combined with the reference filter.

PUT /api/v1/plans/{plan_id}

Update an existing plan.

Request Body: (all fields optional)

{
  "title": "Updated Title",
  "goal": "Updated goal",
  "context": "Updated context",
  "status": "active"
}
FieldTypeDescription
titlestringUpdated title
goalstringUpdated goal
contextstringUpdated context
external_refstringReplacement reference; omitted or null leaves it unchanged
statusstringNew status (must be a valid transition)

References cannot be cleared: empty or whitespace-only values are invalid. Replacing a reference or deleting its plan releases the old value. Completing or archiving a plan retains it. Duplicate updates return 409 and leave the whole plan unchanged.

Plan Status Transitions:

FromAllowed To
draftactive, archived
activecompleted, archived
completedactive (reopen)
archivedactive (unarchive)

Response: Updated plan object

Error Responses:

CodeCondition
400Invalid reference
404Plan not found
409External reference already used by one of your plans
422Invalid status transition
DELETE /api/v1/plans/{plan_id}

Delete a plan. Cascades to all tasks, criteria, and dependencies under the plan.

Response:

{
  "success": true
}

Tasks

GET /api/v1/tasks

Query tasks for a plan. The plan_id query parameter is required.

Query Parameters:

ParameterTypeRequiredDescription
plan_idintyesFilter by plan
statestringnoFilter by state: todo, doing, waiting, done, cancelled
prioritystringnoFilter by priority: P0, P1, P2, P3
assigned_agentstringnoFilter by assigned agent

Response:

{
  "tasks": [
    {
      "id": 1,
      "title": "Task Title",
      "plan_id": 1,
      "state": "todo",
      "priority": "P2",
      "assigned_agent": null,
      "version": 1,
      "criteria_met": 0,
      "criteria_total": 2,
      "blocked": false,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 10
}
GET /api/v1/tasks/{task_id}

Get a single task with its criteria and dependency IDs.

Response:

{
  "id": 1,
  "plan_id": 1,
  "title": "Task Title",
  "description": "Detailed description",
  "state": "todo",
  "priority": "P2",
  "assigned_agent": null,
  "version": 1,
  "criteria": [
    {
      "id": 1,
      "task_id": 1,
      "description": "Acceptance criterion",
      "met": false,
      "met_at": null,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "dependency_ids": [2, 3],
  "created_at": "2024-12-05T10:00:00Z",
  "updated_at": "2024-12-05T10:00:00Z"
}
POST /api/v1/tasks

Create a new task with optional inline criteria and dependencies.

Request Body:

{
  "title": "Task Title",
  "plan_id": 1,
  "description": "Optional description",
  "priority": "P2",
  "assigned_agent": "agent-1",
  "criteria": [
    {"description": "Criterion 1"},
    {"description": "Criterion 2"}
  ],
  "dependency_ids": [2, 3]
}
FieldTypeRequiredDescription
titlestringyesTask title
plan_idintyesParent plan ID
descriptionstringnoDetailed description
prioritystringnoPriority level (default: P2). Values: P0 (critical), P1 (high), P2 (medium), P3 (low)
assigned_agentstringnoAgent assigned to the task
criteriaarraynoInline acceptance criteria to create
dependency_idsarraynoIDs of tasks this task depends on

Response (201): Full task object (see GET response above)

Error Responses:

CodeCondition
400Cyclic dependency detected
404Plan or dependency task not found
422Invalid state transition
PUT /api/v1/tasks/{task_id}

Update task metadata. State changes must go through the transition endpoint.

Request Body: (all fields optional)

{
  "title": "Updated Title",
  "description": "Updated description",
  "priority": "P1"
}
FieldTypeDescription
titlestringUpdated title
descriptionstringUpdated description
prioritystringUpdated priority: P0, P1, P2, P3

Response: Updated task object

DELETE /api/v1/tasks/{task_id}

Delete a task.

Response:

{
  "success": true
}
POST /api/v1/tasks/{task_id}/transition

Transition a task to a new state. Uses optimistic concurrency via the version field -- the request is rejected if the provided version does not match the current version.

Send a JSON object with the documented field types. Versions must be JSON integers; booleans, fractional or integral floats, and numeric strings are rejected rather than coerced.

Request Body:

{
  "state": "doing",
  "version": 1
}
FieldTypeRequiredDescription
statestringyesTarget state: todo, doing, waiting, done, cancelled
versionintyesCurrent version for optimistic concurrency

Task State Transitions:

FromAllowed To
tododoing, waiting, cancelled
doingdone, waiting, todo, cancelled
waitingtodo, doing, cancelled
donetodo (reopen)
cancelledtodo (reinstate)

Response: Updated task object (version is incremented on success)

Error Responses:

CodeCondition
400Malformed JSON, non-object body, or missing/invalid fields
404Task not found
409Version mismatch (optimistic concurrency conflict)
422Invalid state transition, or unmet dependencies block the transition
POST /api/v1/tasks/{task_id}/claim

Claim a task by assigning an agent and transitioning it to doing. Uses optimistic concurrency via the version field.

Send a JSON object with a string agent_id and a JSON integer version. Other values are rejected rather than converted to an identifier or version.

Request Body:

{
  "agent_id": "agent-1",
  "version": 1
}
FieldTypeRequiredDescription
agent_idstringyesIdentifier of the claiming agent
versionintyesCurrent version for optimistic concurrency

Response: Updated task object (state set to doing, assigned_agent set, version incremented)

Error Responses:

CodeCondition
400Malformed JSON, non-object body, or missing/invalid fields
404Task not found
409Version mismatch (optimistic concurrency conflict)
422Invalid state transition, or unmet dependencies block the claim

Criteria

POST /api/v1/tasks/{task_id}/criteria

Add an acceptance criterion to a task.

Request Body:

{
  "description": "Acceptance criterion description"
}
FieldTypeRequiredDescription
descriptionstringyesCriterion description

Response (201):

{
  "id": 1,
  "task_id": 1,
  "description": "Acceptance criterion description",
  "met": false,
  "met_at": null,
  "created_at": "2024-12-05T10:00:00Z",
  "updated_at": "2024-12-05T10:00:00Z"
}

Error Responses:

CodeCondition
404Task not found
422Task is in a state that does not allow adding criteria
PUT /api/v1/criteria/{criterion_id}

Update a criterion (e.g., mark it as met).

Request Body: (all fields optional)

{
  "description": "Updated description",
  "met": true
}
FieldTypeDescription
descriptionstringUpdated description
metboolWhether the criterion has been met

Response: Updated criterion object

DELETE /api/v1/criteria/{criterion_id}

Delete an acceptance criterion.

Response:

{
  "success": true
}

Dependencies

POST /api/v1/tasks/{task_id}/dependencies

Add a dependency, declaring that task_id depends on another task.

Send a JSON object with a JSON integer depends_on_task_id. Booleans, floats, and numeric strings are rejected rather than converted to a different task ID.

Request Body:

{
  "depends_on_task_id": 2
}
FieldTypeRequiredDescription
depends_on_task_idintyesID of the task that must complete first

Response (201):

{
  "id": 1,
  "task_id": 1,
  "depends_on_task_id": 2,
  "created_at": "2024-12-05T10:00:00Z"
}

Error Responses:

CodeCondition
400Malformed JSON, non-object body, missing/invalid fields, cyclic dependency, or task depends on itself
404Task not found
DELETE /api/v1/tasks/{task_id}/dependencies/{depends_on_task_id}

Remove a dependency between two tasks.

Response:

{
  "success": true
}

Error Responses

All error responses follow this format:

{
  "error": "Error message describing what went wrong"
}

For validation errors (400), the error may contain detailed field-level errors:

{
  "error": [
    {
      "loc": ["body", "title"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}
Plan write policy and secondary task outcomes

An administrator may configure the Plan Plugin. Plan create/update/delete and task transition/delete retain their existing flat successful payloads and add plugin_warnings only when nonempty. Plan create remains 201; updates and deletions remain 200. Direct policy rejection returns 400, unavailable policy 503, and an incompatible plan-state change during a before-handler await 409. Existing input validation 400, missing 404, conflicts 409 and invalid state transition 422 remain distinct.

A task can already be committed when automatic parent completion is rejected or unavailable. Its successful response then includes plan_auto_completion_rejected or plan_auto_completion_unavailable; do not retry the task as if it failed. Warn mode can complete the parent with a warning. Other post-commit failures and genuine cancellation retain existing propagation; inspect persisted state before retrying. Read/list results and stored state contain no transient warnings. Plan/task deletion stays REST-only; database cascades do not dispatch per-row hooks.

472 files · 452 not served here

Advantages

  • Automatic knowledge graph construction through semantic linking of atomic memories. evidence: README.md
  • Maintains context window efficiency via configurable token budgets and importance-based ranking. evidence: README.md
  • Flexible deployment options ranging from zero-config local SQLite to scalable PostgreSQL. evidence: README.md

Limitations

  • Requires adherence to the Zettelkasten principle, which may be restrictive for some workflows. evidence: README.md
  • Limits the number of tools exposed to the agent to preserve context window, potentially hiding advanced functionality. evidence: README.md

Security

Risk level: Medium risk

This brain runs

Agent instructions that run or fetch something (34)

  • AGENTS.md: Run `uv run pytest tests/architecture/` to check and prove the agreed import boundaries.
  • AGENTS.md: **Location**: `tests/integration/` **Purpose**: Test business logic with stubbed I/O (no real database required) **Run locally**:
  • AGENTS.md: uv run pytest tests/integration/
  • AGENTS.md: These tests use in-memory stubs and run fast (~seconds).
  • AGENTS.md: **Location**: `tests/e2e_sqlite/` **Purpose**: Test complete stack with in-memory SQLite **Requirements**: None (no Docker required) **Run locally**:
  • AGENTS.md: uv run pytest tests/e2e_sqlite/
  • AGENTS.md: **Location**: `tests/e2e/` **Purpose**: Test complete stack with real PostgreSQL **Requirements**: PostgreSQL running in Docker **Run locally**:
  • AGENTS.md: uv run pytest -m e2e
  • AGENTS.md: Ensure that you run ruff following any changes and address any issues raised
  • AGENTS.md: uv tool run ruff check .
  • skills/forgetful-cli-setup/SKILL.md: The CLI is the shell-native surface: same tool registry as MCP, reachable by anything that can run a command.
  • skills/forgetful-cli-setup/SKILL.md: After changing embedding provider or model: `forgetful re-embed --dry-run` to preview, then `forgetful re-embed` (batch size tunable via `--batch-size`).
  • skills/forgetful-context-gather/SKILL.md: Run MCP-flavored gathers in the main conversation.
  • skills/forgetful-context-gather/SKILL.md: Done when: every angle has run and its outcome is recorded.
  • skills/forgetful-encode-repo/SKILL.md: this skill moves it, and only after a run has reviewed every change up to the new commit.
  • skills/forgetful-encode-repo/SKILL.md: Run git in the repository being encoded (the current directory unless the user named one).
  • skills/forgetful-encode-repo/SKILL.md: **HEAD**: `git rev-parse HEAD`.
  • skills/forgetful-encode-repo/SKILL.md: Unless the user names another range end, this is the end commit: everything this run writes describes it.
  • skills/forgetful-encode-repo/SKILL.md: **Clean worktree**: `git status --porcelain` prints nothing.
  • skills/forgetful-encode-repo/SKILL.md: **Project**: `git remote get-url origin` → `owner/repo` → `list_projects` with `repo_name`.
  • skills/forgetful-encode-repo/SKILL.md: Checks: the checkpoint is a commit here when `git rev-parse --verify --quiet <sha>^{commit}` prints the stored value itself;
  • skills/forgetful-encode-repo/SKILL.md: `git merge-base --is-ancestor <sha> <end>` succeeds when it is an ancestor.
  • skills/forgetful-encode-repo/SKILL.md: Done when: the branch decision is written, and each value in it comes from a command or tool result in this run.
  • skills/forgetful-encode-repo/SKILL.md: **Retrieval**: when the run wrote knowledge, `query_memory` scoped to the project with `strict_project_filter` true and the questions this encoding should answer (how the system fits together, each changed area under its current name).
  • skills/forgetful-encode-repo/SKILL.md: **Pinning**: `git rev-parse HEAD` still matches Step 1, `git status --porcelain` still prints nothing, and `get_project` still shows the checkpoint read in Step 1.
  • skills/forgetful-encode-repo/SKILL.md: Advance the checkpoint only when the run recorded no gaps and the range leaves no unreviewed commit (see user ranges in Step 2).
  • skills/forgetful-encode-repo/SKILL.md: Build it from tool results in this run: an ID or count appears only when a response confirmed it, and a read that failed is reported as unknown.
  • skills/forgetful-encode-repo/SKILL.md: Correct the call against the operation's schema and run it again;
  • skills/forgetful-mcp-setup/SKILL.md: **stdio** — the client spawns the server per session: command `uvx forgetful-ai` (equivalently `forgetful serve --transport stdio`).
  • skills/forgetful-mcp-setup/SKILL.md: Recognised scopes in verified token `scope`/`scp` claims can narrow MCP access further; · uses the network
  • skills/forgetful-mcp-setup/SKILL.md: Run Forgetful MCP calls in the main conversation — subagents do not inherit MCP tool access.
  • skills/forgetful-procedures/SKILL.md: All skill operations run through the standard invocation surface below, the same as every other domain.
  • skills/forgetful-recall/SKILL.md: Run it as a deliberate step, then continue into normal recall as the task demands.
  • skills/forgetful-remember/SKILL.md: Derive the candidate from the repo: `git remote get-url origin` → `owner/repo` → `list_projects` with `repo_name`.

MCP servers (2)

  • README.md (forgetful): uvx forgetful-ai
  • README.md (forgetful): http://localhost:8020/mcp · uses the network

Install scripts (1)

  • app/bootstrap.py: python app/bootstrap.py

Package scripts (1)

  • pyproject.toml (build-hook): hatch build hook vcs (hatch_build.py)
  • Medium risk: runs-mcp-server-unpinned-runner (README.md:159)
  • Medium risk: runs-instruction-instruction-network (skills/forgetful-mcp-setup/SKILL.md:51)
  • Low risk: scripts-present (app/bootstrap.py)
  • Low risk: network-instructions (skills/forgetful-cli-setup/SKILL.md:25)

Attribution and licence

Created by
Scott
Declared authors
  • Scott (declared in pyproject.toml)
Publisher
Scott Raisbeck (Community)
Source
ScottRBK/forgetful · commit 9f85ddc
Licence
MIT License (files served here)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Scott.

Install from source

452 of its files are not served here, so there is no ZIP. ScottRBK/forgetful at 9f85ddc