Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ jobs:
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
# Two-step install: generate a lockfile in-runner with
# --package-lock-only, then install from it with `npm ci`.
# Lockfiles are gitignored at the repo level.
- run: npm install --package-lock-only --legacy-peer-deps --no-audit --no-fund
- run: npm ci --legacy-peer-deps --no-audit --no-fund
# Lockfiles are gitignored, so `npm ci` (which strictly re-validates a
# committed lockfile) buys no reproducibility here — and Node 24+'s
# stricter npm rejects rolldown's optional platform bindings that a
# `--package-lock-only` pass doesn't fully enumerate, failing the matrix
# on 24/26 only. A single lenient `npm install` resolves and installs
# in one pass.
- run: npm install --legacy-peer-deps --no-audit --no-fund
- run: npm run build
- run: npm run skills:check
- run: npm test
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ Hook scripts in `src/hooks/` are standalone Node.js scripts (no iii-sdk import).
## Current Stats (v0.9.28)

- 54 MCP tools (8 visible by default, `AGENTMEMORY_TOOLS=all` for all)
- 129 REST endpoints
- 130 REST endpoints
- 6 MCP resources, 3 MCP prompts
- 12 hooks, 15 skills
- 260+ iii functions
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,50 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

## [0.9.29] — 2026-08-02

Patch release: the `.env` file now actually applies everywhere, imports become searchable, consolidation runs on session stop, twelve MCP-only agents get activated on connect, and every capture surface finally agrees on what "project" means. No breaking changes; read the upgrade notes below for four behavior changes you will notice.

### Upgrade notes

- `~/.agentmemory/.env` values that were silently ignored by most modules now take effect on boot. If that file has stale entries from past experiments, review it before upgrading.
- `agentmemory connect <agent>` now writes a short memory-usage guideline into the agent's native rules file (Cursor, Cline, Continue, Zed, Warp, Kiro, Gemini CLI, Qwen, OpenCode, Droid, Copilot CLI, Antigravity) so MCP-only agents actually call the memory tools. Pass `--no-guidelines` to opt out.
- Installs with an LLM key now run consolidation and crystallization on session stop (previously they never fired), debounced to once per 5 minutes (`AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS`).
- Local embeddings re-download once after the `@huggingface/transformers` migration (different model cache directory). Model IDs are unchanged.

### Added

- `--data-dir` flag and `AGENTMEMORY_DATA_DIR` so iii-engine state lives outside repositories, with gated legacy `./data` adoption and Docker-volume preservation (#314)
- Native hooks adapter for Droid via `~/.factory/hooks.json`, reusing the bundled hook scripts (#1130)
- `mem::graph::import-graphify` and `POST /agentmemory/graph/import-graphify`: merge graphify's `graph.json` into the knowledge graph with confidence tags carried over as edge weights (#1136)
- Connector guideline activation for twelve hook-less agents, with every rules-file path verified against the agent's official documentation (#1136)
- Honest `memory_forget` reporting plus a real lesson delete path (`mem::lesson-delete`, `DELETE`-style REST route, MCP tool) (#1132)
- `AGENTMEMORY_PROJECT_NAME` override in the OpenCode plugin (#1125)
- Provider fetches retry 429/503 honoring `Retry-After` under a total-elapsed budget capped below the iii invocation timeout (#1136)

### Fixed

- Boot hydrates `~/.agentmemory/.env` into `process.env`, closing the class of "env var in .env is ignored" bugs (#1136)
- Imported and replayed observations are indexed into BM25 and the vector index, so imports are searchable (#1072, via #1136)
- Snapshot timer actually runs, non-positive intervals clamp to the default, and snapshot creation is serialized across timer, REST, and MCP (#1006, via #1136)
- CJK-aware dedup with NFC normalization and an exact-match fallback for short memories (#1021, via #1136)
- OpenRouter embeddings no longer hardcode 1536 dimensions (#1002, via #1136)
- Viewer decodes multibyte request bodies correctly (#930, via #1136)
- Session-stop consolidation is debounced and no longer double-fires from the client hook; eviction recovery is bounded to one consolidation pass (#1087, #1131 class, via #1136)
- `/agentmemory/sessions` no longer deadlocks on large session counts (#1100, via #1136)
- Filesystem watcher validates roots before `fs.watch`, fixing Node 24/26 on Linux (#1136)
- `GET /agentmemory/export` and `/agentmemory/mesh/export` refuse an over-frame response instead of shipping it: a payload past the engine 16 MiB transport frame used to drop the worker and 404 every endpoint for ~1s. They now fail that one request (413 for mesh, an `oversized` error for export) with a hint to narrow the range, keeping the daemon up (#1142, #890). Full pagination of the non-session collections is a follow-up.
- Claude bridge writes `MEMORY.md` under the `memory/` subdirectory Claude Code actually reads (#1134)
- Hook project-resolution tests no longer depend on the checkout directory name (#1137, #1138)
- Project-scope parity: the OpenCode plugin, Hermes plugin, Pi extension, and JSONL replay now resolve `project` the same way the hooks do (env override, git toplevel basename, cwd basename) instead of sending raw filesystem paths, so the same repository shares one memory bucket across agents (#903, #1135); the filesystem watcher accepts `AGENTMEMORY_PROJECT_NAME` with the old `AGENTMEMORY_PROJECT` kept as a deprecated alias; replay handles Windows-recorded paths
- OpenCode file enrichment matches the agent's lowercase tool names, which the previous capitalized set never did
- Viewer surfaces health status from non-2xx health responses (#1046)
- Documented REST endpoint count matches the registered routes again (130)

### Changed

- Local embeddings migrate from `@xenova/transformers` to `@huggingface/transformers` v4 with Node 22+ support; CI now tests Node 20, 22, 24, and 26 (#479, #1096)

## [0.9.28] — 2026-07-19

Patch release: hardens the hook runner against malformed payloads and closes a cross-agent context leak. No breaking changes; drop-in upgrade.
Expand Down
23 changes: 13 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,11 @@ PRs with commits lacking sign-off will not merge.
| `src/mcp/` | Standalone MCP server (`@agentmemory/mcp`), tools registry, transport, in-memory KV. |
| `src/functions/` | Core memory operations — observe, compress, consolidate, retention, forget, graph, smart-search, export-import, governance. |
| `src/hooks/` | The 12 auto-hooks that capture sessions in agents. |
| `src/cli/` | The `agentmemory` CLI, including `connect/` adapters for 18 agents and the guideline writer for hook-less agents. |
| `src/health/` | Liveness + readiness + alert thresholds. |
| `src/state/` | KV schema, keyed mutex, access log. |
| `integrations/` | First-party plugins: `hermes/`, `openclaw/`, `filesystem-watcher/`. |
| `plugin/` | Claude Code plugin (`agentmemory@agentmemory`). |
| `integrations/` | First-party plugins: `hermes/`, `openclaw/`, `pi/`, `filesystem-watcher/`. |
| `plugin/` | Agent plugin bundle: Claude Code plugin, hook manifests for Codex/Copilot/Droid, the OpenCode capture plugin, and the skills. Hook manifests and skill REFERENCE files are partly generated; run `npm run skills:gen` after touching registered endpoints or env vars. |
| `website/` | Marketing site (Next.js 16). |
| `test/` | Vitest test suite. |

Expand All @@ -92,18 +93,20 @@ PRs with commits lacking sign-off will not merge.

## Release process

Maintainers cut releases. Every bump touches 8 files in lockstep:
Maintainers cut releases. Every bump touches these files in lockstep (the consistency tests fail if the trio of doc counts or any version drifts):

1. `package.json`
2. `package-lock.json` (top + `packages[""].version`)
2. `src/version.ts`
3. `plugin/.claude-plugin/plugin.json`
4. `packages/mcp/package.json` (self + `~x.y.z` pin on the main package)
5. `src/version.ts` (extend the union, assign)
6. `src/types.ts` (`ExportData.version` union)
7. `src/functions/export-import.ts` (`supportedVersions` Set)
8. `test/export-import.test.ts` (assertion)
4. `plugin/plugin.json`
5. `plugin/.codex-plugin/plugin.json`
6. `packages/mcp/package.json`
7. `src/types.ts` (`ExportData.version` union)
8. `src/functions/export-import.ts` (`supportedVersions` Set)

Then: CHANGELOG section, PR, merge, tag, GitHub release. The `Publish to npm` workflow picks up the release trigger and publishes `@agentmemory/agentmemory`, `@agentmemory/mcp`, and `@agentmemory/fs-watcher` to npm with provenance.
No lockfiles are committed. `test/export-import.test.ts` asserts against the `VERSION` constant, so it needs no per-release edit. Run `npm run skills:gen` if the endpoint or env surface changed.

Then: CHANGELOG section, PR, merge, tag, GitHub release. The `Publish to npm` workflow picks up the release trigger and publishes `@agentmemory/agentmemory`, `@agentmemory/mcp`, and `@agentmemory/fs-watcher` to npm with provenance (`@agentmemory/fs-watcher` versions independently from `integrations/filesystem-watcher/package.json`).

## Security issues

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1498,7 +1498,7 @@ Create `~/.agentmemory/.env`:

<h2 id="api"><picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/section-api.svg"><img src="assets/tags/section-api.svg" alt="API" height="32" /></picture></h2>

129 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer <secret>` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
130 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer <secret>` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.

<details>
<summary>Key endpoints</summary>
Expand Down
4 changes: 2 additions & 2 deletions assets/tags/light/stat-tests.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions assets/tags/stat-tests.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 33 additions & 3 deletions integrations/filesystem-watcher/watcher.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { watch, promises as fsp, statSync } from "node:fs";
import { resolve, relative, join, extname, sep, basename } from "node:path";
import { randomBytes } from "node:crypto";
import { execFileSync } from "node:child_process";

// Same resolution order as the hooks' resolveProject (git toplevel basename,
// then directory basename) so a watched subdirectory scopes to the repository
// name instead of the subdirectory name.
function deriveProjectName(dir) {
try {
const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
cwd: dir,
stdio: ["ignore", "pipe", "ignore"],
encoding: "utf8",
}).trim();
if (top) return basename(top);
} catch {
// not a git repo
}
return basename(dir);
}

const TEXT_EXTENSIONS = new Set([
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
Expand Down Expand Up @@ -123,7 +141,13 @@ export class FilesystemWatcher {
this.secret = config.secret;
this.project =
config.project ||
(this.roots[0] ? basename(this.roots[0]) : "filesystem-watcher");
(this.roots[0] ? deriveProjectName(this.roots[0]) : "filesystem-watcher");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Per-root scope: a multi-root watcher must stamp each event with the
// project of the root that produced it, not the first root's project.
// An explicit config.project overrides for every root.
this.projectByRoot = new Map(
this.roots.map((r) => [r, config.project || deriveProjectName(r)]),
);
this.sessionId =
config.sessionId ||
`fs-watcher-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
Expand Down Expand Up @@ -214,7 +238,7 @@ export class FilesystemWatcher {
const payload = {
hookType: "post_tool_use",
sessionId: this.sessionId,
project: this.project,
project: this.projectByRoot.get(rootDir) ?? this.project,
cwd: rootDir,
timestamp: new Date().toISOString(),
data: {
Expand Down Expand Up @@ -319,7 +343,13 @@ export function configFromEnv(env = process.env) {
roots,
baseUrl: env.AGENTMEMORY_URL,
secret: env.AGENTMEMORY_SECRET,
project: env.AGENTMEMORY_PROJECT || null,
// AGENTMEMORY_PROJECT_NAME is the canonical override (matches the hooks);
// AGENTMEMORY_PROJECT stays as a deprecated alias for existing setups.
// Trimmed, with whitespace-only treated as unset, same as resolveProject.
project:
(env.AGENTMEMORY_PROJECT_NAME || "").trim() ||
(env.AGENTMEMORY_PROJECT || "").trim() ||
null,
sessionId: env.AGENTMEMORY_SESSION_ID || null,
ignorePatterns: extraIgnore,
allowBinary: env.AGENTMEMORY_FS_WATCH_ALLOW_BINARY === "1",
Expand Down
31 changes: 28 additions & 3 deletions integrations/hermes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,30 @@
import os
import sys
import threading
import subprocess
from pathlib import PurePath


def _resolve_project(cwd: str) -> str:
"""Canonical project scope, matching the hooks' resolveProject order:
AGENTMEMORY_PROJECT_NAME env override, git toplevel basename, cwd basename.
Keeps Hermes sessions in the same project bucket as every other agent."""
explicit = os.environ.get("AGENTMEMORY_PROJECT_NAME", "").strip()
if explicit:
return explicit
try:
top = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
cwd=cwd,
capture_output=True,
text=True,
timeout=5,
).stdout.strip()
if top:
return PurePath(top).name
except Exception:
pass
return PurePath(cwd).name or cwd
import time
from pathlib import Path
from typing import Any, Callable
Expand Down Expand Up @@ -188,14 +212,15 @@ def is_available(self) -> bool:
def initialize(self, session_id: str, **kwargs: Any) -> None:
self._base = os.environ.get("AGENTMEMORY_URL", DEFAULT_BASE_URL)
self._session_id = session_id
self._project = kwargs.get("cwd", os.getcwd())
self._cwd = kwargs.get("cwd", os.getcwd())
self._project = _resolve_project(self._cwd)
if os.environ.get("AGENTMEMORY_REQUIRE_HTTPS") == "1":
_check_plaintext_bearer_guard(self._base, os.environ.get("AGENTMEMORY_SECRET", ""))

_api(self._base, "session/start", {
"sessionId": session_id,
"project": self._project,
"cwd": self._project,
"cwd": self._cwd,
})

def get_config_schema(self) -> list[dict]:
Expand Down Expand Up @@ -348,7 +373,7 @@ def sync_turn(self, user: str, assistant: str, **kwargs: Any) -> None:
"hookType": "post_tool_use",
"sessionId": kwargs.get("session_id", self._session_id),
"project": self._project,
"cwd": self._project,
"cwd": self._cwd,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"data": {
"tool_name": "conversation",
Expand Down
35 changes: 31 additions & 4 deletions integrations/pi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "typebox";
import path from "node:path";
import crypto from "node:crypto";
import { execFileSync } from "node:child_process";
import { createPlaintextBearerAuthGuard } from "./security.js";

type TextBlock = { type?: string; text?: string };
Expand Down Expand Up @@ -120,7 +121,31 @@ export default function agentmemoryExtension(pi: ExtensionAPI) {
);
}
let sessionId = `ephemeral-${crypto.randomUUID().slice(0, 8)}`;
let currentProject = process.cwd();
// Canonical project scope, matching the hooks' resolveProject order (env
// override, git toplevel basename, cwd basename) so Pi sessions share a
// project bucket with every other agent instead of scoping on a raw path.
const projectCache = new Map<string, string>();
function resolveProjectName(dir: string): string {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"]?.trim();
if (explicit) return explicit;
const cached = projectCache.get(dir);
if (cached) return cached;
let name = path.basename(dir) || dir;
try {
const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
cwd: dir,
stdio: ["ignore", "pipe", "ignore"],
encoding: "utf8",
}).trim();
if (top) name = path.basename(top);
} catch {
// not a git repo
}
projectCache.set(dir, name);
return name;
}
let currentCwd = process.cwd();
let currentProject = resolveProjectName(currentCwd);
let lastPrompt = "";
let lastHealthOk = false;

Expand Down Expand Up @@ -227,12 +252,14 @@ export default function agentmemoryExtension(pi: ExtensionAPI) {
pi.on("session_start", async (_event, ctx) => {
const sessionFile = ctx.sessionManager.getSessionFile();
sessionId = sessionFile ? path.basename(sessionFile).replace(/\.[^.]+$/, "") : `ephemeral-${crypto.randomUUID().slice(0, 8)}`;
currentProject = process.cwd();
currentCwd = process.cwd();
currentProject = resolveProjectName(currentCwd);
await refreshStatus(ctx);
});

pi.on("before_agent_start", async (event, ctx) => {
currentProject = event.systemPromptOptions.cwd || process.cwd();
currentCwd = event.systemPromptOptions.cwd || process.cwd();
currentProject = resolveProjectName(currentCwd);
lastPrompt = event.prompt?.trim() || "";
if (!lastPrompt) return;

Expand Down Expand Up @@ -262,7 +289,7 @@ export default function agentmemoryExtension(pi: ExtensionAPI) {
hookType: "post_tool_use",
sessionId,
project: currentProject,
cwd: currentProject,
cwd: currentCwd,
timestamp: new Date().toISOString(),
data: {
tool_name: "conversation",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agentmemory/agentmemory",
"version": "0.9.28",
"version": "0.9.29",
"description": "Persistent memory for AI coding agents, powered by iii-engine's three primitives",
"type": "module",
"main": "dist/index.mjs",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agentmemory/mcp",
"version": "0.9.28",
"version": "0.9.29",
"description": "Standalone MCP server for agentmemory — thin shim that re-exposes @agentmemory/agentmemory's MCP entrypoint",
"type": "module",
"bin": {
Expand Down
Loading