n8n-nodes-tembory 1.0.34 → 1.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -577,6 +577,7 @@ const deriveEntityTimeline = (profileFacts = {}, graph = [], recentMessages = []
577
577
  return pruneByLimit(deduped.sort((a, b) => String(a.at || '').localeCompare(String(b.at || ''))), maxItems || 24);
578
578
  };
579
579
  const RECENT_MESSAGE_MARKER = '__tembory_recent_message_v1__';
580
+ const CONVERSATION_LEDGER_MARKER = '__tembory_conversation_ledger_v1__';
580
581
  const TOOL_HISTORY_MARKER = '__tembory_tool_history_v1__';
581
582
  const TOOL_LEDGER_MARKER = '__tembory_tool_ledger_v1__';
582
583
  const encodeRecentMessage = (recent, threadId) => `${RECENT_MESSAGE_MARKER}${safeStringify({
@@ -585,6 +586,20 @@ const encodeRecentMessage = (recent, threadId) => `${RECENT_MESSAGE_MARKER}${saf
585
586
  at: recent.at || nowIso(),
586
587
  thread_id: threadId,
587
588
  })}`;
589
+ const encodeConversationLedger = (messages = [], threadId) => {
590
+ const chronological = sortConversationChronological(messages || []);
591
+ const userMessages = chronological.filter((msg) => /^(user|human)$/i.test(String(msg.role || '')));
592
+ const readable = `Conversation transcript ledger. Use the chronological messages array to answer conversation recall, including what the user said, message order, and the user message immediately before the current request. Total messages: ${chronological.length}. User messages: ${userMessages.length}. `;
593
+ return `${readable}${CONVERSATION_LEDGER_MARKER}${safeStringify({
594
+ thread_id: threadId,
595
+ generated_at: nowIso(),
596
+ messages: chronological.map((msg) => ({
597
+ role: msg.role || 'user',
598
+ content: msg.content || '',
599
+ at: msg.at || nowIso(),
600
+ })),
601
+ })}`;
602
+ };
588
603
  const encodeToolCall = (tool, threadId) => `${TOOL_HISTORY_MARKER}${safeStringify({
589
604
  id: tool.id || tool.callId || '',
590
605
  turn_id: tool.turnId || '',
@@ -684,6 +699,27 @@ const parseToolLedgerMarker = (text) => {
684
699
  return [];
685
700
  }
686
701
  };
702
+ const parseConversationLedgerMarker = (text) => {
703
+ if (!text || typeof text !== 'string')
704
+ return [];
705
+ const markerIndex = text.indexOf(CONVERSATION_LEDGER_MARKER);
706
+ if (markerIndex < 0)
707
+ return [];
708
+ try {
709
+ const parsed = JSON.parse(text.slice(markerIndex + CONVERSATION_LEDGER_MARKER.length));
710
+ const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
711
+ return messages
712
+ .filter((msg) => msg && msg.content)
713
+ .map((msg) => ({
714
+ role: msg.role || 'user',
715
+ content: truncate(msg.content, 2000),
716
+ at: msg.at || parsed.generated_at || nowIso(),
717
+ }));
718
+ }
719
+ catch {
720
+ return [];
721
+ }
722
+ };
687
723
  const recentMessageFromMemory = (item) => {
688
724
  const meta = metadataOf(item);
689
725
  const content = meta.content || item.memory || item.text || item.value || item.content || item.data;
@@ -700,6 +736,15 @@ const recentMessageFromMemory = (item) => {
700
736
  at: meta.at || item.created_at || item.createdAt || nowIso(),
701
737
  };
702
738
  };
739
+ const recentMessagesFromMemory = (item) => {
740
+ const meta = metadataOf(item);
741
+ const content = meta.content || item.memory || item.text || item.value || item.content || item.data;
742
+ const ledger = parseConversationLedgerMarker(content);
743
+ if (ledger.length)
744
+ return ledger;
745
+ const single = recentMessageFromMemory(item);
746
+ return single ? [single] : [];
747
+ };
703
748
  const toolHistoryFromMemory = (item) => {
704
749
  const meta = metadataOf(item);
705
750
  const content = meta.content || item.memory || item.text || item.value || item.content || item.data;
@@ -1022,7 +1067,7 @@ const applyOperationalPreset = (advanced = {}) => {
1022
1067
  topK: 8,
1023
1068
  lastN: 12,
1024
1069
  toolHistoryLastN: 15,
1025
- recentMessagesLastN: 30,
1070
+ recentMessagesLastN: 50,
1026
1071
  },
1027
1072
  productionBalanced: {
1028
1073
  summarySource: 'auto',
@@ -1055,7 +1100,7 @@ const applyOperationalPreset = (advanced = {}) => {
1055
1100
  topK: 6,
1056
1101
  lastN: 8,
1057
1102
  toolHistoryLastN: 10,
1058
- recentMessagesLastN: 30,
1103
+ recentMessagesLastN: 50,
1059
1104
  vectorMemoryMaxChars: 360,
1060
1105
  contextMaxChars: 10000,
1061
1106
  connectedModelSummaryMaxChars: 1200,
@@ -1092,7 +1137,7 @@ const applyOperationalPreset = (advanced = {}) => {
1092
1137
  topK: 4,
1093
1138
  lastN: 4,
1094
1139
  toolHistoryLastN: 5,
1095
- recentMessagesLastN: 30,
1140
+ recentMessagesLastN: 50,
1096
1141
  vectorMemoryMaxChars: 260,
1097
1142
  contextMaxChars: 7000,
1098
1143
  connectedModelSummaryMaxChars: 1000,
@@ -1130,7 +1175,7 @@ const applyOperationalPreset = (advanced = {}) => {
1130
1175
  lastN: 4,
1131
1176
  maxReturn: 6,
1132
1177
  toolHistoryLastN: 6,
1133
- recentMessagesLastN: 30,
1178
+ recentMessagesLastN: 50,
1134
1179
  vectorMemoryMaxChars: 220,
1135
1180
  contextMaxChars: 7000,
1136
1181
  connectedModelSummaryMaxChars: 1200,
@@ -1160,7 +1205,7 @@ const applyOperationalPreset = (advanced = {}) => {
1160
1205
  topK: 10,
1161
1206
  lastN: 20,
1162
1207
  toolHistoryLastN: 20,
1163
- recentMessagesLastN: 30,
1208
+ recentMessagesLastN: 50,
1164
1209
  payloadFormat: 'auditJson',
1165
1210
  },
1166
1211
  };
@@ -1328,13 +1373,10 @@ const previousUserFallbackFromWorkingMemory = (query = '', workingMemory = {}) =
1328
1373
  const firstUserMessageFromConversation = (recentMessages = []) => [...(recentMessages || [])]
1329
1374
  .filter((msg) => /^(user|human)$/i.test(String(msg.role || '')))
1330
1375
  .sort((a, b) => new Date(a.at || 0).getTime() - new Date(b.at || 0).getTime())[0] || null;
1331
- const buildConversationFrame = ({ query = '', recentMessages = [], workingMemory = {} }) => {
1376
+ const buildConversationFrame = ({ query = '', recentMessages = [] }) => {
1332
1377
  const normalizedQuery = normalizeIntentText(query).trim();
1333
1378
  const users = recentUserMessages(recentMessages);
1334
1379
  const currentUser = users.find((msg) => normalizeIntentText(msg.content).trim() === normalizedQuery) || (query ? { role: 'user', content: query, at: nowIso() } : null);
1335
- const previousUser = previousUserMessageForQuery(query, recentMessages);
1336
- const firstUser = firstUserMessageFromConversation(recentMessages);
1337
- const previousUserMessage = previousUser ? truncate(previousUser.content, 500) : previousUserFallbackFromWorkingMemory(query, workingMemory);
1338
1380
  const chronological = (recentMessages || []).map((msg) => ({
1339
1381
  role: msg.role,
1340
1382
  content: truncate(msg.content, 500),
@@ -1343,8 +1385,6 @@ const buildConversationFrame = ({ query = '', recentMessages = [], workingMemory
1343
1385
  const userMessagesChronological = chronological.filter((msg) => /^(user|human)$/i.test(String(msg.role || '')));
1344
1386
  const frame = cleanContextValue({
1345
1387
  current_user_message: currentUser ? truncate(currentUser.content, 500) : truncate(query, 500),
1346
- first_user_message: firstUser ? truncate(firstUser.content, 500) : null,
1347
- previous_user_message: previousUserMessage,
1348
1388
  conversation_history_chronological: chronological,
1349
1389
  recent_messages_chronological: chronological,
1350
1390
  all_user_messages_chronological: userMessagesChronological,
@@ -1352,10 +1392,8 @@ const buildConversationFrame = ({ query = '', recentMessages = [], workingMemory
1352
1392
  .filter((msg) => normalizeIntentText(msg.content).trim() !== normalizedQuery)
1353
1393
  .slice(0, 5)
1354
1394
  .map((msg) => ({ role: msg.role, content: truncate(msg.content, 500), at: msg.at })),
1355
- instruction: 'This is the authoritative short-term conversation frame. If the user asks about current/previous/last client messages or what they already said, answer from current_user_message/previous_user_message/conversation_history_chronological/all_user_messages_chronological before using vector memories, summaries, operational state, or tool history.',
1395
+ instruction: 'This is the authoritative short-term conversation frame. If the user asks about current/previous/last client messages or what they already said, answer from conversation_history_chronological/all_user_messages_chronological before using vector memories, summaries, operational state, or tool history. For the last message before the current one, use the user message immediately before current_user_message in all_user_messages_chronological.',
1356
1396
  });
1357
- if (!previousUserMessage)
1358
- frame.previous_user_message = null;
1359
1397
  return frame;
1360
1398
  };
1361
1399
  const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalState = {}, workingMemory = {} }) => {
@@ -1363,31 +1401,31 @@ const buildCurrentTurnFocus = ({ query = '', recentMessages = [], operationalSta
1363
1401
  const operationalStatus = isOperationalStatusQuery(query);
1364
1402
  if (!recall && !operationalStatus)
1365
1403
  return null;
1366
- const previousUser = previousUserMessageForQuery(query, recentMessages);
1367
- const firstUser = firstUserMessageFromConversation(recentMessages);
1368
1404
  const users = recentUserMessages(recentMessages)
1369
1405
  .filter((msg) => normalizeIntentText(msg.content).trim() !== normalizeIntentText(query).trim())
1370
- .slice(0, 3)
1406
+ .slice(0, 8)
1371
1407
  .map((msg) => ({ role: msg.role, content: truncate(msg.content, 500), at: msg.at }));
1372
1408
  const toolState = (operationalState || {}).tool_state || {};
1373
- const previousUserMessage = previousUser ? truncate(previousUser.content, 500) : previousUserFallbackFromWorkingMemory(query, workingMemory);
1409
+ const chronological = (recentMessages || []).map((msg) => ({
1410
+ role: msg.role,
1411
+ content: truncate(msg.content, 500),
1412
+ at: msg.at,
1413
+ }));
1414
+ const userMessagesChronological = chronological.filter((msg) => /^(user|human)$/i.test(String(msg.role || '')));
1374
1415
  const focus = cleanContextValue({
1375
1416
  current_user_request: truncate(query, 500),
1376
1417
  intent: recall ? 'conversation_recall' : 'tool_or_status_question',
1377
- first_user_message: firstUser ? truncate(firstUser.content, 500) : null,
1378
- previous_user_message: previousUserMessage,
1379
1418
  recent_user_messages: users,
1419
+ all_user_messages_chronological: recall ? userMessagesChronological : undefined,
1380
1420
  tool_status: operationalStatus ? {
1381
1421
  executed_tools: toolState.names || [],
1382
1422
  last_successful_tool: toolState.last_successful_tool || null,
1383
1423
  failed_by_name: toolState.failed_by_name || {},
1384
1424
  } : undefined,
1385
1425
  instruction: recall
1386
- ? 'Answer the user meta-question from previous_user_message/recent_user_messages and the chronological transcript. Do not call tools for recall-only questions.'
1426
+ ? 'Answer the user meta-question from conversation_history_chronological/all_user_messages_chronological. Do not call tools for recall-only questions and do not infer missing turns from vector memories or summaries.'
1387
1427
  : 'Answer status questions from tool_state, tool_history and action_ledger. Do not call tools unless the agent prompt requires it.',
1388
1428
  });
1389
- if (!previousUserMessage)
1390
- focus.previous_user_message = null;
1391
1429
  return focus;
1392
1430
  };
1393
1431
  const buildActionDirective = ({ workingMemory = {}, operationalState = {} }) => {
@@ -1443,7 +1481,7 @@ const deriveNextExpectedAction = (intent, operationalState = {}) => {
1443
1481
  if (intent === 'profile_update')
1444
1482
  return 'save stable profile facts and continue the conversation';
1445
1483
  if (intent === 'conversation_recall')
1446
- return 'answer directly using previous_user_message and conversation_history_chronological; do not call tools for recall-only questions';
1484
+ return 'answer directly using conversation_history_chronological/all_user_messages_chronological; do not call tools for recall-only questions';
1447
1485
  if (intent === 'operational_status_question')
1448
1486
  return 'answer status questions from tool_state, tool_history and action_ledger; do not call tools unless the agent prompt requires it';
1449
1487
  return 'answer using retrieved context and avoid unnecessary tool calls';
@@ -2427,7 +2465,7 @@ class Mem0Memory {
2427
2465
  values: [
2428
2466
  { displayName: 'Incluir Profile Facts', name: 'includeProfileFacts', type: 'boolean', default: true },
2429
2467
  { displayName: 'Incluir Mensagens Recentes', name: 'includeRecentMessages', type: 'boolean', default: true },
2430
- { displayName: 'Últimas N Mensagens', name: 'recentMessagesLastN', type: 'number', default: 30 },
2468
+ { displayName: 'Últimas N Mensagens', name: 'recentMessagesLastN', type: 'number', default: 50 },
2431
2469
  { displayName: 'Incluir Highlights Recentes', name: 'includeRecentHighlights', type: 'boolean', default: true },
2432
2470
  { displayName: 'Máximo de Highlights', name: 'recentHighlightsMaxItems', type: 'number', default: 6 },
2433
2471
  { displayName: 'Incluir Grafo', name: 'includeRelations', type: 'boolean', default: true },
@@ -2582,7 +2620,7 @@ class Mem0Memory {
2582
2620
  { displayName: 'Incluir Entity Timeline', name: 'includeEntityTimeline', type: 'boolean', default: true, description: 'Injeta uma timeline compacta de entidades, fatos de perfil, relações do grafo e eventos importantes da sessão.' },
2583
2621
  { displayName: 'Incluir Compressão de Memória', name: 'includeMemoryCompression', type: 'boolean', default: true, description: 'Injeta resumos compactos de turno, sessão, entidades e workflow para reduzir ruído.' },
2584
2622
  { displayName: 'Incluir Mensagens Recentes', name: 'includeRecentMessages', type: 'boolean', default: true },
2585
- { displayName: 'Últimas N Mensagens', name: 'recentMessagesLastN', type: 'number', default: 30 },
2623
+ { displayName: 'Últimas N Mensagens', name: 'recentMessagesLastN', type: 'number', default: 50 },
2586
2624
  { displayName: 'Incluir Highlights Recentes', name: 'includeRecentHighlights', type: 'boolean', default: true },
2587
2625
  { displayName: 'Máximo de Highlights', name: 'recentHighlightsMaxItems', type: 'number', default: 6 },
2588
2626
  ],
@@ -2839,6 +2877,13 @@ class Mem0Memory {
2839
2877
  source: 'n8n_connected_embedding',
2840
2878
  }, ids));
2841
2879
  }
2880
+ clientMemories.push(await createClientVectorMemory(connectedEmbedding, encodeConversationLedger(recentForTurn, threadId), {
2881
+ kind: 'conversation_ledger',
2882
+ thread_id: threadId,
2883
+ project: project || undefined,
2884
+ source: 'n8n_connected_embedding',
2885
+ generated_at: nowIso(),
2886
+ }, ids));
2842
2887
  }
2843
2888
  if (adv.persistToolFactsToMem0 && toolCalls.length) {
2844
2889
  const facts = toolCalls.map((tool) => `Tool ${tool.name} input=${tool.input}${tool.result ? ` result=${tool.result}` : ''}`).join('\n');
@@ -2894,6 +2939,23 @@ class Mem0Memory {
2894
2939
  user_id: body.user_id,
2895
2940
  });
2896
2941
  }
2942
+ await safePersistLegacyMemory(this, {
2943
+ messages: [{ role: 'system', content: encodeConversationLedger(recentForTurn, threadId) }],
2944
+ infer: false,
2945
+ user_id: body.user_id,
2946
+ agent_id: body.agent_id,
2947
+ run_id: body.run_id,
2948
+ metadata: {
2949
+ kind: 'conversation_ledger',
2950
+ thread_id: threadId,
2951
+ project: project || undefined,
2952
+ source: 'tembory_transcript',
2953
+ generated_at: nowIso(),
2954
+ },
2955
+ }, {
2956
+ kind: 'conversation_ledger',
2957
+ user_id: body.user_id,
2958
+ });
2897
2959
  }
2898
2960
  if (adv.includeToolHistory !== false && toolCalls.length) {
2899
2961
  for (const tool of toolCalls) {
@@ -2962,6 +3024,20 @@ class Mem0Memory {
2962
3024
  },
2963
3025
  });
2964
3026
  }
3027
+ await GenericFunctions_1.mem0ApiRequest.call(this, 'POST', '/v1/memories/', {
3028
+ messages: [{ role: 'system', content: encodeConversationLedger(recentForTurn, threadId) }],
3029
+ infer: false,
3030
+ user_id: body.user_id,
3031
+ agent_id: body.agent_id,
3032
+ run_id: body.run_id,
3033
+ metadata: {
3034
+ kind: 'conversation_ledger',
3035
+ thread_id: threadId,
3036
+ project: project || undefined,
3037
+ source: 'tembory_transcript',
3038
+ generated_at: nowIso(),
3039
+ },
3040
+ });
2965
3041
  }
2966
3042
  if (adv.includeToolHistory !== false && !adv.persistToolFactsToMem0 && toolCalls.length) {
2967
3043
  for (const tool of toolCalls) {
@@ -3267,7 +3343,7 @@ class Mem0Memory {
3267
3343
  try {
3268
3344
  const transcriptProbe = [
3269
3345
  'recent conversation transcript',
3270
- 'first user message earliest message last user messages previous message full conversation',
3346
+ 'chronological user messages earliest message latest message message immediately before current request full conversation',
3271
3347
  query,
3272
3348
  ].filter(Boolean).join('\n');
3273
3349
  const transcriptBody = {
@@ -3317,13 +3393,11 @@ class Mem0Memory {
3317
3393
  }
3318
3394
  if (adv.includeRecentMessages !== false) {
3319
3395
  persistedRecentMessages = persistedMemoryItems
3320
- .map(recentMessageFromMemory)
3321
- .filter(Boolean)
3396
+ .flatMap(recentMessagesFromMemory)
3322
3397
  .sort((a, b) => new Date(a.at || 0).getTime() - new Date(b.at || 0).getTime());
3323
3398
  }
3324
3399
  const vectorRecentMessages = adv.includeRecentMessages === false ? [] : vectorMemories
3325
- .map(recentMessageFromMemory)
3326
- .filter(Boolean)
3400
+ .flatMap(recentMessagesFromMemory)
3327
3401
  .sort((a, b) => new Date(a.at || 0).getTime() - new Date(b.at || 0).getTime());
3328
3402
  if (adv.includeToolHistory !== false) {
3329
3403
  persistedToolHistory = persistedMemoryItems
@@ -3699,7 +3773,9 @@ exports.__private = {
3699
3773
  toolHistoryFromMemory,
3700
3774
  cleanAssistantTranscriptText,
3701
3775
  buildActionDirective,
3776
+ buildConversationFrame,
3702
3777
  recentMessageFromMemory,
3778
+ recentMessagesFromMemory,
3703
3779
  previousUserFallbackFromWorkingMemory,
3704
3780
  firstUserMessageFromConversation,
3705
3781
  sortConversationChronological,
@@ -3710,9 +3786,11 @@ exports.__private = {
3710
3786
  parseToolHistoryMarker,
3711
3787
  parseToolLedgerMarker,
3712
3788
  parseRecentMessageMarker,
3789
+ parseConversationLedgerMarker,
3713
3790
  encodeToolCall,
3714
3791
  encodeToolLedger,
3715
3792
  encodeRecentMessage,
3793
+ encodeConversationLedger,
3716
3794
  userKeyFrom,
3717
3795
  applyOperationalPreset,
3718
3796
  flattenAdvancedGroups,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tembory",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "Tembory node for n8n AI Agents with profile, tools, timeline, graph and semantic memory",
5
5
  "license": "MIT",
6
6
  "homepage": "https://tembory.com",
@@ -42,8 +42,40 @@ function requiredToolFromText(text) {
42
42
  return /"required_tool":"([^"]+)"/.exec(text)?.[1] || null;
43
43
  }
44
44
 
45
- function previousUserMessageFromText(text) {
46
- return /"previous_user_message":"((?:\\"|[^"])*)"/.exec(text)?.[1]?.replace(/\\"/g, '"') || '';
45
+ function conversationFrameFromText(text) {
46
+ const match = /## Conversation frame\n([\s\S]*?)(?:\n\n## |\n## |$)/.exec(text);
47
+ if (!match) return null;
48
+ try {
49
+ return JSON.parse(match[1].trim());
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function previousUserMessageFromText(text, currentMessage) {
56
+ const frame = conversationFrameFromText(text);
57
+ const userMessages = Array.isArray(frame?.all_user_messages_chronological)
58
+ ? frame.all_user_messages_chronological
59
+ : [];
60
+ const normalizedCurrent = normalizeText(currentMessage).trim();
61
+ for (let index = userMessages.length - 1; index >= 0; index -= 1) {
62
+ const message = userMessages[index];
63
+ if (normalizeText(message?.content).trim() === normalizedCurrent) {
64
+ return userMessages[index - 1]?.content || '';
65
+ }
66
+ }
67
+ return userMessages.at(-2)?.content || '';
68
+ }
69
+
70
+ function previousUserMessageFromState(messages, currentMessage) {
71
+ const userMessages = (messages || []).filter((message) => /^(user|human)$/i.test(String(message.role || '')));
72
+ const normalizedCurrent = normalizeText(currentMessage).trim();
73
+ for (let index = userMessages.length - 1; index >= 0; index -= 1) {
74
+ if (normalizeText(userMessages[index]?.content).trim() === normalizedCurrent) {
75
+ return userMessages[index - 1]?.content || '';
76
+ }
77
+ }
78
+ return userMessages.at(-2)?.content || '';
47
79
  }
48
80
 
49
81
  function normalizeText(value) {
@@ -105,7 +137,8 @@ function answerFor({ userMessage, toolCalls, state, contextText }) {
105
137
  .join('\n');
106
138
  }
107
139
  if (/qual.*ultima mensagem antes/.test(normalized)) {
108
- const previous = previousUserMessageFromText(contextText)
140
+ const previous = previousUserMessageFromText(contextText, userMessage)
141
+ || previousUserMessageFromState(state.recentMessages, userMessage)
109
142
  || core.previousUserFallbackFromWorkingMemory(userMessage, state.workingMemory);
110
143
  return previous ? `Sua última mensagem antes desta foi: "${previous}".` : 'Não encontrei mensagem anterior.';
111
144
  }