n8n-nodes-tembory 1.1.25 → 1.1.27

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.
@@ -1205,16 +1205,18 @@ const getMemoryStore = (ctx) => {
1205
1205
  data.tembory.activeSummary = data.tembory.activeSummary || {};
1206
1206
  data.tembory.connectedModelSummaryCache = data.tembory.connectedModelSummaryCache || {};
1207
1207
  data.tembory.captureState = data.tembory.captureState || {};
1208
+ data.tembory.memoryDedupe = data.tembory.memoryDedupe || {};
1208
1209
  return data.tembory;
1209
1210
  }
1210
1211
  catch {
1211
- global.__temboryMemory = global.__temboryMemory || { toolHistory: {}, recentMessages: {}, profileFacts: {}, workingMemory: {}, decisionState: {}, memoryCompression: {}, activeSummary: {}, connectedModelSummaryCache: {}, captureState: {} };
1212
+ global.__temboryMemory = global.__temboryMemory || { toolHistory: {}, recentMessages: {}, profileFacts: {}, workingMemory: {}, decisionState: {}, memoryCompression: {}, activeSummary: {}, connectedModelSummaryCache: {}, captureState: {}, memoryDedupe: {} };
1212
1213
  global.__temboryMemory.workingMemory = global.__temboryMemory.workingMemory || {};
1213
1214
  global.__temboryMemory.decisionState = global.__temboryMemory.decisionState || {};
1214
1215
  global.__temboryMemory.memoryCompression = global.__temboryMemory.memoryCompression || {};
1215
1216
  global.__temboryMemory.activeSummary = global.__temboryMemory.activeSummary || {};
1216
1217
  global.__temboryMemory.connectedModelSummaryCache = global.__temboryMemory.connectedModelSummaryCache || {};
1217
1218
  global.__temboryMemory.captureState = global.__temboryMemory.captureState || {};
1219
+ global.__temboryMemory.memoryDedupe = global.__temboryMemory.memoryDedupe || {};
1218
1220
  return global.__temboryMemory;
1219
1221
  }
1220
1222
  };
@@ -1268,6 +1270,8 @@ const mergeRemoteThreadState = (store, key, state) => {
1268
1270
  store.memoryCompression[key] = state.memoryCompression;
1269
1271
  if (state.captureState && typeof state.captureState === 'object')
1270
1272
  store.captureState[key] = { ...(store.captureState[key] || {}), ...state.captureState };
1273
+ if (state.memoryDedupe && typeof state.memoryDedupe === 'object')
1274
+ store.memoryDedupe[key] = mergeMemoryDedupeState(store.memoryDedupe[key], state.memoryDedupe);
1271
1275
  if (typeof state.activeSummary === 'string' && state.activeSummary)
1272
1276
  store.activeSummary[key] = state.activeSummary;
1273
1277
  };
@@ -1632,6 +1636,195 @@ const normalizeIntentText = (value = '') => String(value || '')
1632
1636
  .toLowerCase()
1633
1637
  .normalize('NFD')
1634
1638
  .replace(/[\u0300-\u036f]/g, '');
1639
+ const MEMORY_DEDUPE_MAX_KEYS = 600;
1640
+ const LEDGER_MEMORY_KINDS = new Set(['conversation_ledger', 'tool_ledger', 'turn_archive']);
1641
+ const normalizeDedupeText = (value = '') => normalizeIntentText(value).replace(/\s+/g, ' ').trim();
1642
+ const memoryBodyText = (body = {}) => {
1643
+ const messages = Array.isArray(body.messages) ? body.messages : [];
1644
+ if (messages.length)
1645
+ return messages.map((message) => message.content || message.text || message.message || '').filter(Boolean).join('\n');
1646
+ return body.memory || body.text || body.value || body.content || '';
1647
+ };
1648
+ const parseTurnArchiveText = (text = '') => {
1649
+ const raw = String(text || '');
1650
+ const start = raw.indexOf('{');
1651
+ if (start < 0)
1652
+ return null;
1653
+ try {
1654
+ const parsed = JSON.parse(raw.slice(start));
1655
+ return parsed && parsed.marker === 'tembory_turn_archive_v1' ? parsed : null;
1656
+ }
1657
+ catch {
1658
+ return null;
1659
+ }
1660
+ };
1661
+ const compactToolForDedupe = (tool = {}, index = 0) => ({
1662
+ id: tool.id || tool.callId || tool.call_id || undefined,
1663
+ sequence: tool.sequence || index + 1,
1664
+ name: tool.name || tool.tool || tool.toolName || '',
1665
+ input: canonicalToolInput(tool.input || tool.toolInput || tool.args || ''),
1666
+ ok: tool.ok !== false && tool.status !== 'failed',
1667
+ result: String(tool.result || tool.output || ''),
1668
+ });
1669
+ const canonicalMemoryPayloadForDedupe = (kind = 'memory', body = {}, diagnostics = {}) => {
1670
+ const meta = body.metadata || {};
1671
+ const text = memoryBodyText(body);
1672
+ if (kind === 'recent_message') {
1673
+ const marker = parseRecentMessageMarker(text);
1674
+ return stableStringify({
1675
+ role: meta.role || marker?.role || 'user',
1676
+ content: normalizeDedupeText(meta.content || marker?.content || text),
1677
+ });
1678
+ }
1679
+ if (kind === 'tool_history') {
1680
+ const marker = parseToolHistoryMarker(text);
1681
+ const tool = marker || {
1682
+ id: meta.id,
1683
+ turnId: meta.turn_id || meta.turnId,
1684
+ sequence: meta.sequence,
1685
+ name: meta.name || diagnostics.tool,
1686
+ input: meta.input,
1687
+ ok: meta.ok,
1688
+ result: meta.result,
1689
+ };
1690
+ return stableStringify(compactToolForDedupe(tool));
1691
+ }
1692
+ if (kind === 'tool_ledger') {
1693
+ return stableStringify(dedupeToolHistory(parseToolLedgerMarker(text)).map(compactToolForDedupe));
1694
+ }
1695
+ if (kind === 'conversation_ledger') {
1696
+ const messages = parseConversationLedgerMarker(text);
1697
+ return stableStringify((messages || []).map((message) => ({
1698
+ role: message.role || 'user',
1699
+ content: normalizeDedupeText(message.content || ''),
1700
+ })));
1701
+ }
1702
+ if (kind === 'turn_archive') {
1703
+ const archive = parseTurnArchiveText(text);
1704
+ if (archive) {
1705
+ return stableStringify({
1706
+ conversation: (archive.conversation || []).map((message) => ({
1707
+ role: message.role || 'user',
1708
+ content: normalizeDedupeText(message.content || ''),
1709
+ })),
1710
+ tools: (archive.tools || []).map(compactToolForDedupe),
1711
+ });
1712
+ }
1713
+ }
1714
+ return normalizeDedupeText(String(text || '')
1715
+ .replace(/"generated_at"\s*:\s*"[^"]+"/g, '"generated_at":"<ts>"')
1716
+ .replace(/"timestamp"\s*:\s*"[^"]+"/g, '"timestamp":"<ts>"'));
1717
+ };
1718
+ const inferMemoryKindFromText = (text = '') => {
1719
+ const raw = String(text || '');
1720
+ if (parseRecentMessageMarker(raw))
1721
+ return 'recent_message';
1722
+ if (parseToolHistoryMarker(raw))
1723
+ return 'tool_history';
1724
+ if (parseToolLedgerMarker(raw).length)
1725
+ return 'tool_ledger';
1726
+ if (parseConversationLedgerMarker(raw).length)
1727
+ return 'conversation_ledger';
1728
+ if (parseTurnArchiveText(raw))
1729
+ return 'turn_archive';
1730
+ return '';
1731
+ };
1732
+ const buildMemoryDedupeCandidate = (body = {}, diagnostics = {}) => {
1733
+ const meta = body.metadata || {};
1734
+ const text = memoryBodyText(body);
1735
+ const kind = String(meta.kind || diagnostics.kind || inferMemoryKindFromText(text) || 'memory');
1736
+ const thread = String(meta.thread_id || diagnostics.thread_id || body.thread_id || '');
1737
+ const project = String(meta.project || diagnostics.project || body.project || '');
1738
+ const slot = [kind, thread, project].filter(Boolean).join(':') || kind;
1739
+ const canonical = canonicalMemoryPayloadForDedupe(kind, body, diagnostics);
1740
+ const key = `${slot}:${stableHash(canonical)}`;
1741
+ return cleanContextValue({
1742
+ key,
1743
+ slot,
1744
+ kind,
1745
+ content_hash: stableHash(canonical),
1746
+ strategy: LEDGER_MEMORY_KINDS.has(kind) ? 'latest-ledger-per-content' : 'exact-canonical-content',
1747
+ });
1748
+ };
1749
+ const mergeMemoryDedupeState = (current = {}, incoming = {}) => {
1750
+ const merged = {
1751
+ seen: { ...((current || {}).seen || {}), ...((incoming || {}).seen || {}) },
1752
+ latestBySlot: { ...((current || {}).latestBySlot || {}), ...((incoming || {}).latestBySlot || {}) },
1753
+ lastSummary: (incoming || {}).lastSummary || (current || {}).lastSummary || undefined,
1754
+ updatedAt: (incoming || {}).updatedAt || (current || {}).updatedAt || undefined,
1755
+ };
1756
+ const entries = Object.entries(merged.seen);
1757
+ if (entries.length > MEMORY_DEDUPE_MAX_KEYS) {
1758
+ entries
1759
+ .sort((a, b) => String(a[1] || '').localeCompare(String(b[1] || '')))
1760
+ .slice(0, entries.length - MEMORY_DEDUPE_MAX_KEYS)
1761
+ .forEach(([key]) => delete merged.seen[key]);
1762
+ }
1763
+ return cleanContextValue(merged);
1764
+ };
1765
+ const ensureMemoryDedupeState = (store, key) => {
1766
+ store.memoryDedupe = store.memoryDedupe || {};
1767
+ store.memoryDedupe[key] = mergeMemoryDedupeState(store.memoryDedupe[key] || {}, {});
1768
+ store.memoryDedupe[key].seen = store.memoryDedupe[key].seen || {};
1769
+ store.memoryDedupe[key].latestBySlot = store.memoryDedupe[key].latestBySlot || {};
1770
+ return store.memoryDedupe[key];
1771
+ };
1772
+ const createDedupeSummary = () => ({
1773
+ enabled: true,
1774
+ write: { candidates: 0, persisted: 0, skipped: 0, byKind: {} },
1775
+ load: {},
1776
+ });
1777
+ const recordDedupeWriteSummary = (summary, candidate, decision) => {
1778
+ if (!summary || !candidate)
1779
+ return;
1780
+ summary.write.candidates += 1;
1781
+ summary.write[decision.persist ? 'persisted' : 'skipped'] += 1;
1782
+ summary.write.byKind[candidate.kind] = summary.write.byKind[candidate.kind] || { candidates: 0, persisted: 0, skipped: 0 };
1783
+ summary.write.byKind[candidate.kind].candidates += 1;
1784
+ summary.write.byKind[candidate.kind][decision.persist ? 'persisted' : 'skipped'] += 1;
1785
+ };
1786
+ const evaluateMemoryDedupeWrite = (state, candidate, at = nowIso()) => {
1787
+ if (!candidate || !candidate.key)
1788
+ return { persist: true, reason: 'no_dedupe_key' };
1789
+ state.seen = state.seen || {};
1790
+ state.latestBySlot = state.latestBySlot || {};
1791
+ if (state.seen[candidate.key])
1792
+ return { persist: false, reason: 'duplicate_memory_key' };
1793
+ state.seen[candidate.key] = at;
1794
+ state.latestBySlot[candidate.slot] = candidate.key;
1795
+ state.updatedAt = at;
1796
+ return { persist: true, reason: 'new_memory_key' };
1797
+ };
1798
+ const dedupeMemoryItemsForLoad = (items = []) => {
1799
+ const list = Array.isArray(items) ? items : [];
1800
+ const seen = new Set();
1801
+ const seenSlots = new Set();
1802
+ const out = [];
1803
+ for (const item of [...list].reverse()) {
1804
+ const candidate = buildMemoryDedupeCandidate({ messages: [{ role: 'system', content: memoryText(item) }], metadata: metadataOf(item) }, {});
1805
+ const slotKey = LEDGER_MEMORY_KINDS.has(candidate.kind) ? candidate.slot : candidate.key;
1806
+ const key = LEDGER_MEMORY_KINDS.has(candidate.kind) ? slotKey : candidate.key;
1807
+ if (LEDGER_MEMORY_KINDS.has(candidate.kind)) {
1808
+ if (seenSlots.has(slotKey))
1809
+ continue;
1810
+ seenSlots.add(slotKey);
1811
+ }
1812
+ else if (seen.has(key)) {
1813
+ continue;
1814
+ }
1815
+ seen.add(key);
1816
+ out.push(item);
1817
+ }
1818
+ out.reverse();
1819
+ return {
1820
+ items: out,
1821
+ summary: cleanContextValue({
1822
+ input: list.length,
1823
+ kept: out.length,
1824
+ removed: Math.max(0, list.length - out.length),
1825
+ }),
1826
+ };
1827
+ };
1635
1828
  const hasCommitIntent = (value = '') => /\b(confirm\w*|confim\w*|cnfirm\w*|cofnirm\w*|ocnfi\w*|ocnfia\w*|fechar|aprovar|finalizar|prosseguir|continuar|sim)\b/.test(normalizeIntentText(value));
1636
1829
  const isConversationRecallQuery = (value = '') => {
1637
1830
  const text = normalizeIntentText(value);
@@ -1859,21 +2052,73 @@ const deriveNextExpectedAction = (intent, operationalState = {}) => {
1859
2052
  return 'continue according to the agent prompt using retrieved context; call tools when the agent prompt or tool policy requires them';
1860
2053
  };
1861
2054
  const isGenericMemoryNextAction = (value = '') => /answer using retrieved context and avoid unnecessary tool calls|continue according to the agent prompt using retrieved context/i.test(String(value || ''));
2055
+ const shouldCarryPreviousGoal = (intent = '', previousGoal = '') => {
2056
+ if (!previousGoal || isGenericMemoryNextAction(previousGoal))
2057
+ return false;
2058
+ return ['general_message', 'profile_update', 'unknown'].includes(intent);
2059
+ };
1862
2060
  const deriveWorkingMemory = ({ query = '', profileFacts = {}, recentMessages = [], toolHistory = [], operationalState = {}, previous = {} }) => {
1863
2061
  const intent = inferUserIntent(query, recentMessages);
1864
- const lastUser = [...(recentMessages || [])].reverse().find((msg) => /^(user|human)$/i.test(String(msg.role || '')));
2062
+ const chronological = sortConversationChronological(recentMessages || []);
2063
+ const userMessages = chronological.filter((msg) => /^(user|human)$/i.test(String(msg.role || '')));
2064
+ const assistantMessages = chronological.filter((msg) => /^(assistant|ai|agent)$/i.test(String(msg.role || '')));
2065
+ const normalizedQuery = normalizeIntentText(query).trim();
2066
+ const lastUser = [...userMessages].reverse().find((msg) => /^(user|human)$/i.test(String(msg.role || '')));
2067
+ const priorUser = [...userMessages].reverse().find((msg) => normalizeIntentText(msg.content).trim() !== normalizedQuery);
2068
+ const lastAssistant = assistantMessages[assistantMessages.length - 1] || null;
1865
2069
  const activeEntities = [];
1866
2070
  for (const key of ['name', 'company', 'email', 'phone']) {
1867
2071
  if (profileFacts && profileFacts[key])
1868
2072
  activeEntities.push({ type: key, value: profileFacts[key] });
1869
2073
  }
1870
2074
  const lastTool = toolHistory && toolHistory.length ? toolHistory[toolHistory.length - 1] : null;
2075
+ const toolState = (operationalState || {}).tool_state || {};
1871
2076
  const nextExpectedAction = deriveNextExpectedAction(intent, operationalState);
2077
+ const carryPreviousGoal = shouldCarryPreviousGoal(intent, previous.current_goal);
2078
+ const currentGoal = carryPreviousGoal ? previous.current_goal : nextExpectedAction;
2079
+ const toolNames = Array.from(new Set((toolHistory || []).map((tool) => tool.name || tool.tool_name || tool.tool).filter(Boolean)));
2080
+ const currentTurnMayNeedTool = ['affirm', 'commit_or_continue', 'tool_action_candidate', 'selection_or_slot'].includes(intent);
1872
2081
  return {
1873
- current_goal: previous.current_goal && !isGenericMemoryNextAction(previous.current_goal) ? previous.current_goal : nextExpectedAction,
2082
+ current_goal: currentGoal,
2083
+ current_goal_reason: carryPreviousGoal ? 'carried_from_previous_non_generic_goal' : 'derived_from_current_turn_intent',
1874
2084
  current_task: nextExpectedAction,
1875
2085
  last_user_intent: intent,
1876
2086
  last_user_message: lastUser ? truncate(lastUser.content, 500) : truncate(query, 500),
2087
+ conversation_digest: cleanContextValue({
2088
+ messages: chronological.length,
2089
+ user_messages: userMessages.length,
2090
+ assistant_messages: assistantMessages.length,
2091
+ current_user: truncate(query, 220),
2092
+ prior_user: priorUser ? { content: truncate(priorUser.content, 180), at: priorUser.at } : undefined,
2093
+ last_assistant: lastAssistant ? { content: truncate(lastAssistant.content, 180), at: lastAssistant.at } : undefined,
2094
+ }),
2095
+ tool_digest: cleanContextValue({
2096
+ total: Array.isArray(toolHistory) ? toolHistory.length : 0,
2097
+ ok: (operationalState || {}).tool_counts?.ok,
2098
+ failed: (operationalState || {}).tool_counts?.failed,
2099
+ names: toolNames.slice(0, 12),
2100
+ last_successful: toolState.last_successful_tool ? {
2101
+ name: toolState.last_successful_tool.name,
2102
+ at: toolState.last_successful_tool.at,
2103
+ status: toolState.last_successful_tool.status || 'ok',
2104
+ } : undefined,
2105
+ failed_by_name: toolState.failed_by_name,
2106
+ }),
2107
+ turn_flags: cleanContextValue({
2108
+ recall_only: intent === 'conversation_recall',
2109
+ status_question: intent === 'operational_status_question',
2110
+ current_turn_may_need_tool: currentTurnMayNeedTool,
2111
+ has_prior_tools: Array.isArray(toolHistory) && toolHistory.length > 0,
2112
+ has_profile_facts: activeEntities.length > 0,
2113
+ carry_previous_goal: carryPreviousGoal,
2114
+ }),
2115
+ context_sources: cleanContextValue({
2116
+ conversation: chronological.length > 0,
2117
+ tool_history: Array.isArray(toolHistory) && toolHistory.length > 0,
2118
+ operational_state: Boolean((operationalState || {}).tool_state || (operationalState || {}).tool_counts),
2119
+ profile_facts: activeEntities.length > 0,
2120
+ }),
2121
+ agent_guidance: nextExpectedAction,
1877
2122
  active_entities: activeEntities,
1878
2123
  open_decisions: previous.open_decisions || [],
1879
2124
  last_error: lastTool && lastTool.ok === false ? { tool: lastTool.name, at: lastTool.at, result: lastTool.result } : null,
@@ -2148,9 +2393,15 @@ const cleanContextValue = (value) => {
2148
2393
  };
2149
2394
  const compactWorkingMemoryForAgent = (memory = {}) => cleanContextValue({
2150
2395
  current_goal: memory.current_goal,
2396
+ current_goal_reason: memory.current_goal_reason,
2151
2397
  current_task: memory.current_task,
2152
2398
  last_user_intent: memory.last_user_intent,
2153
2399
  last_user_message: truncate(memory.last_user_message, 220),
2400
+ conversation_digest: memory.conversation_digest,
2401
+ tool_digest: memory.tool_digest,
2402
+ turn_flags: memory.turn_flags,
2403
+ context_sources: memory.context_sources,
2404
+ agent_guidance: memory.agent_guidance,
2154
2405
  active_entities: memory.active_entities,
2155
2406
  open_decisions: memory.open_decisions,
2156
2407
  last_error: memory.last_error,
@@ -2229,6 +2480,7 @@ const compactSaveAuditForAgent = (capture = {}) => cleanContextValue({
2229
2480
  tool_history_after_save: capture.last_save_tool_history_after_save,
2230
2481
  thread_state_saved: capture.last_save_thread_state_saved,
2231
2482
  backend_memory_persistence: capture.last_save_backend_memory_persistence,
2483
+ dedupe: capture.last_save_dedupe_summary,
2232
2484
  },
2233
2485
  });
2234
2486
  const compactEntityTimelineForAgent = (timeline = [], maxItems = 8) => pruneByLimit(timeline || [], maxItems).map((item) => cleanContextValue({
@@ -2523,6 +2775,7 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
2523
2775
  recentHighlights: Array.isArray(parsed.recentHighlights) ? parsed.recentHighlights.length : undefined,
2524
2776
  });
2525
2777
  summary.lastSave = memoryAudit.last_save || parsed.state?.last_save || undefined;
2778
+ summary.dedupeSummary = parsed.dedupeSummary || parsed.diagnostics?.dedupeSummary || undefined;
2526
2779
  summary.loadedSections = cleanContextValue({
2527
2780
  conversation: Boolean(parsed.conversation),
2528
2781
  summary: Boolean(parsed.summary),
@@ -2543,10 +2796,16 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
2543
2796
  });
2544
2797
  summary.workingMemory = cleanContextValue({
2545
2798
  currentGoal: parsed.workingMemory?.current_goal,
2799
+ currentGoalReason: parsed.workingMemory?.current_goal_reason,
2546
2800
  currentTask: parsed.workingMemory?.current_task,
2547
2801
  nextExpectedAction: parsed.workingMemory?.next_expected_action,
2548
2802
  lastUserIntent: parsed.workingMemory?.last_user_intent,
2549
2803
  lastUserMessage: parsed.workingMemory?.last_user_message ? truncate(parsed.workingMemory.last_user_message, 220) : undefined,
2804
+ conversation: parsed.workingMemory?.conversation_digest,
2805
+ tools: parsed.workingMemory?.tool_digest,
2806
+ turnFlags: parsed.workingMemory?.turn_flags,
2807
+ contextSources: parsed.workingMemory?.context_sources,
2808
+ agentGuidance: parsed.workingMemory?.agent_guidance,
2550
2809
  });
2551
2810
  summary.decisionState = cleanContextValue({
2552
2811
  currentIntent: parsed.decisionState?.current_intent,
@@ -2792,6 +3051,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
2792
3051
  profile: sectionValue('profile_facts'),
2793
3052
  tools: compactToolLedger,
2794
3053
  memoryAudit,
3054
+ dedupeSummary: diagnostics?.dedupeSummary,
2795
3055
  });
2796
3056
  const renderCompactSection = (section) => {
2797
3057
  if (section.value === null || section.value === undefined)
@@ -3311,6 +3571,58 @@ class TemboryMemory {
3311
3571
  const output = cleanAssistantTranscriptText(rawOutput);
3312
3572
  const toolCalls = extractToolCalls(outputValues);
3313
3573
  const saveAt = nowIso();
3574
+ const dedupeState = ensureMemoryDedupeState(store, key);
3575
+ const dedupeSummary = createDedupeSummary();
3576
+ const persistLegacyMemory = async (legacyBody, diagnostics = {}) => {
3577
+ const candidate = buildMemoryDedupeCandidate(legacyBody, diagnostics);
3578
+ const decision = evaluateMemoryDedupeWrite(dedupeState, candidate, saveAt);
3579
+ recordDedupeWriteSummary(dedupeSummary, candidate, decision);
3580
+ if (!decision.persist)
3581
+ return { skipped: true, reason: decision.reason, dedupeKey: candidate.key };
3582
+ const bodyWithDedupe = {
3583
+ ...legacyBody,
3584
+ metadata: cleanContextValue({
3585
+ ...(legacyBody.metadata || {}),
3586
+ memory_dedupe_key: candidate.key,
3587
+ memory_dedupe_slot: candidate.slot,
3588
+ memory_dedupe_strategy: candidate.strategy,
3589
+ }),
3590
+ };
3591
+ return await safePersistLegacyMemory(this, bodyWithDedupe, {
3592
+ ...diagnostics,
3593
+ memory_dedupe_key: candidate.key,
3594
+ memory_dedupe_slot: candidate.slot,
3595
+ });
3596
+ };
3597
+ const finalizeDedupeSummary = async () => {
3598
+ dedupeState.lastSummary = dedupeSummary;
3599
+ dedupeState.updatedAt = nowIso();
3600
+ store.memoryDedupe[key] = mergeMemoryDedupeState(store.memoryDedupe[key], dedupeState);
3601
+ store.captureState[key] = cleanContextValue({
3602
+ ...(store.captureState[key] || {}),
3603
+ last_save_dedupe_summary: dedupeSummary,
3604
+ });
3605
+ const finalThreadStateSaved = await saveThreadState(this, key, threadId, project, {
3606
+ kind: 'tembory.thread_state.v1',
3607
+ threadId,
3608
+ project: project || undefined,
3609
+ updatedAt: nowIso(),
3610
+ recentMessages: recentForTurn,
3611
+ toolHistory: toolHistoryForTurn,
3612
+ profileFacts: store.profileFacts[key] || {},
3613
+ workingMemory: workingMemoryForTurn,
3614
+ decisionState: decisionStateForTurn,
3615
+ memoryCompression: compressionForTurn,
3616
+ operationalState: operationalStateForTurn,
3617
+ captureState: store.captureState[key],
3618
+ memoryDedupe: store.memoryDedupe[key],
3619
+ activeSummary: store.activeSummary[key] || '',
3620
+ });
3621
+ store.captureState[key] = cleanContextValue({
3622
+ ...(store.captureState[key] || {}),
3623
+ last_save_thread_state_saved: (store.captureState[key] || {}).last_save_thread_state_saved || finalThreadStateSaved,
3624
+ });
3625
+ };
3314
3626
  store.captureState[key] = {
3315
3627
  last_save_status: 'started',
3316
3628
  last_save_saved: false,
@@ -3324,6 +3636,7 @@ class TemboryMemory {
3324
3636
  last_save_tool_calls_captured: toolCalls.length,
3325
3637
  last_save_tool_names: toolCalls.map((tool) => tool.name).filter(Boolean).slice(0, 20),
3326
3638
  last_save_capture_sources: Array.from(new Set(toolCalls.map((tool) => tool.source).filter(Boolean))),
3639
+ last_save_dedupe_summary: dedupeSummary,
3327
3640
  };
3328
3641
  const recentForTembory = [];
3329
3642
  const profileFromTurn = extractProfileFactsFromText(input, 'user_message', nowIso());
@@ -3424,6 +3737,7 @@ class TemboryMemory {
3424
3737
  : adv.useVectorMemory === false
3425
3738
  ? 'thread_state_only'
3426
3739
  : 'enabled',
3740
+ last_save_dedupe_summary: dedupeSummary,
3427
3741
  });
3428
3742
  const threadStateSaved = await saveThreadState(this, key, threadId, project, {
3429
3743
  kind: 'tembory.thread_state.v1',
@@ -3438,6 +3752,7 @@ class TemboryMemory {
3438
3752
  memoryCompression: compressionForTurn,
3439
3753
  operationalState: operationalStateForTurn,
3440
3754
  captureState: nextCaptureState,
3755
+ memoryDedupe: dedupeState,
3441
3756
  activeSummary: store.activeSummary[key] || '',
3442
3757
  });
3443
3758
  store.captureState[key] = cleanContextValue({
@@ -3449,10 +3764,14 @@ class TemboryMemory {
3449
3764
  globalData.__dataChanged = true;
3450
3765
  }
3451
3766
  catch { }
3452
- if (adv.persistBackendMemories === false)
3767
+ if (adv.persistBackendMemories === false) {
3768
+ await finalizeDedupeSummary();
3453
3769
  return;
3454
- if (adv.useVectorMemory === false)
3770
+ }
3771
+ if (adv.useVectorMemory === false) {
3772
+ await finalizeDedupeSummary();
3455
3773
  return;
3774
+ }
3456
3775
  const connectedEmbedding = await getConnectedEmbedding(this, itemIndex);
3457
3776
  if (connectedEmbedding) {
3458
3777
  const ids = { user_id: body.user_id, agent_id: body.agent_id, run_id: body.run_id };
@@ -3465,21 +3784,32 @@ class TemboryMemory {
3465
3784
  memoryCompression: compressionForTurn,
3466
3785
  operationalState: operationalStateForTurn,
3467
3786
  });
3468
- await saveClientVectorMemories(this, [
3469
- await createClientVectorMemory(connectedEmbedding, archiveText, {
3470
- kind: 'turn_archive',
3471
- thread_id: threadId,
3472
- project: project || undefined,
3473
- source: 'n8n_connected_embedding',
3474
- generated_at: nowIso(),
3475
- message_count: recentForTurn.length,
3476
- tool_count: toolHistoryForTurn.length,
3477
- latest_tool: toolHistoryForTurn.at(-1)?.name || null,
3478
- }, ids),
3479
- ], ids);
3787
+ const archiveMetadata = {
3788
+ kind: 'turn_archive',
3789
+ thread_id: threadId,
3790
+ project: project || undefined,
3791
+ source: 'n8n_connected_embedding',
3792
+ generated_at: nowIso(),
3793
+ message_count: recentForTurn.length,
3794
+ tool_count: toolHistoryForTurn.length,
3795
+ latest_tool: toolHistoryForTurn.at(-1)?.name || null,
3796
+ };
3797
+ const archiveCandidate = buildMemoryDedupeCandidate({ messages: [{ role: 'system', content: archiveText }], metadata: archiveMetadata }, { kind: 'turn_archive', user_id: body.user_id });
3798
+ const archiveDecision = evaluateMemoryDedupeWrite(dedupeState, archiveCandidate, saveAt);
3799
+ recordDedupeWriteSummary(dedupeSummary, archiveCandidate, archiveDecision);
3800
+ if (archiveDecision.persist) {
3801
+ await saveClientVectorMemories(this, [
3802
+ await createClientVectorMemory(connectedEmbedding, archiveText, {
3803
+ ...archiveMetadata,
3804
+ memory_dedupe_key: archiveCandidate.key,
3805
+ memory_dedupe_slot: archiveCandidate.slot,
3806
+ memory_dedupe_strategy: archiveCandidate.strategy,
3807
+ }, ids),
3808
+ ], ids);
3809
+ }
3480
3810
  if (adv.includeRecentMessages !== false && recentForTembory.length) {
3481
3811
  for (const recent of recentForTembory) {
3482
- await safePersistLegacyMemory(this, {
3812
+ await persistLegacyMemory({
3483
3813
  messages: [{ role: 'system', content: encodeRecentMessage(recent, threadId) }],
3484
3814
  infer: false,
3485
3815
  user_id: body.user_id,
@@ -3497,9 +3827,10 @@ class TemboryMemory {
3497
3827
  }, {
3498
3828
  kind: 'recent_message',
3499
3829
  user_id: body.user_id,
3830
+ thread_id: threadId,
3500
3831
  });
3501
3832
  }
3502
- await safePersistLegacyMemory(this, {
3833
+ await persistLegacyMemory({
3503
3834
  messages: [{ role: 'system', content: encodeConversationLedger(recentForTurn, threadId) }],
3504
3835
  infer: false,
3505
3836
  user_id: body.user_id,
@@ -3515,11 +3846,12 @@ class TemboryMemory {
3515
3846
  }, {
3516
3847
  kind: 'conversation_ledger',
3517
3848
  user_id: body.user_id,
3849
+ thread_id: threadId,
3518
3850
  });
3519
3851
  }
3520
3852
  if (adv.includeToolHistory !== false && toolCalls.length) {
3521
3853
  for (const tool of toolCalls) {
3522
- await safePersistLegacyMemory(this, {
3854
+ await persistLegacyMemory({
3523
3855
  messages: [{ role: 'system', content: encodeToolCall(tool, threadId) }],
3524
3856
  infer: false,
3525
3857
  user_id: body.user_id,
@@ -3543,9 +3875,10 @@ class TemboryMemory {
3543
3875
  kind: 'tool_history',
3544
3876
  user_id: body.user_id,
3545
3877
  tool: tool.name,
3878
+ thread_id: threadId,
3546
3879
  });
3547
3880
  }
3548
- await safePersistLegacyMemory(this, {
3881
+ await persistLegacyMemory({
3549
3882
  messages: [{ role: 'system', content: encodeToolLedger(toolHistoryForTurn, threadId) }],
3550
3883
  infer: false,
3551
3884
  user_id: body.user_id,
@@ -3561,18 +3894,21 @@ class TemboryMemory {
3561
3894
  }, {
3562
3895
  kind: 'tool_ledger',
3563
3896
  user_id: body.user_id,
3897
+ thread_id: threadId,
3564
3898
  });
3565
3899
  }
3900
+ await finalizeDedupeSummary();
3566
3901
  return;
3567
3902
  }
3568
3903
  if (messages.length)
3569
- await safePersistLegacyMemory(this, body, {
3904
+ await persistLegacyMemory(body, {
3570
3905
  kind: 'conversation_messages',
3571
3906
  user_id: body.user_id,
3907
+ thread_id: threadId,
3572
3908
  });
3573
3909
  if (adv.includeRecentMessages !== false && recentForTembory.length) {
3574
3910
  for (const recent of recentForTembory) {
3575
- await safePersistLegacyMemory(this, {
3911
+ await persistLegacyMemory({
3576
3912
  messages: [{ role: 'system', content: encodeRecentMessage(recent, threadId) }],
3577
3913
  infer: false,
3578
3914
  user_id: body.user_id,
@@ -3589,9 +3925,10 @@ class TemboryMemory {
3589
3925
  }, {
3590
3926
  kind: 'recent_message',
3591
3927
  user_id: body.user_id,
3928
+ thread_id: threadId,
3592
3929
  });
3593
3930
  }
3594
- await safePersistLegacyMemory(this, {
3931
+ await persistLegacyMemory({
3595
3932
  messages: [{ role: 'system', content: encodeConversationLedger(recentForTurn, threadId) }],
3596
3933
  infer: false,
3597
3934
  user_id: body.user_id,
@@ -3607,11 +3944,12 @@ class TemboryMemory {
3607
3944
  }, {
3608
3945
  kind: 'conversation_ledger',
3609
3946
  user_id: body.user_id,
3947
+ thread_id: threadId,
3610
3948
  });
3611
3949
  }
3612
3950
  if (adv.includeToolHistory !== false && !adv.persistToolFactsToTembory && toolCalls.length) {
3613
3951
  for (const tool of toolCalls) {
3614
- await safePersistLegacyMemory(this, {
3952
+ await persistLegacyMemory({
3615
3953
  messages: [{ role: 'system', content: encodeToolCall(tool, threadId) }],
3616
3954
  infer: false,
3617
3955
  user_id: body.user_id,
@@ -3635,9 +3973,10 @@ class TemboryMemory {
3635
3973
  kind: 'tool_history',
3636
3974
  user_id: body.user_id,
3637
3975
  tool: tool.name,
3976
+ thread_id: threadId,
3638
3977
  });
3639
3978
  }
3640
- await safePersistLegacyMemory(this, {
3979
+ await persistLegacyMemory({
3641
3980
  messages: [{ role: 'system', content: encodeToolLedger(toolHistoryForTurn, threadId) }],
3642
3981
  infer: false,
3643
3982
  user_id: body.user_id,
@@ -3653,11 +3992,12 @@ class TemboryMemory {
3653
3992
  }, {
3654
3993
  kind: 'tool_ledger',
3655
3994
  user_id: body.user_id,
3995
+ thread_id: threadId,
3656
3996
  });
3657
3997
  }
3658
3998
  if (adv.persistToolFactsToTembory && toolCalls.length) {
3659
3999
  const facts = toolCalls.map((tool) => `Tool ${tool.name} input=${tool.input}${tool.result ? ` result=${tool.result}` : ''}`).join('\n');
3660
- await safePersistLegacyMemory(this, {
4000
+ await persistLegacyMemory({
3661
4001
  messages: [{ role: 'system', content: `Tool facts (read-only):\n${truncate(facts, 2000)}` }],
3662
4002
  infer: false,
3663
4003
  user_id: body.user_id,
@@ -3665,9 +4005,10 @@ class TemboryMemory {
3665
4005
  }, {
3666
4006
  kind: 'tool_facts',
3667
4007
  user_id: body.user_id,
4008
+ thread_id: threadId,
3668
4009
  });
3669
4010
  for (const tool of toolCalls) {
3670
- await safePersistLegacyMemory(this, {
4011
+ await persistLegacyMemory({
3671
4012
  messages: [{ role: 'system', content: encodeToolCall(tool, threadId) }],
3672
4013
  infer: false,
3673
4014
  user_id: body.user_id,
@@ -3690,9 +4031,29 @@ class TemboryMemory {
3690
4031
  kind: 'tool_history',
3691
4032
  user_id: body.user_id,
3692
4033
  tool: tool.name,
4034
+ thread_id: threadId,
3693
4035
  });
3694
4036
  }
4037
+ await persistLegacyMemory({
4038
+ messages: [{ role: 'system', content: encodeToolLedger(toolHistoryForTurn, threadId) }],
4039
+ infer: false,
4040
+ user_id: body.user_id,
4041
+ agent_id: body.agent_id,
4042
+ run_id: body.run_id,
4043
+ metadata: {
4044
+ kind: 'tool_ledger',
4045
+ thread_id: threadId,
4046
+ project: project || undefined,
4047
+ source: 'tembory_transcript',
4048
+ generated_at: nowIso(),
4049
+ },
4050
+ }, {
4051
+ kind: 'tool_ledger',
4052
+ user_id: body.user_id,
4053
+ thread_id: threadId,
4054
+ });
3695
4055
  }
4056
+ await finalizeDedupeSummary();
3696
4057
  }
3697
4058
  async loadMemoryVariablesForItem(itemIndex, inputValues = {}) {
3698
4059
  var _a, _b, _c, _d, _e, _f, _g;
@@ -3744,6 +4105,11 @@ class TemboryMemory {
3744
4105
  catch { }
3745
4106
  let payload;
3746
4107
  let vectorMemories = [];
4108
+ const loadDedupeSummary = {
4109
+ enabled: true,
4110
+ load: {},
4111
+ lastSave: store.memoryDedupe[key]?.lastSummary,
4112
+ };
3747
4113
  if (vectorMemoryEnabled && (retrievalMode === 'semantic' || retrievalMode === 'semanticV2' || retrievalMode === 'hybrid')) {
3748
4114
  const body = { query: String(query || '') };
3749
4115
  // IDs
@@ -4004,6 +4370,12 @@ class TemboryMemory {
4004
4370
  connectedAi.errors.push(`persistedContext.load: ${error.message || String(error)}`);
4005
4371
  }
4006
4372
  }
4373
+ const vectorDedupe = dedupeMemoryItemsForLoad(vectorMemories);
4374
+ vectorMemories = vectorDedupe.items;
4375
+ loadDedupeSummary.load.vectorMemories = vectorDedupe.summary;
4376
+ const persistedDedupe = dedupeMemoryItemsForLoad(persistedMemoryItems);
4377
+ persistedMemoryItems = persistedDedupe.items;
4378
+ loadDedupeSummary.load.persistedMemories = persistedDedupe.summary;
4007
4379
  if (adv.includeRecentMessages !== false) {
4008
4380
  persistedRecentMessages = persistedMemoryItems
4009
4381
  .flatMap(recentMessagesFromMemory)
@@ -4018,7 +4390,13 @@ class TemboryMemory {
4018
4390
  .sort((a, b) => new Date(a.at || 0).getTime() - new Date(b.at || 0).getTime());
4019
4391
  }
4020
4392
  const storedRecentMessages = adv.includeRecentMessages === false ? [] : (store.recentMessages[key] || []).concat(persistedRecentMessages, vectorRecentMessages);
4021
- const allRecentMessages = dedupeRecentMessages(appendCurrentUserMessage(storedRecentMessages, query));
4393
+ const appendedRecentMessages = appendCurrentUserMessage(storedRecentMessages, query);
4394
+ const allRecentMessages = dedupeRecentMessages(appendedRecentMessages);
4395
+ loadDedupeSummary.load.recentMessages = cleanContextValue({
4396
+ input: appendedRecentMessages.length,
4397
+ kept: allRecentMessages.length,
4398
+ removed: Math.max(0, appendedRecentMessages.length - allRecentMessages.length),
4399
+ });
4022
4400
  const recentMessages = pruneConversationMessagesPreserveAnchors(allRecentMessages, Math.max(Number(adv.recentMessagesLastN || 8), 8));
4023
4401
  const toolHistoryFromRecentMessages = [];
4024
4402
  if (adv.includeToolHistory !== false) {
@@ -4027,7 +4405,13 @@ class TemboryMemory {
4027
4405
  }
4028
4406
  const toolHistoryFromVectorMarkers = adv.includeToolHistory === false ? [] : vectorMemories.flatMap(explicitToolHistoryItemsFromMemory);
4029
4407
  const toolHistoryFromVectorMemories = adv.includeToolHistory === false || adv.includeToolHistorySemanticFallback !== true ? [] : vectorMemories.flatMap(toolHistoryItemsFromMemory);
4030
- const toolHistory = adv.includeToolHistory === false ? [] : applyToolHistoryWindow((store.toolHistory[key] || []).concat(persistedToolHistory, toolHistoryFromRecentMessages, toolHistoryFromVectorMarkers, toolHistoryFromVectorMemories), adv.toolHistoryTTLSeconds, adv.toolHistoryLastN || 15);
4408
+ const combinedToolHistory = (store.toolHistory[key] || []).concat(persistedToolHistory, toolHistoryFromRecentMessages, toolHistoryFromVectorMarkers, toolHistoryFromVectorMemories);
4409
+ const toolHistory = adv.includeToolHistory === false ? [] : applyToolHistoryWindow(combinedToolHistory, adv.toolHistoryTTLSeconds, adv.toolHistoryLastN || 15);
4410
+ loadDedupeSummary.load.toolHistory = cleanContextValue({
4411
+ input: adv.includeToolHistory === false ? 0 : combinedToolHistory.length,
4412
+ kept: toolHistory.length,
4413
+ removed: Math.max(0, (adv.includeToolHistory === false ? 0 : combinedToolHistory.length) - toolHistory.length),
4414
+ });
4031
4415
  const highlights = adv.includeRecentHighlights === false ? [] : getRecentHighlights(recentMessages, toolHistory, adv.recentHighlightsMaxItems || 6);
4032
4416
  const profileFacts = adv.includeProfileFacts === false ? {} : mergeProfileFacts(store.profileFacts[key], profileFactsFromMessages(allRecentMessages), profileFactsFromMemories(vectorMemories));
4033
4417
  if (adv.includeProfileFacts !== false && Object.keys(profileFacts).length) {
@@ -4157,6 +4541,7 @@ class TemboryMemory {
4157
4541
  },
4158
4542
  connectedAi,
4159
4543
  activeSummary: summaryDiagnostics,
4544
+ dedupeSummary: cleanContextValue(loadDedupeSummary),
4160
4545
  };
4161
4546
  const contextHealth = deriveContextHealth({
4162
4547
  userId: key,
@@ -4296,6 +4681,7 @@ class TemboryMemory {
4296
4681
  })),
4297
4682
  },
4298
4683
  diagnostics,
4684
+ dedupeSummary: diagnostics.dedupeSummary,
4299
4685
  };
4300
4686
  return {
4301
4687
  response: {
@@ -4429,6 +4815,10 @@ exports.__private = {
4429
4815
  deriveOperationalState,
4430
4816
  deriveActionLedger,
4431
4817
  deriveEntityTimeline,
4818
+ buildMemoryDedupeCandidate,
4819
+ dedupeMemoryItemsForLoad,
4820
+ mergeMemoryDedupeState,
4821
+ evaluateMemoryDedupeWrite,
4432
4822
  scoreOf,
4433
4823
  scoreMetaOf,
4434
4824
  extractProfileFactsFromText,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tembory",
3
- "version": "1.1.25",
3
+ "version": "1.1.27",
4
4
  "description": "Tembory node for n8n AI Agents with operational memory, tool history and decision state",
5
5
  "license": "MIT",
6
6
  "homepage": "https://tembory.com",