Adds the governance / contribution / lifecycle documentation required
by aaif.io's Project Lifecycle Policy and their proposal template.
New files:
- GOVERNANCE.md — LF Minimum Viable Governance pattern scoped to the
project's current single-maintainer state, with explicit process for
adding Maintainers, lazy-consensus PR flow, and non-PR decision path
via 'governance'-labeled issues.
- CONTRIBUTING.md — fork/branch naming, Node requirements, local build
+ test expectations, DCO sign-off, PR flow, subsystem cheat-sheet,
step-by-step guidance for adding MCP tools and hooks, release
process summary.
- MAINTAINERS.md — current Maintainer roster (just me for now), an
Emeritus section, and named notable contributors in line for future
Maintainer review.
- ROADMAP.md — 12 months of planned work through Q1 2027, split into
quarterly themes (Depth / Breadth / Trust / v1.0) with shipped /
active / planned / candidate buckets per quarter.
- CODE_OF_CONDUCT.md — Contributor Covenant 2.1 with the enforcement
path pointing at the Maintainer email.
- SECURITY.md — vulnerability reporting flow via GitHub Security
Advisories, supported-version policy, scope, safe-harbor clause,
and a pointer to the .github/security-advisories/ drafts.
- .github/AAIF-PROPOSAL-DRAFT.md — paste-ready answers for every
required field on the aaif/project-proposals issue template,
targeted at Growth Stage entry.
No code changes.
Website chip + stats + terminal + features were hardcoded to 0.8.13,
44, 12, 49 — they drifted every release. Now every one of them is
read at build time from the real source of truth:
- lib/meta.ts (server-only) reads package.json, src/triggers/api.ts
(api_path count), src/mcp/tools-registry.ts (memory_* name count),
and the HookType union in src/types.ts.
- Hero chip, Stats, LiveTerminal, and Features all consume the
computed counts via props; no more literal strings in component
bodies.
- Fallback defaults (44 / 12 / 49) kick in only if the source files
cannot be read, so a monorepo re-root still boots.
Result: cutting a release bumps the website hero to v<new>
automatically, and any new MCP tool or REST endpoint raises the
displayed count without an edit.
next build stays clean — single static page prerendered.
Ships the new filesystem connector to npm on every release.
Also adds workflow_dispatch so we can manually publish packages
without cutting a release — useful for back-filling fs-watcher
against v0.9.0 which merged before the workflow knew about it.
Nav
- Replace overlay hamburger with a solid horizontal bar.
- Use the real agentmemory icon from assets/icon.svg as the brand mark.
- Single GitHub pill: GH icon + GITHUB label + gold star + live count,
the entire pill links to the repo. No more separate icon button.
- Server-rendered count pulled from api.github.com/repos/... with
revalidate 3600. Falls back to 0 on rate limit.
- Mobile (<720px) collapses to a hamburger sheet with sections + links.
Command Center (new)
- Tabbed section showing the shipped viewer (:3113), the iii console
(:3114), the raw KV state browser, and OTel traces.
- Real screenshots vendored into public/ (dashboard.png,
states.png, traces-waterfall.png, demo.gif).
- Each tab ships with a description, 4-5 capability bullets, and a
copy-ready launch command.
Features (new)
- 12-tile grid covering 12 auto-hooks, 44 MCP tools, 49 REST
endpoints, BM25+vector+graph recall, auto consolidation, JSONL
replay, knowledge graph extraction, mesh federation, Obsidian
export, 5 LLM providers, OTel observability, 0 external DBs.
Agents
- Replaced the flat tile grid with an agents.md-style two-tier layout.
- Top row: 4 featured first-party integrations (Claude Code,
OpenClaw, Hermes, Codex CLI) as big cards with real logos, brand
accent colors, and integration pitch.
- Bottom row: CSS keyframe marquee of every other supported agent
(Claude Desktop, Cursor, Gemini CLI, OpenCode, Cline, Roo,
Kilo, Goose, Aider, Windsurf) with real logos + brand borders on
hover. Pauses on hover. Edges fade into the page. Respects
prefers-reduced-motion.
Install · step 3 · MCP wiring
- Dropped the 10-tab toggle and the Cursor-only gold hero button.
- Left column: equal-weight agent chips in a 2x3 grid — Cursor,
VS Code, Claude Code, Claude Desktop, Gemini CLI, Codex CLI.
Click copies the right snippet (JSON / CLI command / TOML) or
fires the one-click deeplink where one exists.
- Right column: UNIVERSAL MCP JSON — paste-ready block that works
for Claude Desktop, Cursor, Cline, Windsurf, Gemini CLI, OpenCode.
- Expando reveals Hermes, OpenClaw, and Codex TOML shapes.
Infrastructure
- lib/github.ts server-only fetch for repo stats.
- lib/format.ts (client-safe) formatCompact.
- next.config.ts: turbopack.root pinned to silence workspace warning,
images.remotePatterns for github.com / GH avatars / Cursor /
Windsurf logos.
- layout.tsx: icon metadata wired to /icon.svg.
Build: next build clean, 3 routes prerendered static, 0 vulns.
Single-page marketing site for agentmemory generated from the
getdesign@latest 'lamborghini' design system.
- DESIGN.md at repo root: the full 288-line design brief (black canvas,
gold accent, Neo-Grotesk display, zero border-radius, hexagonal
motifs). Future UI PRs should read this file first.
- website/index.html: hero + stats + primitives + live terminal +
competitor comparison + agents grid + install + footer.
- website/styles.css: black-first palette (#000 canvas, #FFC000
accent, #202020 surface), Archivo display at 160px hero, JetBrains
Mono for the terminal, sharp-edge buttons, reveal-on-intersect.
- website/script.js: vanilla, zero deps.
- animated canvas memory graph in the hero with a hexagonal pause
control
- stat counters that run on first intersect
- 3D mouse-tilt on primitive cards
- typewriter terminal demo of memory.recall and memory.consolidate
with a REPLAY button
- click-to-copy install boxes with success feedback
- top scroll progress rail in gold
- full prefers-reduced-motion short-circuit
No framework, no build step. Drop behind any static host.
Three issues flagged by review.
1. CRITICAL - observe endpoint rejects current payload shape. The
/agentmemory/observe handler requires a full HookPayload (non-empty
hookType from the fixed HookType union, sessionId, project, cwd,
timestamp) with observation data under 'data'. The watcher was
emitting hookType='file_change'/'file_delete' (not valid HookType
values), possibly-null sessionId and project, no cwd or timestamp,
and files/content/metadata at the top level. The server would 400
every observation.
Now emits hookType='post_tool_use' with changeKind moved into
data.changeKind. sessionId auto-generates as
fs-watcher-<ts>-<rand> when not supplied; project defaults to the
first watched directory's basename. cwd is set to the root that
triggered the event, timestamp to new Date().toISOString().
files/content/rootDir/absPath/size/truncated/source all sit under
data.
2. MAJOR - start() returned successfully even when every fs.watch call
failed. In CLI/supervisor usage that looked like a healthy start
while no observations ever fired. Now start() throws if
this.watchers ends up empty, with the node-version hint in the
message since Node 18 + Linux is the likely cause.
3. MAJOR - engines.node was >=18, but fs.watch(recursive: true) on
Linux only landed in Node 19.1.0. Raised to >=20 (next LTS after
19.x) and documented the requirement in README.
test/fs-watcher.test.ts:
- Updated two shape assertions (hookType, sessionId, project, cwd,
timestamp at top level; files/content/changeKind under data).
- Added test that start() throws when no roots can be watched.
First of the data-source connectors tracked in #62. Ships as
@agentmemory/fs-watcher under integrations/filesystem-watcher/.
Scope (v0.1.0):
- Watches one or more roots via node:fs.watch({recursive: true}).
No native deps; works on macOS / Linux / Windows 10+.
- Emits file_change / file_delete observations to
POST /agentmemory/observe on the running server.
- Debounces 500ms per path so a stream of editor saves collapses
to a single observation.
- Reads the first 4 KB of text files for content preview;
truncation is flagged in metadata. Binary files are skipped by
default (AGENTMEMORY_FS_WATCH_ALLOW_BINARY=1 to opt in).
- Default ignore set: .git, node_modules, dist, build, .next,
.turbo, coverage, .DS_Store, *.log, *.lock. Extensible via
AGENTMEMORY_FS_WATCH_IGNORE.
- Attaches Bearer AGENTMEMORY_SECRET when set.
- CLI and env-driven config; CLI args win. Can be supervised with
launchd / systemd / pm2.
Tests (test/fs-watcher.test.ts):
- file_change on write, file_delete on unlink.
- Default ignore set skips node_modules/.
- Bearer auth header attached when secret is configured.
- Debounce collapses four rapid writes to <=2 observations.
- configFromEnv parses dirs and regex ignore patterns.
GitHub and Slack connectors (from #62) will be separate PRs since
they need OAuth / token handling and a shared event-normalizer;
the file-watcher has no such dependency and is useful standalone.
Addresses issue #125's policy question.
Decision adopted (hybrid of Options A and C):
- EVERY structural deletion must call recordAudit. Silent deletes
are forbidden.
- Scoped deletions (governance-delete, forget) emit one audit row
per call with targetIds populated.
- Bulk sweeps (retention-evict, evict, auto-forget) emit one
batched audit row per invocation with targetIds listing every
removed id, to avoid flooding the log during routine sweeps.
Implementation gap closed: mem::forget previously deleted memories,
observations, sessions, and summaries without ever calling
recordAudit. Added a single batched audit row per invocation with
sessionId, counts by type, and sessionDeleted flag.
Policy comment added to src/functions/audit.ts so new deletion
sites have an explicit precedent instead of discovering it via
review cycles.
Test coverage (test/remember-forget-audit.test.ts):
- Single-memory forget: one audit row, targetIds=[memId], forget op.
- Whole-session forget: one audit row batching all observation ids,
sessionDeleted=true, deleted count sums memories+observations+2.
- No-op forget (neither memoryId nor sessionId): no audit row.
All 769 existing tests still pass.
Resolve conflicts introduced by subsequent main commits touching the
same retention paths:
- types.ts: union the AuditEntry operation set — keep main's superset
(adds relation_create/relation_update/retention_score/skill_extract/
core_add/core_remove/auto_page). Drop HEAD's duplicate source key
on RetentionScore (already present).
- functions/retention.ts: keep main's cleaner score assembly that
uses pre-computed temporalDecay / reinforcementBoost / logs-based
lastAccessed and accessCount. Keep main's evictedIds /
evictedEpisodic / evictedSemantic counters. Adopt main's batched
audit emission (single record per invocation with reason) from
after the loop — HEAD's per-candidate audit emission fails the
'single batched audit record' test and would also flood the audit
log during normal eviction sweeps. Preserve HEAD's null-guarded
continue when neither the episodic nor the semantic store has the
candidate, so legacy rows with a missing source field still evict
cleanly without a spurious delete against the wrong scope.
- test/retention.test.ts: drop HEAD's smaller test in favor of
main's more comprehensive suite (scores tag rows with source /
evicts from mem:semantic / batched audit on success / no audit
when evicted=0 / score emits per-rescore audit / probes namespaces
for legacy semantic rows / routes legacy missing-source episodic
rows to mem:memories). Main's suite is a strict superset of HEAD's
original assertion.
- 04-mesh-unauth.md: correct 4->5 mesh REST endpoints in patch summary.
- 05-obsidian-export-traversal.md: document the known symlink-traversal
limitation of resolveVaultDir(). The 0.8.2 fix uses lexical containment
only (path.resolve + startsWith) and does not call realpath/lstat, so a
pre-existing symlink under AGENTMEMORY_EXPORT_ROOT pointing outside the
root is still a viable escape. Advisory now names this explicitly and
points users at sandboxing or a follow-up issue for full hardening.
Four issues flagged by review.
1. Handle cache was never invalidated. Once resolveHandle() set
cached = local, a later server start would never be picked up
until the process restarted. Add invalidateHandle() + a 30s TTL
on local-mode caching so the next call after proxy failure or
timeout re-probes.
2. Response shapes diverged between proxy and local. memory_recall
and memory_smart_search returned { mode, query, results } from
the server but Memory[] from local; memory_audit returned an
array from local and whatever the server sent from proxy.
Normalize local responses to server-shaped envelopes
({ mode: compact, results: [...] } and { entries: [...] }) so
a per-request fallback no longer flips structure under the caller.
3. Validation errors from handleProxy were logged as 'proxy call
failed' and then retried against local, which re-threw the same
validation error. Extract validate(toolName, args) and run it
once before mode resolution, so empty content/query/memoryIds
throw cleanly with no misleading stderr line.
4. Bearer-token test was vacuous — seen.every() passes trivially
on an empty list and only the probe was guaranteed to record.
Replace with a Map<path, auth> assertion that pins both the
probe at /livez and the proxied tool call at /sessions to
Bearer s3cret.
Updates test/mcp-standalone.test.ts to match the new local
response shape (previously asserted raw Memory[] length, now
asserts results.length).
CodeRabbit flagged that the 'visibility without inflating severity'
goal was only met at critical-level heap. When heap sat in the warn
band (80-95%) with RSS below the floor, no alert was produced at
all, so operators got zero signal that heap pressure was building.
Widen the heap_tight branch to cover the full memoryWarnPercent+
band. New test: 85%% heap / 50 MB RSS stays healthy but records
memory_heap_tight_*.
Previously @agentmemory/mcp's handleToolCall only ever used a local
InMemoryKV, so in the supported Claude Code setup (plugin +
@agentmemory/agentmemory server) the MCP tools never saw the data
hooks had captured — memory_sessions returned [] even when the REST
endpoint had 69 observations, memory_smart_search searched a
standalone.json that only contained explicit memory_save calls, and
the server's BM25 + vector + graph indexing was inaccessible through
MCP.
Now handleToolCall resolves a Handle once per process: if a server
responds to GET /agentmemory/livez within 500ms, all subsequent tool
calls route through the REST endpoints and see exactly what hooks and
the viewer see. If the probe fails, the caller falls back to the
existing local InMemoryKV path so users who run MCP alone still work.
- New src/mcp/rest-proxy.ts: probe + cached Handle with 15s call
timeout. Honours AGENTMEMORY_URL (defaults to http://localhost:3111)
and attaches Bearer AGENTMEMORY_SECRET when set.
- src/mcp/standalone.ts: split handleProxy and handleLocal branches;
announce active mode once to stderr so users can see which store
their tools are reading. On per-request proxy failure, log and
fall back to local KV for that single call.
- test/mcp-standalone-proxy.test.ts: five cases covering proxy happy
path, auth header, unreachable server fallback, and post-probe
proxy failure fallback.
Previously heapUsed/heapTotal ratio alone decided memory severity, so
a small steady-state Node process whose V8 heap naturally fills its
46 MB allocation could report memory_critical_97% while the RSS sat
at 120 MB and the host had plenty of free RAM.
Require a second signal before degrading overall health: heap ratio
above threshold AND RSS above memoryRssFloorBytes (default 512 MB).
When heap is tight but RSS is below the floor, record a non-alerting
memory_heap_tight_NN% note so the info is still visible in the
snapshot without inflating status to degraded/critical.
New tests cover the reporter's live-deployment example plus the
critical / degraded / healthy branches around the RSS floor.
Add two screenshots sourced from iii.dev/docs/console and vendored into
assets/iii-console/ so they don't depend on CDN signed URLs:
- dashboard.png — hero shot showing counters, application flow, and
registered triggers.
- traces-waterfall.png — trace waterfall with per-span durations,
placed above the traces setup block.
states.png is checked in for a future edit but not yet referenced.
agentmemory runs on the iii engine, so the official iii console gives
users OTEL traces, the raw KV state browser, the stream monitor, and a
direct function invoker for every piece of memory machinery without
wiring up MCP or a custom client.
- Install and launch instructions, with --port 3114 to avoid the
agentmemory viewer already holding 3113.
- Page-by-page guide covering Functions, Triggers, States, Streams,
Traces, and Logs with agentmemory-specific use cases.
- Notes that the iii-observability worker is already enabled in
iii-config.yaml with exporter: memory, so Traces and Logs light
up by default.
- Warns that the console has no built-in auth and must stay on
127.0.0.1.
Security:
- Tighten sensitive-path guard to boundary-aware regexes (no more
false positive on project names containing "token", "secret", etc.)
- Re-check every walked JSONL file against the guard, not just the
root argument
- Expand leading "~" in user-supplied paths before resolve()
Correctness:
- Update existing Session.observationCount on re-import instead of
silently leaving it stale
- Parallelize per-session observation writes via Promise.all
- Capture a single fallback timestamp in the parser so startedAt and
endedAt agree when metadata is missing
- Trim incoming path in the import REST handler
Robustness:
- Add AbortSignal.timeout(120s) to the CLI import fetch and surface
TimeoutError cleanly
- Stop the replay interval when switching away from the replay tab
- Style the Replay tab empty state with a new .empty rule
Sync:
- Bump REST endpoint count (104 -> 107) in startup banner + README
- Add 0.7.9 and 0.8.0 to VERSION union so it matches ExportData.version
Tests:
- Add replay-sensitive.test.ts covering false-positive avoidance
- Add parser test for shared fallback timestamp
Adds a Replay tab to the viewer that plays any stored session as a
scrubbable timeline (prompts, tool calls, tool results, responses)
with play/pause, speed control, and keyboard navigation.
Adds `agentmemory import-jsonl [path]` CLI subcommand and a matching
POST /agentmemory/replay/import-jsonl endpoint to backfill sessions
from Claude Code JSONL transcripts (default ~/.claude/projects).
All new logic routes through iii-sdk primitives: registerFunction for
mem::replay::load / mem::replay::sessions / mem::replay::import-jsonl,
and the existing HTTP trigger in api.ts for REST exposure.
Security: JSONL import rejects symlinks, paths containing sensitive
terms, and tolerates malformed lines without aborting the batch.
All imports are recorded in the audit log.
lstat alone has a TOCTOU race: a symlink swap between the check and the
writeFile call lets an attacker redirect the write. Replace the final write to
absolutePath with open(O_NOFOLLOW | O_WRONLY | O_CREAT | O_TRUNC) so the
kernel rejects the open atomically if the path is a symlink at open-time.
Keep lstat as a fast-fail before the LLM call. Handle ELOOP/EINVAL from open
and return the same "symlinks are not supported" error.
Tests: add open mock with ELOOP support, fix lstat mock to throw ENOENT for
unknown paths (matches runtime behavior), add TOCTOU test, add negative
assertions to the symlink test (no summarize call, no writes), add "file not
found" test for the ENOENT path.
lstat() check before any I/O now rejects symlinks, closing the bypass
where notes.md -> vault.txt passed the .md extension check but the
subsequent readFile/writeFile followed the symlink to the real target.
Adds a regression test that marks a path as a symlink in the mock and
verifies the function returns {success:false, error: "symlinks are not supported"}.
Two changes:
1. detectProvider() now accepts GOOGLE_API_KEY as an alias for GEMINI_API_KEY.
Emits a stderr advisory to rename the variable.
2. When no provider key is found and AGENTMEMORY_AUTO_COMPRESS=true, emits a
clear warning that agent-sdk will share Claude Code's API quota — the root
cause of #149 where a wrong env var name caused silent fallback to agent-sdk
during a heavy Opus session.
lstat() check before any I/O now rejects symlinks, closing the bypass
where notes.md -> vault.txt passed the .md extension check but the
subsequent readFile/writeFile followed the symlink to the real target.
Adds a regression test that marks a path as a symlink in the mock and
verifies the function returns {success:false, error: "symlinks are not supported"}.
The new file-compression feature promises automatic backup creation,
but `*.original.md` inputs currently collapse the backup path onto the
source path and lose the original content. This keeps the common
`foo.md -> foo.original.md` behavior unchanged while assigning a
distinct backup name for already-suffixed inputs.
Constraint: Keep the fix narrow to backup-path generation and one regression test
Rejected: Introduce numbered backup discovery logic | larger behavior change than needed for the verified bug
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Preserve backup-path distinctness for any future suffix handling changes
Tested: npm test -- test/compress-file.test.ts; npm run build
Not-tested: Full npm test still hits an unrelated timeout in test/auto-compress.test.ts on this machine
* feat: add token-efficient recall and file compression tool
Add memory_recall format and token budget support, plus a new memory_compress_file tool with API/MCP wiring, tests, and updated tool/endpoint docs.
* chore: remove temporary architecture assets
Delete temporary architecture and memory-cycle asset files added during implementation.
String.includes(query) required the full query to appear as a contiguous
substring, so "BEAM bi-level LLM" returned nothing even when each word
existed across title/content/concepts.
Split on whitespace and require every token to match (AND semantics).
Single-word queries are unaffected — a one-element array with .every
behaves identically to the old .includes.
Fixes#147
* fix: replace getContext() with logger shim for iii-sdk v0.11 compatibility
iii-sdk v0.11 dropped getContext() entirely. 32 src/functions/*.ts files still
imported and called it, crashing node dist/index.mjs on first import with:
SyntaxError: module 'iii-sdk' does not provide an export named 'getContext'
- Add src/logger.ts: thin stderr shim (info/warn/error) with identical call signature
- Remove getContext import and const ctx = getContext() from all 32 function files
- Replace ctx.logger.* with logger.* throughout
- Fix search.ts registerFunction({ id: ... }) -> registerFunction('id') for v0.11 API
- Update 45 test mock blocks from vi.mock("iii-sdk") to vi.mock("../src/logger.js")
- Bump to v0.8.11
Tests: 731/731 pass. Binary starts cleanly. Closes#116.
* fix: bump version to 0.8.11 in plugin.json, export-import supported versions, and test