fix(n8n): restore App ID, Includes and Excludes, add App ID filters

Reverts the removal in 2a61fe1a. That commit dropped app_id, includes and
excludes from Add because docs/openapi.json does not declare them on
POST /v3/memories/add/. The schema is incomplete: probing the live endpoint
shows all three are accepted and honoured.

- app_id is persisted and is queryable via /v3/memories/search/ and
  /v3/memories/, so the entity-id guard accepts it again as a valid scope
- includes/excludes demonstrably steer extraction. The same message stored
  three memories unfiltered and one under
  includes: "only record food and diet preferences"

Search and Get Many gain an App ID filter alongside User/Agent/Run. Writing
an app-scoped memory was otherwise unreadable from this node, and app_id is
how the OpenCode and Pi Agent plugins express project scope.

buildEntityFilters now takes the ids as an object rather than four adjacent
positional strings, so a fifth entity is a one-key change.
This commit is contained in:
kartik-mem0
2026-07-29 22:18:19 +05:30
parent a8e2a49209
commit 29601e54f4
4 changed files with 120 additions and 26 deletions
@@ -53,6 +53,44 @@ describe('Mem0 node (offline)', () => {
expect(out[0][0].json.error).toMatch(/at least one of User ID/i);
});
it('forwards app id, includes and excludes on Add', async () => {
const ctx = makeCtx(
'add',
{
'messages.message': [{ role: 'user', content: 'hi' }],
addFields: {
app_id: 'p1',
includes: 'only food preferences',
excludes: 'nothing about vehicles',
},
userId: 'u1',
},
async () => ({}),
);
await run(ctx);
expect(ctx.requests[0].body).toMatchObject({
app_id: 'p1',
includes: 'only food preferences',
excludes: 'nothing about vehicles',
});
});
it('accepts an app id alone as the entity scope on Add', async () => {
const ctx = makeCtx(
'add',
{
'messages.message': [{ role: 'user', content: 'hi' }],
addFields: { app_id: 'p1' },
userId: '',
},
async () => ({}),
{ continueOnFail: true },
);
const out: any = await run(ctx);
expect(out[0][0].json.error).toBeUndefined();
expect(ctx.requests[0].body.app_id).toBe('p1');
});
it('reports a clear error on invalid JSON in Custom Categories', async () => {
const ctx = makeCtx(
'add',
@@ -103,15 +141,21 @@ describe('Mem0 node (offline)', () => {
it('combines entity ids with OR, never AND (entities are stored separately, so AND matches nothing)', async () => {
const ctx = makeCtx(
'search',
{ query: 'x', userId: 'u1', agentId: 'a1', runId: 'r1' },
{ query: 'x', userId: 'u1', agentId: 'a1', appId: 'p1', runId: 'r1' },
async () => ({ results: [] }),
);
await run(ctx);
expect(ctx.requests[0].body.filters).toEqual({
OR: [{ user_id: 'u1' }, { agent_id: 'a1' }, { run_id: 'r1' }],
OR: [{ user_id: 'u1' }, { agent_id: 'a1' }, { app_id: 'p1' }, { run_id: 'r1' }],
});
});
it.each(['search', 'getAll'])('filters %s by app id alone', async (op) => {
const ctx = makeCtx(op, { query: 'x', appId: 'p1' }, async () => ({ results: [] }));
await run(ctx);
expect(ctx.requests[0].body.filters).toEqual({ app_id: 'p1' });
});
it('filters Get Many by agent id alone', async () => {
const ctx = makeCtx('getAll', { agentId: 'a1' }, async () => ({ results: [] }));
await run(ctx);