Fork of rohitg00/agentmemory.
This README is the source of truth for this fork: why it exists, the Cursor-host goals, and what has shipped here versus upstream. Keep it current when that picture changes. AGENTS.md is for architecture and consistency rules. For install, MCP tools, REST endpoints, and the rest of the product, read the upstream README.
Upstream AgentMemory assumes Claude Code. Sessions start and end. Tool calls are the main observation type. Content gets deduped at ingest. Processing often waits for the session to finish.
Cursor does not work that way, and Cursor Cloud is worse. Conversations stay open. End hooks flake. If you force Cursor into the Claude model on the client, you invent fake tool observations, cache prompts, and fake session boundaries. Fix the server instead.
This fork treats Cursor as a real host. Host adapters stay thin. They map native events into one shared envelope and send them. The server keeps the raw event log, accepts late events on old conversations, and consolidates asynchronously.
A second, later gap: the production deploy of this fork OOM-killed under Cursor hook load. Upstream's graph and search paths assume a kv.list of a whole KV scope is cheap. In iii-engine it is not — state::list has no pagination, so a graph-table list materializes as one JSON array. This fork now bounds that work and treats the graph as a derived catalog, not a source of truth.
- Stay compatible with upstream. Prefer small, additive changes. No breaking changes or big rewrites. Keep Cursor support without drifting farther from upstream than you need to.
- Cursor is a real host. Stop adapting Cursor to look like Claude Code.
- Keep an immutable raw event log. Store hook types such as
prompt_submit,assistant_response,post_tool_use/post_tool_failure, andsubagent_start/subagent_stopinKV.rawEvents(mem:raw:{sessionId}). Do not rewrite conversation messages as fake tool observations. Summaries and compressed observations stay inKV.observationsas derived views. They must never overwrite the source, so you can rerun summarization later. The raw log starts empty at deploy. Historical observations had their source destroyed at ingest time and cannot be backfilled.mem::compressreads from the raw log; summarization still reads derived observations. - Idempotency uses client event IDs. Each event gets a stable client-generated ID. Retrying the same ID is a no-op. Two different IDs with the same content both stay. Drop ingest dedup by session, content, or short time windows. Semantic dedup belongs on derived memories. Idempotency is durable:
eventIdis stored on the raw event and indexed in KV for as long as that event exists. - Conversations are open-ended streams. Clients still send a
sessionIdfor grouping. Ingest must not requiresessionStartorsessionEnd. There is no usefulactiveorcompletedingest state. People come back later. - Drive processing with watermarks. Track progress with
(timestamp, id)event cursors on the session row:lastEventAt,lastSummarizedEventId/lastSummarizedEventAt,lastReflectedEventId/lastReflectedEventAt,lastGraphExtractedEventId/lastGraphExtractedEventAt, plussummaryRevisionandsummarizedObservationCount. Summarization, reflection, and graph extraction run incrementally over new compressed observations only. Consolidation, crystallization, and skill extraction still run on their existing cadences. None of them wait for a completed session. - Trigger work from signals, not session end. Run at turn boundaries, after idle time, after event or observation-count thresholds, on an explicit summarize, and from a periodic recovery sweep. Do not call an LLM on every observation. If Cursor Cloud skips a hook, memory formation must still catch up later.
/session/endis a deprecated noop. Keep the route for old clients. It must not close the conversation, stamp completed, fan out processing, or reject later events. If the UI needs an archive flag, that is a user action, not an ingest state.- Keep host adapters thin. They translate hook payloads into the shared envelope. They do not paper over server gaps with prompt caches or fake tool events. Skip agent thoughts by default. Keep prompts, final responses, decisions, and useful tool outcomes.
- Import Cursor local transcripts. Upstream already imports Claude Code JSONL (
import-jsonl/POST /agentmemory/replay/import-jsonl, parser insrc/replay/jsonl-parser.ts). That path is Claude-shaped (type/message.role/tool_uselines under~/.claude/projects) and writes synthetic compressed observations plus heuristic crystals. It does not walkmem::observelike live hooks. Cursor stores a different local transcript schema. This fork should parse Cursor transcripts (prompts and agent messages first; useful tool outcomes when present), map them into the shared envelope (prompt_submit/assistant_response/ tool events as appropriate), and ingest through the standard observe → compress → summarize / idle-sweep path so historical chats become real memory the same way hook traffic does. - Stay alive under Cursor hook load. Hook clients abort at 2.5s and the server is never told. Bound the hook path, do not
kv.listthe graph tables, and keep the live graph a derived catalog of observations and memories.
- Session start is optional.
/observe,/summarize, and/enrichlazy-create a session whensessionId+project+cwdarrive without a prior/session/start. RequestagentIdis honored on that create (e.g."cursor");/session/startstill works for clients that call it. - Pass B.
/session/enddeprecated noop. Sessions are not closed by ingest lifecycle (api::session::end/event::session::endeddo not stampcompleted/endedAtor fan out stopped work). Start remains optional (Pass A). - Pass C. Idle catch-up sweep. A bounded
mem::idle-sweeptimer processes sessions that have pending observations and either (a) went idle, or (b) accumulated ≥N new observations since last sweep (so all-day Cursor chats are not stuck because/observekeeps refreshingupdatedAt). It reusesevent::session::stoppedwithskipConsolidation. Failed attempts stamplastSweepAttemptAtfor cooldown without advancing summarize watermarks. Turn-boundaryPOST /summarizestays primary; eviction recovery is unchanged. Knobs:AGENTMEMORY_IDLE_SWEEP_*,AGENTMEMORY_IDLE_THRESHOLD_MS. - Pass D. Client event ID idempotency.
/observeaccepts optionaleventId. When present, retries of the samesessionId+eventIdreturn{ deduplicated: true }and do not write again. When absent, the observation is always written (content/time-window ingest dedup is gone).eventIdis optional so existing hooks keep working; clients that care about retries should send one. - Pass E. Assistant and subagent compression.
assistant_response,subagent_start, andsubagent_stopare lifted into the compression path (synthetic or LLM) so titles and narratives summarize them like tool observations. Adapters can send these hook types directly instead of remapping them to fakepost_tool_usetool names. - Pass F. Durable raw event log.
mem::observewritesRawObservationrows toKV.rawEventsand syntheticCompressedObservationrows toKV.observationsimmediately (both auto-compress modes).mem::compressupgrades the derived row when LLM compression succeeds. Forget, eviction, export replace, and snapshots prune or carry both scopes. Export includes raw events only whenincludeRawEvents=true. - Pass G. Event watermarks. Sessions track
(timestamp, id)cursors for summarize, reflect, and graph extraction (lastSummarizedEventId/lastSummarizedEventAt, etc.) pluslastEventAton ingest.mem::summarizeincrementally merges new compressed observations into the stored summary via the existing reduce prompt, with a periodic full rebuild everyAGENTMEMORY_SUMMARY_REBUILD_INTERVALrevisions (default 10). Pass C'sidleProcessedObservationCount/idleProcessedAtmarkers are replaced bysummarizedObservationCount/lastSweepAttemptAt. Legacy rows without cursors get one full pass then are stamped. Summarize still readsKV.observations, not the raw log. - Pass H. Durable ingest idempotency.
eventIdis persisted onRawObservationinKV.rawEventsand indexed inKV.eventIds(mem:evt:{sessionId}). Retries survive restart (no TTL) for as long as the event exists; a retry that arrives after the first write has landed will dedup on any replica. Concurrent same-eventIddelivery to different replicas can still double-write (the observe lock is in-process). The index entry is pruned with the raw event (and cleared on whole-session forget). Import and snapshot restore re-index events that carry aneventId. - Pass I. Raw log reader and upgrade-aware summarize.
mem::compressloads its input fromKV.rawEvents(payloadrawremains an optional fallback). Derived rows recordderivedBy: "synthetic" | "llm". WhenAGENTMEMORY_AUTO_COMPRESSis on, summarize truncates its eligible batch at the first non-llmrow still insideAGENTMEMORY_COMPRESS_UPGRADE_GRACE_MS(default 5 minutes), including periodic full rebuilds, so an in-place LLM upgrade cannot be skipped by the watermark. Past the grace window the synthetic row is summarized as-is. - Pass J. Upgrade-aware reflect and graph extraction. Shared helper
truncateAwaitingLlmUpgradeinsrc/functions/compress-upgrade-gate.tsgatesmem::slot-reflect, theevent::session::stoppedgraph-extract fan-out, and session-scopedmem::graph-extractthe same way as summarize when auto-compress is on. Watermarks and downstream work derive from the gated batch only; import callers withoutsessionIdstay ungated. - Cursor local transcript import. Added
POST /agentmemory/observe/bulk(serial batch ofmem::observe, max 500). Historical local chats are imported with agentmemory-cursor-importer: prompts + assistant text only, session-exists skip, stableeventIds, timestamps from user<timestamp>tags or file birthtime/+1ms. Cloud transcripts are out of scope.
Production of this fork (memory on Railway) OOM-killed and returned HTTP 499s under Cursor hook load. The main defect: every hybrid search listed KV.graphNodes and KV.graphEdges (no pagination), then discarded the hits because retrieval hardcoded an empty sessionId. At 32k nodes / 61k edges that was ~60 MB of JSON and ~2s of KV time per query, plus engine RSS that never came back. Hooks abort at 2.5s; iii's HTTP trigger has no abort signal, so the server kept working.
What shipped (merged PRs #25–#29):
- Bounded hook path. Enrich budget, search concurrency cap, KV read timeouts, observe embedding moved off the session lock onto a queue, cached file-context enumerations. Writes stay unbounded on purpose: a timeout is not a cancel, and a late commit would race observe rollback. Health reads the cgroup (the container the OOM killer sees), not just this process's heap. Evidence:
docs/investigations/2026-08-24-latency-and-oom.md. - Background LLM gate. Compress, summarize, and LLM graph-extract share
AGENTMEMORY_BACKGROUND_LLM_CONCURRENCY(default 2) so a hook burst does not start one provider call per event. - Snapshot-only graph readers. After a graph reset,
KV.graphNodes/KV.graphEdgesstill hold orphan rows. Query, search, export, reflect, mesh, cascade, temporal merge, and MCP graph stats read the snapshot only.mem::graph-snapshot-rebuildrefuses afterresetAteven withforce. Incremental extract still fills the snapshot. - Write-path graph search indexes. When
AGENTMEMORY_GRAPH_SEARCH=true, search uses token, adjacency, and observation indexes written bypersistGraphDelta, temporal extract, mesh apply/receive, and cascade. Keys are namespaced byresetAt, so pre-reset orphans are never read. First search backfills from the snapshot once per reset. It does notkv.listthe graph tables. The flag must be the stringtrue; the code default stays off.
Graph model this fork settled on:
- Observations and memories are the source of truth.
- The graph is a derived catalog. Reset clears the live snapshot; it does not delete orphan graph-table rows.
- Leave those orphans on disk. Do not index them. Do not vacuum. Do not run
snapshot-rebuildafterresetAt. - A reset live graph starts empty (0/0). Graph search is then safe but adds no recall until extract writes post-reset nodes.
Intentionally not done: real HTTP cancel (iii has no abort), vacuum, hook rewrite, and vision.
Upstream syncs are high-risk now. The fork sits on the same lines upstream keeps editing; a clean merge can silently restore the old OOM path. Walk .cursor/commands/sync-upstream.md (including its load-bearing patches table) even when there are no conflicts. Do not edit the guard tests to match upstream.
- Vision. Investigated and dropped 2026-08-11. Ingest already stores images (
extractImage→~/.agentmemory/images/{sha256}, refcounted inKV.imageRefs), but nothing downstream works.describeImageis optional onMemoryProviderand onlyAnthropicProviderimplements it, yetResilientProvider(whatcreateProvideractually returns) does not delegate it, so the caption path is dead for every provider.memory_vision_searchsearches CLIP vectors only, produced by a localXenova/clip-vit-base-patch32behindAGENTMEMORY_IMAGE_EMBEDDINGS, unrelated to the configured LLM. None of that matters yet because Cursor supplies no images:beforeSubmitPromptattachments are{ type: "file" | "rule", file_path }andafterAgentResponseis one text field. If images ever become reachable it will be throughtranscript_path, so vision belongs with the Cursor transcript import goal, not as its own pass.