Skip to content

feat(slots): editable pinned memory slots + idle reflection + global scope#182

Merged
rohitg00 merged 3 commits intomainfrom
feat/memory-slots-v010
Apr 21, 2026
Merged

feat(slots): editable pinned memory slots + idle reflection + global scope#182
rohitg00 merged 3 commits intomainfrom
feat/memory-slots-v010

Conversation

@rohitg00
Copy link
Copy Markdown
Owner

@rohitg00 rohitg00 commented Apr 21, 2026

Summary

Adds memory slots as a new first-class memory primitive, orthogonal to observations / memories / graph. A slot is a labelled, size-limited, agent-editable chunk of context. 8 defaults seed on first run:

  • persona — how the agent should see itself (tone, role)
  • user_preferences — coding style, tool habits
  • tool_guidelines — rules for tool picking / sequencing
  • project_context — architecture, conventions, build/test commands
  • guidance — active advice for the next session
  • pending_items — explicit TODOs
  • session_patterns — recurring behaviours across sessions
  • self_notes — free-form scratch for the agent

Agent edits via MCP tools; pinned slots serialise cleanly and can be injected into SessionStart context.

Why this is different from existing memory_save

  • Addressable: every slot has a stable label → agent can append/replace in-place
  • Size-limited: hard cap per slot (default 2KB, max 20KB) → predictable context footprint
  • Pinned vs not: pinned slots are candidates for SessionStart injection; others live until explicitly fetched
  • Scope-aware: project vs global — one brain across all projects for persona / user_preferences / tool_guidelines
  • Not vector-searched: slots are keyed access, not retrieval. BM25 + graph + vision stay intact

Surfaces

Functions (src/functions/slots.ts)

mem::slot-list, mem::slot-get, mem::slot-create, mem::slot-append (refuses writes over sizeLimit), mem::slot-replace, mem::slot-delete, mem::slot-reflect.

REST (src/triggers/api.ts)

  • GET /agentmemory/slots
  • GET /agentmemory/slot?label=...
  • POST /agentmemory/slot
  • POST /agentmemory/slot/append
  • POST /agentmemory/slot/replace
  • DELETE /agentmemory/slot?label=...
  • POST /agentmemory/slot/reflect

MCP (src/mcp/tools-registry.ts, src/mcp/server.ts)

memory_slot_list, memory_slot_get, memory_slot_create, memory_slot_append, memory_slot_replace, memory_slot_delete.

Idle reflection

event::session::stopped fires mem::slot-reflect via triggerVoid when AGENTMEMORY_REFLECT=true. The reflector reads the last N observations, extracts TODO-flavoured titles into pending_items, counts error/command types into session_patterns, and records touched files in project_context. Fire-and-forget — does not block the summarise path.

Scope: project vs global

Slots carry a scope field (\"project\" | \"global\"). Global slots live at KV.globalSlots (mem:slots:global) — one brain across all projects for things like persona, user_preferences, tool_guidelines. A project-scoped slot with the same label shadows the global one on read.

Opt-in

  • AGENTMEMORY_SLOTS=true — enable the whole primitive
  • AGENTMEMORY_REFLECT=true — enable Stop-hook reflection (requires SLOTS)

Both default OFF. Matches the opt-in convention of #138 (auto-compress), #143 (inject-context), #160 (health thresholds), #179 (vision embeddings).

Counts bumped

44 MCP tools → 50 across README stats badge, install instructions, config block; plugin.json description. New env flag docs for AGENTMEMORY_SLOTS / AGENTMEMORY_REFLECT.

Test plan

  • npm run build clean
  • npm test800/800 pass (+12 new cases in test/slots.test.ts)
  • Coverage: default seeding by scope, label validation, create/get/append/replace/delete round-trip, duplicate-create rejection, sizeLimit enforcement, project-shadows-global, pinned list + render, reflect no-op on empty session, reflect updates pending_items / session_patterns / project_context from TODO + error + file signals
  • Manual: AGENTMEMORY_SLOTS=true npx @agentmemory/agentmemory, curl :3111/agentmemory/slots → 8 default slots
  • Manual: append to pending_items, start new session, verify Stop hook with AGENTMEMORY_REFLECT=true updates slot content

Next

Phase B (.af-compatible import/export) and a viewer "Slots" tab are filed as follow-ups. Wanted this PR to stay focused on the primitive.

Summary by CodeRabbit

  • New Features

    • Added memory slot management (create, read, append, replace, delete) and optional automatic reflection; new HTTP endpoints and status exposure when enabled.
  • Documentation

    • Updated supported tool count to 51 and added commented configuration flags for memory slots and reflection; test suite count updated to 800.
  • Tests

    • Added comprehensive integration tests covering slot lifecycle and reflection behavior.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentmemory Ready Ready Preview, Comment Apr 21, 2026 6:54pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: aaadc2ef-0f6e-42f2-aff9-60370a252c4b

📥 Commits

Reviewing files that changed from the base of the PR and between b813d30 and 02d13c5.

📒 Files selected for processing (11)
  • README.md
  • plugin/.claude-plugin/plugin.json
  • src/functions/slots.ts
  • src/index.ts
  • src/mcp/server.ts
  • src/mcp/tools-registry.ts
  • src/state/schema.ts
  • src/triggers/api.ts
  • src/triggers/events.ts
  • src/types.ts
  • test/slots.test.ts

📝 Walkthrough

Walkthrough

Adds a KV-backed "memory slots" feature with CRUD and reflect operations, SDK and HTTP endpoints, MCP tools and registry entries, startup/event hooks, types/schema updates, README/plugin documentation edits, and tests for seeding, CRUD, limits, scope shadowing, and reflect behavior.

Changes

Cohort / File(s) Summary
Documentation
README.md, plugin/.claude-plugin/plugin.json
Updated MCP tool counts (README → 50 tools; plugin description → 51 tools). README adds AGENTMEMORY_SLOTS and AGENTMEMORY_REFLECT flags and increases test count to 800.
Types & Schema
src/types.ts, src/state/schema.ts
Added MemorySlot interface and new audit operations; added KV keys mem:slots and mem:slots:global.
Slots Implementation
src/functions/slots.ts
New module: default pinned slots, KV seeding, helpers (isSlotsEnabled, isReflectEnabled, pinned utilities), and SDK endpoints mem::slot-list/get/create/append/replace/delete/reflect with validation, keyed locks, audits, and size limit enforcement.
Integration & Startup
src/index.ts, src/triggers/events.ts
Conditional registration/logging for slots when enabled; event::session::stopped now awaits summarize and fire-and-forgets mem::slot-reflect when reflect is enabled (with error logging).
MCP Tooling
src/mcp/tools-registry.ts, src/mcp/server.ts
Added V010_SLOTS_TOOLS (six memory_slot_* tools) and MCP handler cases mapping memory_slot_* calls to SDK triggers with per-tool argument validation and response mapping.
HTTP API Endpoints
src/triggers/api.ts
Added authenticated endpoints api::slot-* for list/get/create/append/replace/delete/reflect with input validation, parsing, and mapped HTTP status semantics.
Tests
test/slots.test.ts
New Vitest suite covering seeding, CRUD operations, size limits, scope shadowing, pinned-slot utilities, and reflect behavior including observation-driven updates.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant App as AppStartup
    participant Events as event::session::stopped
    participant Summ as mem::summarize
    participant SlotReflect as mem::slot-reflect
    participant Observations as Observations_KV
    participant Slots as Slots_KV

    Client->>App: start
    App->>Slots: register mem::slot-* endpoints (if AGENTMEMORY_SLOTS)

    Client->>Events: session stops
    Events->>Summ: trigger(mem::summarize)
    Summ-->>Events: summary

    alt AGENTMEMORY_REFLECT enabled
        Events->>SlotReflect: triggerVoid(mem::slot-reflect, {sessionId})
        SlotReflect->>Observations: read compressed observations
        SlotReflect->>SlotReflect: extract TODOs, patterns, touched files
        SlotReflect->>Slots: acquire keyed lock -> update pending_items (truncate to sizeLimit)
        SlotReflect->>Slots: acquire keyed lock -> update session_patterns
        SlotReflect->>Slots: acquire keyed lock -> update project_context
        SlotReflect-->>SlotReflect: record slot_reflect audit
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐇 Burrowed bytes and pinned delights,

Fifty slots to guard the nights,
I nibble logs and stitch the seams,
I watch your TODOs and keep your dreams,
Hooray — I hop, and hold your mem'ry lights.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(slots): editable pinned memory slots + idle reflection + global scope' directly and accurately captures the main features introduced in the changeset: the new memory slots feature with pinned slot support, reflection capability, and global scope handling. The title is specific, concise, and clearly conveys the primary additions.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/memory-slots-v010

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/index.ts (1)

334-335: ⚠️ Potential issue | 🟡 Minor

Stale MCP tool count in startup log.

getAllTools() now returns 50 entries (with V010_SLOTS_TOOLS), and the plugin manifest/README badge were updated to 50, but this startup banner still reports 44.

Proposed fix
-    `[agentmemory] Endpoints: 107 REST + 44 MCP tools + 6 MCP resources + 3 MCP prompts`,
+    `[agentmemory] Endpoints: 107 REST + 50 MCP tools + 6 MCP resources + 3 MCP prompts`,

Based on learnings: "When adding or removing MCP tools, update ALL of: tools-registry.ts, server.ts, triggers/api.ts, index.ts, test/mcp-standalone.test.ts, README.md, and plugin/.claude-plugin/plugin.json".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 334 - 335, The startup banner in index.ts
currently hardcodes "44 MCP tools" which is stale; use the actual MCP tools
count from getAllTools() instead of a magic number. Update the log construction
where the string `[agentmemory] Endpoints: 107 REST + 44 MCP tools + 6 MCP
resources + 3 MCP prompts` is emitted to compute the tools count dynamically
(e.g., call getAllTools() and use its length) or otherwise replace 44 with the
current getAllTools() result so the banner always reflects the real number of
MCP tools.
README.md (1)

596-620: ⚠️ Potential issue | 🟡 Minor

Stale tool counts throughout this section.

The badge on line 28 and the prose on line 596 say "50 tools", but:

  • Line 598: ### 44 Tools
  • Line 620: <summary>Extended tools (44 total — set AGENTMEMORY_TOOLS=all)</summary>

The new memory_slot_* tools (6 total) are also not listed in the extended-tools table, so readers of AGENTMEMORY_TOOLS=all won't discover them from the README. Update the header/summary to 50 and add a short slots row block (or point to V010_SLOTS_TOOLS).

Additionally:

  • Lines 344 and 366 still say "all 43 memory tools" in the OpenClaw and Hermes paste-this prompts.
  • Lines 904 and 924 still say "646 tests" — the PR summary says 800/800 passing.

Based on learnings: "When adding or removing MCP tools, update ALL of: tools-registry.ts, server.ts, triggers/api.ts, index.ts, test/mcp-standalone.test.ts, README.md, and plugin/.claude-plugin/plugin.json".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 596 - 620, Update README.md to reflect the new total
tool and test counts and to list the new slot tools: change the "### 44 Tools"
and the extended-tools summary text ("Extended tools (44 total — set
AGENTMEMORY_TOOLS=all)") to 50, and ensure the top badge/prose and those
summaries are consistent; add a short table row or a collapsible block
describing the six new memory_slot_* tools (or add a pointer to
V010_SLOTS_TOOLS) inside the extended-tools section so they appear when
AGENTMEMORY_TOOLS=all; replace the two occurrences of "all 43 memory tools" in
the OpenClaw and Hermes paste-this prompt blocks with the correct phrase (e.g.,
"all 50 memory tools"); and update the test count strings "646 tests" to "800
tests" (or "800/800 passing") to match the PR summary—search for these literal
strings to find all sites to change.
🧹 Nitpick comments (4)
src/triggers/events.ts (1)

44-50: Use the exported isReflectEnabled() helper instead of re-reading the env directly.

src/functions/slots.ts already exports isReflectEnabled(), which is the source of truth used in src/index.ts. Reading process.env["AGENTMEMORY_REFLECT"] inline here duplicates that check and can drift (e.g., if the helper ever normalizes values or adds a second flag).

Also worth noting: triggerVoid here is truly fire-and-forget and has no .catch() — if the slot-reflect function rejects (or the slots feature is disabled but AGENTMEMORY_REFLECT=true was set), the rejection will surface as an unhandled promise rejection. Consider sdk.triggerVoid(...).catch(() => {}) or attaching a logger.

Proposed fix
+import { isReflectEnabled } from "../functions/slots.js";
 ...
   sdk.registerFunction("event::session::stopped", async (data: { sessionId: string }) => {
     const summary = await sdk.trigger({ function_id: "mem::summarize", payload: data });
-    if (process.env["AGENTMEMORY_REFLECT"] === "true") {
-      sdk.triggerVoid("mem::slot-reflect", { sessionId: data.sessionId });
+    if (isReflectEnabled()) {
+      void Promise.resolve(sdk.triggerVoid("mem::slot-reflect", { sessionId: data.sessionId }))
+        .catch(() => {});
     }
     return summary;
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/triggers/events.ts` around lines 44 - 50, Replace the direct env check in
the "event::session::stopped" handler with the exported helper
isReflectEnabled() from src/functions/slots.ts (i.e., call isReflectEnabled()
instead of reading process.env["AGENTMEMORY_REFLECT"]), and make the
fire-and-forget call to sdk.triggerVoid("mem::slot-reflect", { sessionId:
data.sessionId }) resilient by attaching a .catch(() => {}) or logging the error
so rejections don't become unhandled; keep the rest of the handler (calling
sdk.trigger for "mem::summarize" and returning summary) unchanged.
src/mcp/server.ts (1)

1111-1176: Slot handlers: consistent validation pattern, but consider masking not-found as 404.

Right now, a non-existent label bubbles up through the catch on line 1184 and returns a generic 500 Internal error, which hides the real cause (Slot "X" not found thrown by mem::slot-get / -append / -replace / -delete). Consider returning 404 with the thrown message for slot operations so agents get actionable feedback instead of a 500.

Otherwise, validation + payload construction looks correct and aligns with the slot function signatures in src/functions/slots.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp/server.ts` around lines 1111 - 1176, The slot handlers
(memory_slot_get, memory_slot_append, memory_slot_replace, memory_slot_delete)
currently let "slot not found" errors bubble up and become 500s; change each
handler to catch errors from its sdk.trigger call (the calls to mem::slot-get,
mem::slot-append, mem::slot-replace, mem::slot-delete) and, if the thrown error
message indicates the slot was not found (e.g. contains 'Slot "' or 'not
found'), return status_code 404 with body.error set to the error message;
otherwise rethrow or return the existing 500 behavior so other failures remain
unchanged.
src/functions/slots.ts (1)

138-142: Reuse one timestamp for new slot metadata.

createdAt and updatedAt should be identical for a newly-created slot; capture nowIso() once and reuse it.

Suggested fix
+    const timestamp = nowIso();
     const slot: MemorySlot = {
       ...tmpl,
-      createdAt: nowIso(),
-      updatedAt: nowIso(),
+      createdAt: timestamp,
+      updatedAt: timestamp,
     };
+      const timestamp = nowIso();
       const slot: MemorySlot = {
         label,
         content,
         sizeLimit,
         description,
         pinned: data?.pinned !== false,
         readOnly: false,
         scope,
-        createdAt: nowIso(),
-        updatedAt: nowIso(),
+        createdAt: timestamp,
+        updatedAt: timestamp,
       };

As per coding guidelines, capture timestamps once with new Date().toISOString() and reuse instead of calling Date multiple times.

Also applies to: 227-237

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/slots.ts` around lines 138 - 142, The code calls nowIso() twice
when constructing a new MemorySlot causing createdAt and updatedAt to differ;
capture the timestamp once (e.g., const ts = nowIso() or const now = new
Date().toISOString()) and use that single value for both createdAt and updatedAt
when building the slot object (see MemorySlot/slot creation where createdAt and
updatedAt are set) and apply the same change to the other slot-creation block
later in the file (the similar construct around lines ~227-237).
test/slots.test.ts (1)

128-151: Exercise scope shadowing through the public slot functions.

This test bypasses mem::slot-create, so it won’t catch regressions where creating a project slot with the same label as a global slot is rejected. Use the handlers to set up the override.

Suggested test setup
-    await kv.set(KV.globalSlots, "persona", {
-      label: "persona",
-      content: "global-persona",
-      sizeLimit: 1000,
-      description: "",
-      pinned: true,
-      readOnly: false,
-      scope: "global",
-      createdAt: new Date().toISOString(),
-      updatedAt: new Date().toISOString(),
-    });
-    await kv.set(KV.slots, "persona", {
-      label: "persona",
-      content: "project-override",
-      sizeLimit: 1000,
-      description: "",
-      pinned: true,
-      readOnly: false,
-      scope: "project",
-      createdAt: new Date().toISOString(),
-      updatedAt: new Date().toISOString(),
-    });
+    await handlers["mem::slot-replace"]({
+      label: "persona",
+      content: "global-persona",
+    });
+    const created = (await handlers["mem::slot-create"]({
+      label: "persona",
+      content: "project-override",
+      scope: "project",
+    })) as { success: boolean };
+    expect(created.success).toBe(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/slots.test.ts` around lines 128 - 151, The test currently bypasses the
public API by writing directly to KV.slots/KV.globalSlots; instead create the
slots via the handlers to exercise scope validation—call
handlers["mem::slot-create"] to create the global slot (scope:"global") and
again to create the project-scoped override (scope:"project") with the same
label, then call handlers["mem::slot-get"] to assert the project slot shadows
the global one; this ensures regressions in mem::slot-create scope/rejection
logic are caught rather than masked by direct kv.set usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/functions/slots.ts`:
- Around line 370-408: The code reads slots via readSlot("pending_items") and
readSlot("session_patterns") before taking locks and then writes back via
kv.set, which can cause lost updates; fix by moving the readSlot call inside the
withKeyedLock for "pending_items" (i.e., call readSlot(kv, "pending_items") from
within the async callback passed to withKeyedLock(`slot:pending_items`)) and
also wrap the session_patterns update in a withKeyedLock (e.g.,
withKeyedLock(`slot:session_patterns`, async () => { const { slot, scope } =
await readSlot(kv, "session_patterns"); ... kv.set(scopeKv(scope),
"session_patterns", {...}) })), ensuring you re-read slot content under the lock
before computing fresh/truncated content and before calling kv.set to avoid
stale overwrites.
- Around line 315-323: The delete path must acquire the same slot-level lock
used by writers to avoid the race where mem::slot-delete removes a slot while
mem::slot-append/mem::slot-replace later recreate it; wrap the
readSlot/check/readOnly/kv.delete/recordAudit sequence inside the same slot lock
(the helper used by append/replace, e.g., withSlotLock or acquireSlotLock for
the slot/scope) so you re-check existence/readOnly while holding the lock, then
call kv.delete(scopeKv(scope), label) and recordAudit("slot_delete",
"mem::slot-delete", ...) before releasing the lock.
- Around line 213-221: The create-slot flow uses validateLabel and readSlot but
checks for duplicates across all scopes and silently defaults invalid sizeLimit
values; update the logic so scope is explicitly validated from data.scope
(allowed values "global" or "project") into SlotScope and use that scope when
calling readSlot(kv, label, scope) (or equivalent) so duplicate checks only
apply within the target scope and permit project shadowing of global slots;
change sizeLimit handling in the slot creation path (currently using
DEFAULT_SIZE_LIMIT) to validate that data.sizeLimit is an integer between 1 and
20000 and return a validation error if invalid instead of defaulting; keep
validateLabel usage and ensure all validation failures return { success: false,
error: ... } as before.

In `@src/mcp/server.ts`:
- Around line 1129-1143: The memory_slot_create branch currently only recognizes
the string forms ("false" for args.pinned and "global" for args.scope), so
boolean false/true from clients are ignored; update the checks around
args.pinned and args.scope before building payload so they accept both boolean
and string forms (e.g., treat args.pinned === false || args.pinned === "false"
as payload.pinned = false, and treat args.pinned === true || args.pinned ===
"true" as payload.pinned = true), and similarly treat args.scope === "global" ||
args.scope === true as payload.scope = "global" (or otherwise normalize allowed
scope values) so the payload passed to sdk.trigger({ function_id:
"mem::slot-create", payload }) reflects the intended boolean/string inputs;
adjust logic in the memory_slot_create case to coerce/normalize these fields
rather than only checking for specific string literals.

In `@src/triggers/api.ts`:
- Around line 1319-1329: The handler currently silently drops invalid fields;
instead validate and reject malformed inputs before calling
sdk.trigger("mem::slot-create"): if body["content"] exists and is not a string
return 400; if body["scope"] exists and is not "project" or "global" return 400;
ensure body["pinned"] if present is boolean or return 400; use
parseOptionalPositiveInt(body["sizeLimit"]) but also enforce an upper bound
(20*1024 bytes) returning 400 when exceeded; only populate payload with
validated fields and then call sdk.trigger({ function_id: "mem::slot-create",
payload }).

In `@src/types.ts`:
- Around line 206-216: The MemorySlot interface currently types sizeLimit as
number but slot creation in src/functions/slots.ts may assign null; update the
type or the creator to be consistent: either change MemorySlot.sizeLimit to
number | null to accept null from the create/seed/reflect flows, or modify the
slot creation logic (the function(s) that set sizeLimit in
src/functions/slots.ts) to default to a numeric value (e.g., 2000) so
MemorySlot.sizeLimit remains a non-nullable number; pick one approach and apply
it everywhere (MemorySlot interface and the create/seed/reflect codepaths) so
the runtime shape and the TypeScript type match.

---

Outside diff comments:
In `@README.md`:
- Around line 596-620: Update README.md to reflect the new total tool and test
counts and to list the new slot tools: change the "### 44 Tools" and the
extended-tools summary text ("Extended tools (44 total — set
AGENTMEMORY_TOOLS=all)") to 50, and ensure the top badge/prose and those
summaries are consistent; add a short table row or a collapsible block
describing the six new memory_slot_* tools (or add a pointer to
V010_SLOTS_TOOLS) inside the extended-tools section so they appear when
AGENTMEMORY_TOOLS=all; replace the two occurrences of "all 43 memory tools" in
the OpenClaw and Hermes paste-this prompt blocks with the correct phrase (e.g.,
"all 50 memory tools"); and update the test count strings "646 tests" to "800
tests" (or "800/800 passing") to match the PR summary—search for these literal
strings to find all sites to change.

In `@src/index.ts`:
- Around line 334-335: The startup banner in index.ts currently hardcodes "44
MCP tools" which is stale; use the actual MCP tools count from getAllTools()
instead of a magic number. Update the log construction where the string
`[agentmemory] Endpoints: 107 REST + 44 MCP tools + 6 MCP resources + 3 MCP
prompts` is emitted to compute the tools count dynamically (e.g., call
getAllTools() and use its length) or otherwise replace 44 with the current
getAllTools() result so the banner always reflects the real number of MCP tools.

---

Nitpick comments:
In `@src/functions/slots.ts`:
- Around line 138-142: The code calls nowIso() twice when constructing a new
MemorySlot causing createdAt and updatedAt to differ; capture the timestamp once
(e.g., const ts = nowIso() or const now = new Date().toISOString()) and use that
single value for both createdAt and updatedAt when building the slot object (see
MemorySlot/slot creation where createdAt and updatedAt are set) and apply the
same change to the other slot-creation block later in the file (the similar
construct around lines ~227-237).

In `@src/mcp/server.ts`:
- Around line 1111-1176: The slot handlers (memory_slot_get, memory_slot_append,
memory_slot_replace, memory_slot_delete) currently let "slot not found" errors
bubble up and become 500s; change each handler to catch errors from its
sdk.trigger call (the calls to mem::slot-get, mem::slot-append,
mem::slot-replace, mem::slot-delete) and, if the thrown error message indicates
the slot was not found (e.g. contains 'Slot "' or 'not found'), return
status_code 404 with body.error set to the error message; otherwise rethrow or
return the existing 500 behavior so other failures remain unchanged.

In `@src/triggers/events.ts`:
- Around line 44-50: Replace the direct env check in the
"event::session::stopped" handler with the exported helper isReflectEnabled()
from src/functions/slots.ts (i.e., call isReflectEnabled() instead of reading
process.env["AGENTMEMORY_REFLECT"]), and make the fire-and-forget call to
sdk.triggerVoid("mem::slot-reflect", { sessionId: data.sessionId }) resilient by
attaching a .catch(() => {}) or logging the error so rejections don't become
unhandled; keep the rest of the handler (calling sdk.trigger for
"mem::summarize" and returning summary) unchanged.

In `@test/slots.test.ts`:
- Around line 128-151: The test currently bypasses the public API by writing
directly to KV.slots/KV.globalSlots; instead create the slots via the handlers
to exercise scope validation—call handlers["mem::slot-create"] to create the
global slot (scope:"global") and again to create the project-scoped override
(scope:"project") with the same label, then call handlers["mem::slot-get"] to
assert the project slot shadows the global one; this ensures regressions in
mem::slot-create scope/rejection logic are caught rather than masked by direct
kv.set usage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae8a1faa-d3e9-48f2-9324-fd2c539db492

📥 Commits

Reviewing files that changed from the base of the PR and between 4036640 and d280ea0.

📒 Files selected for processing (11)
  • README.md
  • plugin/.claude-plugin/plugin.json
  • src/functions/slots.ts
  • src/index.ts
  • src/mcp/server.ts
  • src/mcp/tools-registry.ts
  • src/state/schema.ts
  • src/triggers/api.ts
  • src/triggers/events.ts
  • src/types.ts
  • test/slots.test.ts

Comment thread src/functions/slots.ts Outdated
Comment thread src/functions/slots.ts Outdated
Comment thread src/functions/slots.ts Outdated
Comment thread src/mcp/server.ts
Comment thread src/triggers/api.ts Outdated
Comment thread src/types.ts
rohitg00 added a commit that referenced this pull request Apr 21, 2026
## Concurrency

- mem::slot-create now wraps the duplicate check + set + audit inside
  withKeyedLock. Previously a racing slot-append could create a slot
  that slot-create assumed was absent.
- mem::slot-delete acquires the same slot-level lock writers use, so
  an in-flight append/replace cannot resurrect a deleted slot.
- mem::slot-reflect now reads pending_items / session_patterns /
  project_context INSIDE their per-slot locks, computes the new
  content under the lock, and writes back under the same lock.
  The old path read-then-locked-then-wrote, which could clobber a
  concurrent slot-replace on the same label.

## Scope + validation

- mem::slot-create duplicate check is now scope-local via
  readSlotInScope(). A project slot with the same label as a global
  slot is allowed and shadows the global on read (matches the read
  precedence). Before, create rejected any label that already existed
  in either scope.
- validateScope() returns null on unknown scope values; handler
  returns "scope must be 'project' or 'global'" 400 instead of
  silently defaulting.
- validateSizeLimit() returns null on non-integers, negatives, or
  values > 20000; handler returns "sizeLimit must be an integer
  between 1 and 20000" 400 instead of silently falling back to
  DEFAULT_SIZE_LIMIT.
- Timestamp captured once (ts) then used for createdAt + updatedAt,
  both in the create path and in seedDefaults.

## HTTP surface

- api::slot-create now rejects bad types up front:
  content / description must be strings, pinned must be a boolean,
  scope must be "project" or "global", sizeLimit must be a positive
  integer <= 20000. Malformed inputs get a 400 with a specific error
  message instead of being silently dropped.

## MCP surface

- memory_slot_create accepts both boolean and string-boolean forms
  for pinned (MCP clients bind either depending on their schema
  wrapper). Same for scope, which now also accepts explicit "project".

## Other cleanup

- event::session::stopped uses isReflectEnabled() helper instead of
  poking process.env directly. triggerVoid call wrapped in try/catch
  with a warn log so a reflect failure doesn't surface as an
  unhandled rejection.
- src/index.ts boot banner computes MCP tool count via
  getAllTools().length instead of hardcoding 44 — banner now tracks
  the actual registry size.
- README counts bumped: "all 43 memory tools" → "all 50 memory tools"
  (x2), "### 44 Tools" → "### 50 Tools", "Extended tools (44 total" →
  "(50 total", "646 tests" → "800 tests" (x2).

## Tests

- test/slots.test.ts: shadow test now goes through the public handler
  API instead of direct kv.set. Two new cases: invalid sizeLimit
  (over max, negative, non-integer) rejected; unknown scope rejected.
- Full suite: 802/802 pass (+2 new).
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

619-656: ⚠️ Potential issue | 🟡 Minor

Extended tools list doesn't include the new memory_slot_* tools (or several other recent additions).

The badge and configuration blocks now advertise “50 tools / AGENTMEMORY_TOOLS=all (50 tools)” and mention memory_slot_* by name, but the <details> table under "Extended tools (50 total — set AGENTMEMORY_TOOLS=all)" still shows the old entries and omits memory_slot_list, memory_slot_get, memory_slot_create, memory_slot_append, memory_slot_replace, memory_slot_delete (and, pre-existing gap: memory_lesson_save, memory_lesson_recall, memory_reflect, memory_insight_list, memory_obsidian_export). Users enabling the slots feature won’t find them documented here.

♻️ Suggested additions
 | `memory_verify` | Trace provenance |
+| `memory_slot_list` | List memory slots (merged global + project) |
+| `memory_slot_get` | Read a slot by label |
+| `memory_slot_create` | Create a new slot (requires `AGENTMEMORY_SLOTS=true`) |
+| `memory_slot_append` | Append text to a slot |
+| `memory_slot_replace` | Replace a slot's content |
+| `memory_slot_delete` | Delete a non-readOnly slot |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 619 - 656, Update the "Extended tools (50 total — set
AGENTMEMORY_TOOLS=all)" details table to include the missing memory_slot_*
entries (memory_slot_list, memory_slot_get, memory_slot_create,
memory_slot_append, memory_slot_replace, memory_slot_delete) plus the other
omitted tools (memory_lesson_save, memory_lesson_recall, memory_reflect,
memory_insight_list, memory_obsidian_export), and ensure the summary/badge count
and the parentheses after "AGENTMEMORY_TOOLS=all" reflect the new total; modify
the table rows (the | Tool | Description | entries) to add one line per missing
tool with concise descriptions and update the header text "Extended tools (50
total — set AGENTMEMORY_TOOLS=all)" to the correct total and matching string.
🧹 Nitpick comments (1)
src/functions/slots.ts (1)

154-167: Parallelize default-slot seeding.

Eight independent kv.get + kv.set pairs run serially on startup. Per repo guidelines, independent KV writes/reads should use Promise.all.

♻️ Proposed refactor
-async function seedDefaults(kv: StateKV): Promise<void> {
-  const ts = nowIso();
-  for (const tmpl of DEFAULT_SLOTS) {
-    const target = scopeKv(tmpl.scope);
-    const existing = await kv.get<MemorySlot>(target, tmpl.label);
-    if (existing) continue;
-    const slot: MemorySlot = {
-      ...tmpl,
-      createdAt: ts,
-      updatedAt: ts,
-    };
-    await kv.set(target, tmpl.label, slot);
-  }
-}
+async function seedDefaults(kv: StateKV): Promise<void> {
+  const ts = nowIso();
+  await Promise.all(
+    DEFAULT_SLOTS.map(async (tmpl) => {
+      const target = scopeKv(tmpl.scope);
+      const existing = await kv.get<MemorySlot>(target, tmpl.label);
+      if (existing) return;
+      await kv.set(target, tmpl.label, { ...tmpl, createdAt: ts, updatedAt: ts });
+    }),
+  );
+}

As per coding guidelines: "Use parallel operations where possible with Promise.all for independent kv writes/reads".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/slots.ts` around lines 154 - 167, seedDefaults currently runs
kv.get and kv.set sequentially for each entry in DEFAULT_SLOTS; change it to run
each slot's work in parallel by mapping DEFAULT_SLOTS to an array of async tasks
(each task computes target via scopeKv(tmpl.scope), does the
kv.get<MemorySlot>(target, tmpl.label) and if missing does kv.set(target,
tmpl.label, slot) with createdAt/updatedAt timestamps) and then await
Promise.all(tasks). Keep the same variable names (seedDefaults, DEFAULT_SLOTS,
scopeKv, kv.get, kv.set) and ensure the timestamp is computed per slot or shared
before Promise.all as appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/functions/slots.ts`:
- Around line 384-397: The reflect heuristics are doing sloppy substring
matches: replace the naive narrative/title check (narrative.includes("todo") ||
title.includes("todo")) inside the mem::slot-reflect loop with a
word-boundary/regex test (e.g. /\b(todo|fixme)\b/i) so only real TODO/FIXME
markers are considered before pushing into pendingLines; and fix the
duplicate-file logic that uses already.includes(f) by splitting the multi-line
already string into exact lines (already.split(/\r?\n/)), checking exact
equality against those lines (not substring), and only appending to
project_context when no exact match is found to avoid false dedupes like
src/a.ts vs src/a.tsx.

---

Outside diff comments:
In `@README.md`:
- Around line 619-656: Update the "Extended tools (50 total — set
AGENTMEMORY_TOOLS=all)" details table to include the missing memory_slot_*
entries (memory_slot_list, memory_slot_get, memory_slot_create,
memory_slot_append, memory_slot_replace, memory_slot_delete) plus the other
omitted tools (memory_lesson_save, memory_lesson_recall, memory_reflect,
memory_insight_list, memory_obsidian_export), and ensure the summary/badge count
and the parentheses after "AGENTMEMORY_TOOLS=all" reflect the new total; modify
the table rows (the | Tool | Description | entries) to add one line per missing
tool with concise descriptions and update the header text "Extended tools (50
total — set AGENTMEMORY_TOOLS=all)" to the correct total and matching string.

---

Nitpick comments:
In `@src/functions/slots.ts`:
- Around line 154-167: seedDefaults currently runs kv.get and kv.set
sequentially for each entry in DEFAULT_SLOTS; change it to run each slot's work
in parallel by mapping DEFAULT_SLOTS to an array of async tasks (each task
computes target via scopeKv(tmpl.scope), does the kv.get<MemorySlot>(target,
tmpl.label) and if missing does kv.set(target, tmpl.label, slot) with
createdAt/updatedAt timestamps) and then await Promise.all(tasks). Keep the same
variable names (seedDefaults, DEFAULT_SLOTS, scopeKv, kv.get, kv.set) and ensure
the timestamp is computed per slot or shared before Promise.all as appropriate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 83606e49-2539-4a86-b15d-6d63c02a1a29

📥 Commits

Reviewing files that changed from the base of the PR and between d280ea0 and b813d30.

📒 Files selected for processing (7)
  • README.md
  • src/functions/slots.ts
  • src/index.ts
  • src/mcp/server.ts
  • src/triggers/api.ts
  • src/triggers/events.ts
  • test/slots.test.ts
✅ Files skipped from review due to trivial changes (1)
  • test/slots.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/triggers/events.ts
  • src/index.ts
  • src/triggers/api.ts

Comment thread src/functions/slots.ts
Comment on lines +384 to +397
for (const obs of recent) {
const title = (obs.title || "").toLowerCase();
const narrative = (obs.narrative || "").toLowerCase();
if (narrative.includes("todo") || title.includes("todo")) {
pendingLines.push(`- ${obs.title || obs.id}`);
}
if (obs.type === "error") {
patternCounts.set("errors", (patternCounts.get("errors") ?? 0) + 1);
}
if (obs.type === "command_run") {
patternCounts.set("commands", (patternCounts.get("commands") ?? 0) + 1);
}
if (obs.files) for (const f of obs.files) files.add(f);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reflect heuristics have sloppy substring matches that will misclassify observations.

Two spots silently produce false positives/negatives in mem::slot-reflect:

  1. Line 387: narrative.includes("todo") || title.includes("todo") matches any substring — "todoist", "mastodon", "stodolist", words mid-sentence — and pushes them into pending_items. Use a word-boundary match (or a structured TODO signal) so only real TODO/FIXME markers land in pending.
  2. Lines 450–452 (same function): already.includes(f) treats file paths as substrings — once src/a.ts is recorded, src/a.ts.bak and src/a.tsx are incorrectly considered already-present and never appended to project_context. Split already on newlines (matching the format written at line 457–459) and dedupe on exact line equality.
🩹 Proposed fix
-        if (narrative.includes("todo") || title.includes("todo")) {
+        if (/\btodo\b|\bfixme\b/i.test(narrative) || /\btodo\b|\bfixme\b/i.test(title)) {
           pendingLines.push(`- ${obs.title || obs.id}`);
         }
@@
-          const already = slot.content;
-          const fresh = Array.from(files)
-            .filter((f) => !already.includes(f))
-            .slice(0, 20);
+          const already = slot.content;
+          const alreadyFiles = new Set(
+            already.split("\n").map((l) => l.replace(/^- /, "").trim()).filter(Boolean),
+          );
+          const fresh = Array.from(files)
+            .filter((f) => !alreadyFiles.has(f))
+            .slice(0, 20);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/functions/slots.ts` around lines 384 - 397, The reflect heuristics are
doing sloppy substring matches: replace the naive narrative/title check
(narrative.includes("todo") || title.includes("todo")) inside the
mem::slot-reflect loop with a word-boundary/regex test (e.g.
/\b(todo|fixme)\b/i) so only real TODO/FIXME markers are considered before
pushing into pendingLines; and fix the duplicate-file logic that uses
already.includes(f) by splitting the multi-line already string into exact lines
(already.split(/\r?\n/)), checking exact equality against those lines (not
substring), and only appending to project_context when no exact match is found
to avoid false dedupes like src/a.ts vs src/a.tsx.

…bal scope

Introduces memory slots as a new first-class memory primitive, orthogonal
to observations / memories / graph. A slot is a labelled, size-limited,
agent-editable chunk of context. 8 defaults seed on first run
(persona, user_preferences, tool_guidelines, project_context, guidance,
pending_items, session_patterns, self_notes). Agent reads/writes via
MCP tools; pinned slots serialise cleanly into SessionStart context.

Functions (`src/functions/slots.ts`):
- mem::slot-list
- mem::slot-get
- mem::slot-create
- mem::slot-append   (refuses writes over sizeLimit)
- mem::slot-replace  (refuses content over sizeLimit)
- mem::slot-delete
- mem::slot-reflect  (used by idle reflection)

REST (`src/triggers/api.ts`):
- GET    /agentmemory/slots
- GET    /agentmemory/slot?label=...
- POST   /agentmemory/slot
- POST   /agentmemory/slot/append
- POST   /agentmemory/slot/replace
- DELETE /agentmemory/slot?label=...
- POST   /agentmemory/slot/reflect

MCP tools (`src/mcp/tools-registry.ts`, `src/mcp/server.ts`):
- memory_slot_list
- memory_slot_get
- memory_slot_create
- memory_slot_append
- memory_slot_replace
- memory_slot_delete

event::session::stopped fires mem::slot-reflect via triggerVoid when
AGENTMEMORY_REFLECT=true. The reflector reads the last N observations,
extracts TODO-flavoured titles into pending_items, counts error /
command types into session_patterns, and records touched files in
project_context. Fire-and-forget: does not block summarise.

Slots carry a scope field ("project" | "global"). Global slots live at
KV.globalSlots ("mem:slots:global") — one brain across all projects for
things like persona / user_preferences / tool_guidelines. Project-
scoped slot with the same label shadows the global one on read.

- AGENTMEMORY_SLOTS=true    — enable the whole primitive
- AGENTMEMORY_REFLECT=true  — enable Stop-hook reflection (requires SLOTS)

Both default OFF. Matches the opt-in convention of auto-compress (#138),
inject-context (#143), vision embeddings (#179).

44 MCP tools → 50 across README (stats badge, install instructions,
config block) and plugin/.claude-plugin/plugin.json description. New
AGENTMEMORY_SLOTS and AGENTMEMORY_REFLECT entries in the config block.

test/slots.test.ts — 12 new cases: default seeding by scope, label
validation, create/get/append/replace/delete round-trip, duplicate-
create rejection, sizeLimit enforcement on append + replace, project
shadows global, listPinnedSlots + renderPinnedContext, reflect no-op
on empty session, reflect updates pending_items / session_patterns /
project_context from TODO + error + file signals.

Full suite: 800/800 pass (+12 new). Build clean.
## Concurrency

- mem::slot-create now wraps the duplicate check + set + audit inside
  withKeyedLock. Previously a racing slot-append could create a slot
  that slot-create assumed was absent.
- mem::slot-delete acquires the same slot-level lock writers use, so
  an in-flight append/replace cannot resurrect a deleted slot.
- mem::slot-reflect now reads pending_items / session_patterns /
  project_context INSIDE their per-slot locks, computes the new
  content under the lock, and writes back under the same lock.
  The old path read-then-locked-then-wrote, which could clobber a
  concurrent slot-replace on the same label.

## Scope + validation

- mem::slot-create duplicate check is now scope-local via
  readSlotInScope(). A project slot with the same label as a global
  slot is allowed and shadows the global on read (matches the read
  precedence). Before, create rejected any label that already existed
  in either scope.
- validateScope() returns null on unknown scope values; handler
  returns "scope must be 'project' or 'global'" 400 instead of
  silently defaulting.
- validateSizeLimit() returns null on non-integers, negatives, or
  values > 20000; handler returns "sizeLimit must be an integer
  between 1 and 20000" 400 instead of silently falling back to
  DEFAULT_SIZE_LIMIT.
- Timestamp captured once (ts) then used for createdAt + updatedAt,
  both in the create path and in seedDefaults.

## HTTP surface

- api::slot-create now rejects bad types up front:
  content / description must be strings, pinned must be a boolean,
  scope must be "project" or "global", sizeLimit must be a positive
  integer <= 20000. Malformed inputs get a 400 with a specific error
  message instead of being silently dropped.

## MCP surface

- memory_slot_create accepts both boolean and string-boolean forms
  for pinned (MCP clients bind either depending on their schema
  wrapper). Same for scope, which now also accepts explicit "project".

## Other cleanup

- event::session::stopped uses isReflectEnabled() helper instead of
  poking process.env directly. triggerVoid call wrapped in try/catch
  with a warn log so a reflect failure doesn't surface as an
  unhandled rejection.
- src/index.ts boot banner computes MCP tool count via
  getAllTools().length instead of hardcoding 44 — banner now tracks
  the actual registry size.
- README counts bumped: "all 43 memory tools" → "all 50 memory tools"
  (x2), "### 44 Tools" → "### 50 Tools", "Extended tools (44 total" →
  "(50 total", "646 tests" → "800 tests" (x2).

## Tests

- test/slots.test.ts: shadow test now goes through the public handler
  API instead of direct kv.set. Two new cases: invalid sizeLimit
  (over max, negative, non-integer) rejected; unknown scope rejected.
- Full suite: 802/802 pass (+2 new).
@rohitg00 rohitg00 force-pushed the feat/memory-slots-v010 branch from b813d30 to 02d13c5 Compare April 21, 2026 18:54
@rohitg00 rohitg00 merged commit 480d8e8 into main Apr 21, 2026
4 of 5 checks passed
@rohitg00 rohitg00 deleted the feat/memory-slots-v010 branch April 21, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant