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'),