Skip to content

fix(slots): wire pinned slot injection into mem::context#288

Merged
rohitg00 merged 2 commits into
rohitg00:mainfrom
wyh0626:fix/286-slot-injection
May 11, 2026
Merged

fix(slots): wire pinned slot injection into mem::context#288
rohitg00 merged 2 commits into
rohitg00:mainfrom
wyh0626:fix/286-slot-injection

Conversation

@wyh0626
Copy link
Copy Markdown
Contributor

@wyh0626 wyh0626 commented May 11, 2026

Summary

Closes #286. renderPinnedContext and listPinnedSlots were introduced in #182 and tested in test/slots.test.ts, but no code path actually invokes them. mem::context — the function /agentmemory/session/start calls and that session-start.mjs reads back into Claude Code — reads profile / sessions / observations but never reads slots, so pinned content stays in KV and never reaches the conversation.

#182's description hedged with "pinned slots can be injected into SessionStart context" and "pinned slots are candidates for SessionStart injection". The helpers and unit tests landed; the wiring didn't.

Fix

Add the listPinnedSlotsrenderPinnedContext call at the top of mem::context, gated by isSlotsEnabled():

if (isSlotsEnabled()) {
  const slotContent = renderPinnedContext(
    await listPinnedSlots(kv).catch(() => []),
  );
  if (slotContent) {
    blocks.push({
      type: "memory",
      content: slotContent,
      tokens: estimateTokens(slotContent),
      recency: Date.now(),
    });
  }
}

recency: Date.now() so the pinned block sorts to the top of the budget-bounded selection. Mirrors how isGraphExtractionEnabled() guards mem::graph-extract after #215.

AGENTMEMORY_SLOTS=false (the default) and AGENTMEMORY_INJECT_CONTEXT=false (also the default) both keep the existing behaviour untouched — the gate composition matches the established opt-in convention (#138, #143, #160, #179).

On cross-project visibility

This PR uses the same listPinnedSlots(kv) reader as the existing mem::slot-list / memory_slot_list / memory_slot_get APIs and the mem::slot-reflect writer. Project-scoped slot storage (KV.slots = "mem:slots") is keyed by label without a project prefix today, so cross-project visibility already exists via the slot list/get tools and via reflect. This PR doesn't change the underlying isolation model; if tighter per-project keying is wanted, that's an orthogonal concern in the slot storage layer.

Verify

# server: AGENTMEMORY_SLOTS=true AGENTMEMORY_INJECT_CONTEXT=true
curl -X POST localhost:3111/agentmemory/slot/replace \
  -H 'Content-Type: application/json' \
  -d '{"label":"tool_guidelines","content":"rule-alpha"}'

curl -X POST localhost:3111/agentmemory/session/start \
  -H 'Content-Type: application/json' \
  -d '{"sessionId":"s1","project":"/tmp","cwd":"/tmp"}'

Before: "context": "" (0 chars).
After: context contains the tool_guidelines slot wrapped in <agentmemory-context>.

Tests

6 cases in test/context-slots.test.ts:

  • pinned global slot lands in returned context
  • multiple pinned slots render in label-sorted order
  • unpinned slots are excluded even with content
  • empty pinned slots (the seeded defaults) are excluded
  • project-scoped slot shadows global slot with the same label
  • AGENTMEMORY_SLOTS=off path emits nothing

npm test for the file: 6/6 pass. Pre-existing failures in test/mcp-standalone.test.ts and test/fs-watcher.test.ts are unaffected by this change.

Summary by CodeRabbit

Release Notes

  • New Features

    • Support for pinned slots in context memory: when enabled, pinned slot content is injected into memory blocks, sorted by label, with project-scoped slots overriding global ones. If disabled, no pinned content is included.
  • Tests

    • Added tests verifying pinned slot injection behavior across configurations and precedence rules.

Review Change Stack

`renderPinnedContext` and `listPinnedSlots` were introduced in rohitg00#182 and tested
in `test/slots.test.ts`, but no code path actually invokes them. `mem::context`
(which `/agentmemory/session/start` calls, and which the `session-start.mjs`
hook reads back to populate Claude Code's SessionStart context) reads profile
/ sessions / observations but never reads slots — so pinned content stays in
KV and never reaches the conversation.

rohitg00#182's description hedged with "pinned slots **can be** injected into
SessionStart context" and described pinned slots as "**candidates for**
SessionStart injection". The helpers and tests for them landed; the wiring
did not.

Reflection has the same shape of problem: `mem::slot-reflect` writes to
`pending_items` / `session_patterns` / `project_context` on the Stop hook,
but the next session never reads those slots, so the reflect → next-session
loop is open.

This adds the missing call at the top of `mem::context`, gated by
`isSlotsEnabled()` so behaviour for `AGENTMEMORY_SLOTS=false` users is
unchanged. Slot content is pushed as a `ContextBlock` with
`recency = Date.now()` so pinned content ranks first in the budget-bounded
selection — "always current" semantics. The existing `INJECT_CONTEXT` gate
in `session-start.mjs` still decides whether the result actually reaches
Claude Code stdout, so default-off users are unaffected on both flags.

`test/context-slots.test.ts` covers:

- pinned global slot lands in returned context
- multiple pinned slots render in label-sorted order
- unpinned slots are excluded even when they have content
- empty pinned slots (the seeded defaults) are excluded
- project-scoped slot shadows global slot with the same label
- AGENTMEMORY_SLOTS=off path emits nothing

Signed-off-by: wyh0626 <44987669+wyh0626@users.noreply.github.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

@wyh0626 is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4c88aba6-62d2-47a0-8eb2-b0ed1d352aba

📥 Commits

Reviewing files that changed from the base of the PR and between 0572cbb and b22e256.

📒 Files selected for processing (1)
  • src/functions/context.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/functions/context.ts

📝 Walkthrough

Walkthrough

Conditionally fetches and renders pinned slot content into the mem::context output when AGENTMEMORY_SLOTS is enabled; adds a memory block with token count and recency only if rendered content is non-empty. Tests verify enabled/disabled behavior and precedence between project and global slots.

Changes

Pinned Slots Context Integration

Layer / File(s) Summary
Slot Feature Imports
src/functions/context.ts
Adds imports for isSlotsEnabled, listPinnedSlots, and renderPinnedContext to enable conditional pinned-slot context generation.
Pinned Slots Context Generation
src/functions/context.ts
mem::context handler now checks if slots are enabled, fetches pinned slot data from KV, renders it as a single context block, and adds it to memory blocks with token estimation and current timestamp if content is non-empty.
Test Infrastructure and Mocking
test/context-slots.test.ts
Establishes in-memory KV mock and registers mem::context handler for isolated testing of pinned slot injection behavior.
Test Fixture Helpers
test/context-slots.test.ts
Provides seedPinnedSlot helper function to seed consistent pinned KV slot records into global or project-scoped storage with standard metadata (pinned/readOnly flags, timestamps, limits).
Pinned Slots Enabled Tests
test/context-slots.test.ts
Verifies that when AGENTMEMORY_SLOTS=true, context includes pinned global slot content sorted by label, excludes unpinned and empty slots, and applies project-scoped slot precedence over global slots when labels collide.
Pinned Slots Disabled Tests
test/context-slots.test.ts
Confirms backward compatibility: when AGENTMEMORY_SLOTS is absent or false, context returns without any injected slot content.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

Possibly related PRs

  • rohitg00/agentmemory#182: Introduced the slots subsystem functions (isSlotsEnabled, listPinnedSlots, renderPinnedContext) that this PR integrates into mem::context.

Poem

I nibble through code, a rabbit so spry,
Pinned memories gathered where tokens lie.
When the flag blooms, I stitch them in place,
Project-first whispers, global finds their space.
Hooray for context — hopping with grace. 🐇

🚥 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 title 'fix(slots): wire pinned slot injection into mem::context' clearly summarizes the main change: integrating slot injection into the mem::context handler. It is concise, specific, and directly reflects the core modification across both the implementation and test files.
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 unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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
Contributor

@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.

🧹 Nitpick comments (1)
src/functions/context.ts (1)

41-52: ⚡ Quick win

Consider parallelizing independent KV reads.

The listPinnedSlots call and the profile fetch (line 54-56) are independent operations reading from different KV scopes. Per the coding guideline, these could be executed in parallel using Promise.all to reduce latency.

⚡ Proposed refactor to parallelize KV reads
-    if (isSlotsEnabled()) {
-      const pinned = await listPinnedSlots(kv).catch(() => []);
-      const slotContent = renderPinnedContext(pinned);
-      if (slotContent) {
-        blocks.push({
-          type: "memory",
-          content: slotContent,
-          tokens: estimateTokens(slotContent),
-          recency: Date.now(),
-        });
-      }
-    }
-
-    const profile = await kv
-      .get<ProjectProfile>(KV.profiles, data.project)
-      .catch(() => null);
+    const [pinnedSlots, profile] = await Promise.all([
+      isSlotsEnabled() 
+        ? listPinnedSlots(kv).catch(() => [])
+        : Promise.resolve([]),
+      kv.get<ProjectProfile>(KV.profiles, data.project).catch(() => null),
+    ]);
+
+    if (pinnedSlots.length > 0) {
+      const slotContent = renderPinnedContext(pinnedSlots);
+      if (slotContent) {
+        blocks.push({
+          type: "memory",
+          content: slotContent,
+          tokens: estimateTokens(slotContent),
+          recency: Date.now(),
+        });
+      }
+    }
+
     if (profile) {

As per coding guidelines: "Use parallel operations where possible with Promise.all for independent kv writes/reads" for files in src/functions/**/*.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/functions/context.ts` around lines 41 - 52, The two independent KV reads
(listPinnedSlots and the profile fetch used just after this block) should run in
parallel using Promise.all to reduce latency: call
Promise.all([listPinnedSlots(kv), fetchProfile(kvOrScope)]) (replace
fetchProfile with the actual profile-fetch function used in the file), then
destructure the results into pinned and profile; keep the existing fallback for
pinned (empty array) and handle a missing profile as before, then call
renderPinnedContext(pinned) and push the memory block if slotContent exists;
ensure you preserve estimateTokens(slotContent) and recency timestamp and
maintain any existing error handling for each read (e.g., map the pinned promise
to .catch(() => []) if you want that specific fallback).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/functions/context.ts`:
- Around line 41-52: The two independent KV reads (listPinnedSlots and the
profile fetch used just after this block) should run in parallel using
Promise.all to reduce latency: call Promise.all([listPinnedSlots(kv),
fetchProfile(kvOrScope)]) (replace fetchProfile with the actual profile-fetch
function used in the file), then destructure the results into pinned and
profile; keep the existing fallback for pinned (empty array) and handle a
missing profile as before, then call renderPinnedContext(pinned) and push the
memory block if slotContent exists; ensure you preserve
estimateTokens(slotContent) and recency timestamp and maintain any existing
error handling for each read (e.g., map the pinned promise to .catch(() => [])
if you want that specific fallback).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c97c886-7838-44a3-81bd-989a6f59df9c

📥 Commits

Reviewing files that changed from the base of the PR and between ec84ff8 and 0572cbb.

📒 Files selected for processing (2)
  • src/functions/context.ts
  • test/context-slots.test.ts

Per CodeRabbit feedback on rohitg00#288: `listPinnedSlots` and the profile fetch read
independent KV scopes, so the project's `src/functions/**` guideline asks for
`Promise.all`. When `AGENTMEMORY_SLOTS=false` the slot side short-circuits to
`Promise.resolve([])`, so the existing flag gate is preserved and no extra
KV traffic is introduced for opt-out users.

Signed-off-by: wyh0626 <44987669+wyh0626@users.noreply.github.com>
@rohitg00 rohitg00 merged commit ccf4409 into rohitg00:main May 11, 2026
1 of 2 checks passed
@rohitg00
Copy link
Copy Markdown
Owner

Thanks @wyh0626

rohitg00 added a commit that referenced this pull request May 11, 2026
…290)

Two field-reported fixes landed since v0.9.8:

- #288 (closes #286) — wires pinned memory slots into mem::context so
  agentmemory pinned-slot content actually reaches SessionStart. The
  renderPinnedContext / listPinnedSlots helpers from #182 had zero
  callers; this lands the wiring behind isSlotsEnabled().
- #289 (closes #285) — MiniMax provider now reads MINIMAX_BASE_URL via
  the shared getEnvVar() loader (so ~/.agentmemory/.env values get
  picked up), and the default endpoint is bumped from the stale
  api.minimaxi.com to api.minimax.io/anthropic per MiniMax's current
  Anthropic-compatible docs.

Bumping 0.9.8 -> 0.9.9 across the 8 standard files (package.json,
packages/mcp/package.json, plugin/.claude-plugin/plugin.json,
src/version.ts, src/types.ts ExportData literal,
src/functions/export-import.ts supportedVersions, the export
round-trip test expectation, and CHANGELOG.md).

868 / 868 tests pass.
rohitg00 added a commit that referenced this pull request May 12, 2026
…, #301) (#304)

@flamerged reported three issues on a real production deployment:

1. (#301) v0.9.7's working-directory fix moved iii-config paths from
   ./data/... to /data/... so the named volume mount is actually
   reached. But iiidev/iii is distroless and runs as UID 65532, while
   `docker volume create` initializes the named volume mountpoint as
   root:root mode 755. Engine writes fail Permission denied (os error
   13), the API silently buffers in RAM, every API call returns
   success, and state evaporates on every container restart — exactly
   what 0.9.7 set out to fix.

   Fix: ship a one-shot iii-init service in docker-compose.yml
   (busybox:1.36, ~4MB, exits in <100ms) that chowns /data to
   65532:65532. iii-engine now has user: "65532:65532" and
   depends_on.iii-init.condition: service_completed_successfully.
   Verified live: pre-fix volume stayed 4.0K after API writes; post-
   fix volume grows to 44K with state_store.db/mem%3A*.bin files
   written through the named volume.

2. (#299) src/viewer/index.html ports detection hardcoded ':3113' as
   the fallback when window.location.port is empty (page served on
   80/443 behind a reverse proxy). Every browser-side /agentmemory/*
   fetch went to <host>:3113, which is typically loopback-only on the
   self-hosted shape — the dashboard rendered cleanly but every
   panel showed the empty "first run" state.

   Fix: when neither ?port=N nor window.location.port is set, use
   window.location.origin as the REST base and window.location.host
   for the WebSocket URL — same-origin path works for both REST and
   live updates. Explicit ?port=N / non-default window.location.port
   paths unchanged.

3. (bundled) mem::context budget loop used `break` on first oversized
   block. With #288's new pinned-slot injection sorting first via
   recency: Date.now(), one fat pinned slot could starve every
   smaller block downstream that would have fit. Switched to
   `continue` so smaller blocks still slip into remaining budget.
   Total tokens still bounded by tokenBudget; only composition under
   contention changes.

Bumping 0.9.9 -> 0.9.10 across the 8 standard files (package.json,
packages/mcp/package.json, plugin/.claude-plugin/plugin.json,
src/version.ts, src/types.ts ExportData literal,
src/functions/export-import.ts supportedVersions, the export
round-trip test expectation, and CHANGELOG.md).

868 / 868 tests pass. Build clean. Volume + viewer fixes verified
end-to-end live.
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.

Pinned slot content never reaches SessionStart context — renderPinnedContext has zero callers

2 participants