Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion plugin/hooks/lib/api.js
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -8,17 +11,22 @@ RULES:

EXTRACT:
- User preferences, accepted decisions, durable workflows, actions, and learnings

<examples note="another project, never extract">
- 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"
</examples>

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

</guidance> 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
Expand Down
25 changes: 25 additions & 0 deletions test/unit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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, /<examples note="another project, never extract">/);
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(
/<examples note="another project, never extract">([\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);
Expand Down Expand Up @@ -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, /<examples note="another project, never extract">/);

const state = readState('sess-2', {
dataDir: join(home, '.supermemory-claude', 'statusline'),
Expand Down