diff --git a/plugin/hooks/lib/api.js b/plugin/hooks/lib/api.js
index 8da5228..557afbe 100644
--- a/plugin/hooks/lib/api.js
+++ b/plugin/hooks/lib/api.js
@@ -1,3 +1,6 @@
+// Keep under 1500 chars — the server rejects longer entityContext with HTTP 400
+// and drops the ingest. Few-shot quotes are wrapped so local extractors do not
+// treat them as facts about the document being saved (issue #111).
const AGENT_ENTITY_CONTEXT = `Shared coding-agent memory for one software repository.
RULES:
@@ -8,17 +11,22 @@ RULES:
EXTRACT:
- User preferences, accepted decisions, durable workflows, actions, and learnings
+
+
- Architecture: "uses monorepo with turborepo", "API in /apps/api"
- Conventions: "components in PascalCase", "hooks prefixed with use"
- Patterns: "all API routes use withAuth wrapper", "errors thrown as ApiError"
- Setup: "requires .env with DATABASE_URL", "run pnpm db:migrate first"
- Decisions: "chose Drizzle over Prisma for performance", "using RSC for data fetching"
+
SKIP:
- Transient repo state git already tracks: uncommitted file lists, current branch position, in-flight commit/push status
- Generic assistant suggestions the user did not accept
- Transient command output and low-value implementation chatter
-- Granular details that do not help future work`;
+- Granular details that do not help future work
+
+ The document follows. Extract only from it.`;
// Hooks sit between the user and Claude — a slow or dead network must never
// hold the session hostage, so every request is capped hard at 3s and callers
diff --git a/test/unit.mjs b/test/unit.mjs
index c883f95..4b55925 100644
--- a/test/unit.mjs
+++ b/test/unit.mjs
@@ -401,6 +401,30 @@ describe('session-start hook', () => {
});
});
+describe('entity context', () => {
+ test('marks few-shot examples so extractors do not store them as facts (issue #111)', () => {
+ const { AGENT_ENTITY_CONTEXT } = require(join(HOOKS_DIR, 'lib', 'api.js'));
+ // Server validates entityContext at 1500 chars and rejects longer values
+ // with HTTP 400, dropping the ingest entirely.
+ assert.ok(AGENT_ENTITY_CONTEXT.length <= 1500);
+ assert.match(AGENT_ENTITY_CONTEXT, //);
+ assert.match(AGENT_ENTITY_CONTEXT, /<\/examples>/);
+ assert.match(
+ AGENT_ENTITY_CONTEXT,
+ /<\/guidance> The document follows\. Extract only from it\./,
+ );
+ // The turborepo / Drizzle quotes must sit inside the examples block, not
+ // as bare EXTRACT bullets the model can lift as facts about this repo.
+ const examplesBlock = AGENT_ENTITY_CONTEXT.match(
+ /([\s\S]*?)<\/examples>/,
+ );
+ assert.ok(examplesBlock);
+ assert.match(examplesBlock[1], /monorepo with turborepo/);
+ assert.match(examplesBlock[1], /chose Drizzle over Prisma/);
+ assert.match(AGENT_ENTITY_CONTEXT, /remember things that a human would remember/);
+ });
+});
+
describe('capture hook', () => {
test('saves the transcript delta with scope metadata and entity context', async (t) => {
const { repo, home } = makeRepo(t);
@@ -447,6 +471,7 @@ describe('capture hook', () => {
assert.equal(body.metadata.sm_scope, 'personal');
assert.equal(body.customId, 'sess-2');
assert.match(body.entityContext, /EXTRACT/);
+ assert.match(body.entityContext, //);
const state = readState('sess-2', {
dataDir: join(home, '.supermemory-claude', 'statusline'),