agentmemory is a persistent memory system for AI coding agents, built on iii-engine's three primitives (Worker/Function/Trigger). Everything goes through registerFunction/registerTrigger/sdk.trigger() — never bypass iii-engine with standalone SQLite or in-process alternatives.
- Engine: iii-sdk (WebSocket to iii-engine on port 49134)
- State: File-based SQLite via iii-engine's StateModule (
./data/state_store.db) - Build: TypeScript → ESM via tsdown, output to
dist/ - Test: vitest (
npm testexcludes integration tests)
When adding or removing MCP tools, you MUST update ALL of the following:
src/mcp/tools-registry.ts— tool definition +getAllTools()arraysrc/mcp/server.ts— handler case in themcp::tools::callswitchsrc/triggers/api.ts— REST endpoint registrationsrc/index.ts— function registration + endpoint count in the log linetest/mcp-standalone.test.ts— tool count assertionREADME.md— tool counts (search for "MCP tools")plugin/.claude-plugin/plugin.json— tool count in descriptionplugin/plugin.jsonandplugin/.mcp.copilot.json(when present) — tool count or MCP exposure
When adding REST endpoints, you MUST update:
src/triggers/api.ts— endpoint registrationsrc/index.ts— endpoint count in the log lineREADME.md— endpoint count (search for "REST endpoints" and "endpoints on port")
When bumping version, you MUST update ALL of the following:
package.json— version fieldsrc/version.ts— VERSION constant and type unionsrc/types.ts— ExportData version unionsrc/functions/export-import.ts— supportedVersions settest/export-import.test.ts— version assertionplugin/.claude-plugin/plugin.json— version fieldplugin/plugin.json(when present) — version field
When adding new KV scopes:
src/state/schema.ts— add to the KV objectsrc/types.ts— add the corresponding interface
When adding new audit operations:
src/types.ts— add to AuditEntry.operation union type
sdk.registerFunction(
"mem::your-function",
async (data: { ... }) => {
// validate inputs
// do work via kv.get/kv.set/kv.list
// record audit via recordAudit()
return { success: true, ... };
},
);sdk.registerFunction("api::your-endpoint", async (req: ApiRequest) => {
const denied = checkAuth(req, secret);
if (denied) return denied;
const body = req.body as Record<string, unknown>;
// validate + whitelist fields (never pass raw body to sdk.trigger)
const result = await sdk.trigger({
function_id: "mem::your-function",
payload: { ... },
});
return { status_code: 200, body: result };
});
sdk.registerTrigger({
type: "http",
function_id: "api::your-endpoint",
config: { api_path: "/agentmemory/your-path", http_method: "POST" },
});case "memory_your_tool": {
// validate args with typeof checks
// parse CSV args: args.field.split(",").map(t => t.trim()).filter(Boolean)
const result = await sdk.trigger({
function_id: "mem::your-function",
payload: { ... },
});
return { status_code: 200, body: { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] } };
}Hook scripts in src/hooks/ are standalone Node.js scripts (no iii-sdk import). They read JSON from stdin, make HTTP calls to the REST API, and exit. There are two patterns depending on whether Claude Code consumes the script's stdout:
- Context-injecting hooks (
pre-tool-use,pre-compact,session-start) write recalled context to stdout for Claude Code to inject. These MUST usetry/catchwithawait fetch(..., { signal: AbortSignal.timeout(N) })— the script has to wait for the response before exiting, and the timeout is the only bound on hang time. - Telemetry-only hooks (
notification,post-tool-failure,post-tool-use,prompt-submit,stop,session-end,subagent-start,subagent-stop,task-completed) write nothing to stdout. These MUST use fire-and-forgetfetch(..., { signal: AbortSignal.timeout(N) }).catch(() => {})paired withsetTimeout(() => process.exit(0), 500).unref(). The unawaited fetch dispatches the request; the unref'dsetTimeoutforce-exits the process after the request has been flushed to the local daemon's socket buffer (~500ms is enough for single-request hooks; use 1500ms for multi-request hooks likestopandsession-endso all fetches have time to start, especially whenAGENTMEMORY_URLpoints to a remote daemon). Without thesetTimeoutNode keeps the event loop alive waiting for any in-flight fetch to settle, which means the hook still blocks Claude Code's next-prompt boundary for up to the AbortSignal duration — exactly the bug fire-and-forget is meant to fix.
- TypeScript, ESM only (
"type": "module") - No code comments explaining WHAT — use clear naming instead
- Use
fingerprintId()for content-addressable dedup,generateId()for unique IDs - Parallel operations where possible (
Promise.allfor independent kv writes/reads) - Input validation at system boundaries (MCP handlers, REST endpoints)
- REST endpoints must whitelist fields — never pass raw request body to
sdk.trigger() - Use
recordAudit()for state-changing operations - Timestamps: capture once with
new Date().toISOString()and reuse
- All tests must pass before PR:
npm test(950+ tests) - Mock pattern:
vi.mock("iii-sdk")with mocksdk.trigger,kv.get/set/list - Test files go in
test/with.test.tsextension - Follow existing patterns in
test/crystallize.test.tsfor function tests
- 53 MCP tools (8 visible by default,
AGENTMEMORY_TOOLS=allfor all) - 128 REST endpoints
- 6 MCP resources, 3 MCP prompts
- 12 hooks, 15 skills
- 50+ iii functions
- 950+ tests