n8n-nodes-tembory 1.1.26 → 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);
@@ -2287,6 +2480,7 @@ const compactSaveAuditForAgent = (capture = {}) => cleanContextValue({
2287
2480
  tool_history_after_save: capture.last_save_tool_history_after_save,
2288
2481
  thread_state_saved: capture.last_save_thread_state_saved,
2289
2482
  backend_memory_persistence: capture.last_save_backend_memory_persistence,
2483
+ dedupe: capture.last_save_dedupe_summary,
2290
2484
  },
2291
2485
  });
2292
2486
  const compactEntityTimelineForAgent = (timeline = [], maxItems = 8) => pruneByLimit(timeline || [], maxItems).map((item) => cleanContextValue({
@@ -2581,6 +2775,7 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
2581
2775
  recentHighlights: Array.isArray(parsed.recentHighlights) ? parsed.recentHighlights.length : undefined,
2582
2776
  });
2583
2777
  summary.lastSave = memoryAudit.last_save || parsed.state?.last_save || undefined;
2778
+ summary.dedupeSummary = parsed.dedupeSummary || parsed.diagnostics?.dedupeSummary || undefined;
2584
2779
  summary.loadedSections = cleanContextValue({
2585
2780
  conversation: Boolean(parsed.conversation),
2586
2781
  summary: Boolean(parsed.summary),
@@ -2856,6 +3051,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
2856
3051
  profile: sectionValue('profile_facts'),
2857
3052
  tools: compactToolLedger,
2858
3053
  memoryAudit,
3054
+ dedupeSummary: diagnostics?.dedupeSummary,
2859
3055
  });
2860
3056
  const renderCompactSection = (section) => {
2861
3057
  if (section.value === null || section.value === undefined)
@@ -3375,6 +3571,58 @@ class TemboryMemory {
3375
3571
  const output = cleanAssistantTranscriptText(rawOutput);
3376
3572
  const toolCalls = extractToolCalls(outputValues);
3377
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
+ };
3378
3626
  store.captureState[key] = {
3379
3627
  last_save_status: 'started',
3380
3628
  last_save_saved: false,
@@ -3388,6 +3636,7 @@ class TemboryMemory {
3388
3636
  last_save_tool_calls_captured: toolCalls.length,
3389
3637
  last_save_tool_names: toolCalls.map((tool) => tool.name).filter(Boolean).slice(0, 20),
3390
3638
  last_save_capture_sources: Array.from(new Set(toolCalls.map((tool) => tool.source).filter(Boolean))),
3639
+ last_save_dedupe_summary: dedupeSummary,
3391
3640
  };
3392
3641
  const recentForTembory = [];
3393
3642
  const profileFromTurn = extractProfileFactsFromText(input, 'user_message', nowIso());
@@ -3488,6 +3737,7 @@ class TemboryMemory {
3488
3737
  : adv.useVectorMemory === false
3489
3738
  ? 'thread_state_only'
3490
3739
  : 'enabled',
3740
+ last_save_dedupe_summary: dedupeSummary,
3491
3741
  });
3492
3742
  const threadStateSaved = await saveThreadState(this, key, threadId, project, {
3493
3743
  kind: 'tembory.thread_state.v1',
@@ -3502,6 +3752,7 @@ class TemboryMemory {
3502
3752
  memoryCompression: compressionForTurn,
3503
3753
  operationalState: operationalStateForTurn,
3504
3754
  captureState: nextCaptureState,
3755
+ memoryDedupe: dedupeState,
3505
3756
  activeSummary: store.activeSummary[key] || '',
3506
3757
  });
3507
3758
  store.captureState[key] = cleanContextValue({
@@ -3513,10 +3764,14 @@ class TemboryMemory {
3513
3764
  globalData.__dataChanged = true;
3514
3765
  }
3515
3766
  catch { }
3516
- if (adv.persistBackendMemories === false)
3767
+ if (adv.persistBackendMemories === false) {
3768
+ await finalizeDedupeSummary();
3517
3769
  return;
3518
- if (adv.useVectorMemory === false)
3770
+ }
3771
+ if (adv.useVectorMemory === false) {
3772
+ await finalizeDedupeSummary();
3519
3773
  return;
3774
+ }
3520
3775
  const connectedEmbedding = await getConnectedEmbedding(this, itemIndex);
3521
3776
  if (connectedEmbedding) {
3522
3777
  const ids = { user_id: body.user_id, agent_id: body.agent_id, run_id: body.run_id };
@@ -3529,21 +3784,32 @@ class TemboryMemory {
3529
3784
  memoryCompression: compressionForTurn,
3530
3785
  operationalState: operationalStateForTurn,
3531
3786
  });
3532
- await saveClientVectorMemories(this, [
3533
- await createClientVectorMemory(connectedEmbedding, archiveText, {
3534
- kind: 'turn_archive',
3535
- thread_id: threadId,
3536
- project: project || undefined,
3537
- source: 'n8n_connected_embedding',
3538
- generated_at: nowIso(),
3539
- message_count: recentForTurn.length,
3540
- tool_count: toolHistoryForTurn.length,
3541
- latest_tool: toolHistoryForTurn.at(-1)?.name || null,
3542
- }, ids),
3543
- ], 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
+ }
3544
3810
  if (adv.includeRecentMessages !== false && recentForTembory.length) {
3545
3811
  for (const recent of recentForTembory) {
3546
- await safePersistLegacyMemory(this, {
3812
+ await persistLegacyMemory({
3547
3813
  messages: [{ role: 'system', content: encodeRecentMessage(recent, threadId) }],
3548
3814
  infer: false,
3549
3815
  user_id: body.user_id,
@@ -3561,9 +3827,10 @@ class TemboryMemory {
3561
3827
  }, {
3562
3828
  kind: 'recent_message',
3563
3829
  user_id: body.user_id,
3830
+ thread_id: threadId,
3564
3831
  });
3565
3832
  }
3566
- await safePersistLegacyMemory(this, {
3833
+ await persistLegacyMemory({
3567
3834
  messages: [{ role: 'system', content: encodeConversationLedger(recentForTurn, threadId) }],
3568
3835
  infer: false,
3569
3836
  user_id: body.user_id,
@@ -3579,11 +3846,12 @@ class TemboryMemory {
3579
3846
  }, {
3580
3847
  kind: 'conversation_ledger',
3581
3848
  user_id: body.user_id,
3849
+ thread_id: threadId,
3582
3850
  });
3583
3851
  }
3584
3852
  if (adv.includeToolHistory !== false && toolCalls.length) {
3585
3853
  for (const tool of toolCalls) {
3586
- await safePersistLegacyMemory(this, {
3854
+ await persistLegacyMemory({
3587
3855
  messages: [{ role: 'system', content: encodeToolCall(tool, threadId) }],
3588
3856
  infer: false,
3589
3857
  user_id: body.user_id,
@@ -3607,9 +3875,10 @@ class TemboryMemory {
3607
3875
  kind: 'tool_history',
3608
3876
  user_id: body.user_id,
3609
3877
  tool: tool.name,
3878
+ thread_id: threadId,
3610
3879
  });
3611
3880
  }
3612
- await safePersistLegacyMemory(this, {
3881
+ await persistLegacyMemory({
3613
3882
  messages: [{ role: 'system', content: encodeToolLedger(toolHistoryForTurn, threadId) }],
3614
3883
  infer: false,
3615
3884
  user_id: body.user_id,
@@ -3625,18 +3894,21 @@ class TemboryMemory {
3625
3894
  }, {
3626
3895
  kind: 'tool_ledger',
3627
3896
  user_id: body.user_id,
3897
+ thread_id: threadId,
3628
3898
  });
3629
3899
  }
3900
+ await finalizeDedupeSummary();
3630
3901
  return;
3631
3902
  }
3632
3903
  if (messages.length)
3633
- await safePersistLegacyMemory(this, body, {
3904
+ await persistLegacyMemory(body, {
3634
3905
  kind: 'conversation_messages',
3635
3906
  user_id: body.user_id,
3907
+ thread_id: threadId,
3636
3908
  });
3637
3909
  if (adv.includeRecentMessages !== false && recentForTembory.length) {
3638
3910
  for (const recent of recentForTembory) {
3639
- await safePersistLegacyMemory(this, {
3911
+ await persistLegacyMemory({
3640
3912
  messages: [{ role: 'system', content: encodeRecentMessage(recent, threadId) }],
3641
3913
  infer: false,
3642
3914
  user_id: body.user_id,
@@ -3653,9 +3925,10 @@ class TemboryMemory {
3653
3925
  }, {
3654
3926
  kind: 'recent_message',
3655
3927
  user_id: body.user_id,
3928
+ thread_id: threadId,
3656
3929
  });
3657
3930
  }
3658
- await safePersistLegacyMemory(this, {
3931
+ await persistLegacyMemory({
3659
3932
  messages: [{ role: 'system', content: encodeConversationLedger(recentForTurn, threadId) }],
3660
3933
  infer: false,
3661
3934
  user_id: body.user_id,
@@ -3671,11 +3944,12 @@ class TemboryMemory {
3671
3944
  }, {
3672
3945
  kind: 'conversation_ledger',
3673
3946
  user_id: body.user_id,
3947
+ thread_id: threadId,
3674
3948
  });
3675
3949
  }
3676
3950
  if (adv.includeToolHistory !== false && !adv.persistToolFactsToTembory && toolCalls.length) {
3677
3951
  for (const tool of toolCalls) {
3678
- await safePersistLegacyMemory(this, {
3952
+ await persistLegacyMemory({
3679
3953
  messages: [{ role: 'system', content: encodeToolCall(tool, threadId) }],
3680
3954
  infer: false,
3681
3955
  user_id: body.user_id,
@@ -3699,9 +3973,10 @@ class TemboryMemory {
3699
3973
  kind: 'tool_history',
3700
3974
  user_id: body.user_id,
3701
3975
  tool: tool.name,
3976
+ thread_id: threadId,
3702
3977
  });
3703
3978
  }
3704
- await safePersistLegacyMemory(this, {
3979
+ await persistLegacyMemory({
3705
3980
  messages: [{ role: 'system', content: encodeToolLedger(toolHistoryForTurn, threadId) }],
3706
3981
  infer: false,
3707
3982
  user_id: body.user_id,
@@ -3717,11 +3992,12 @@ class TemboryMemory {
3717
3992
  }, {
3718
3993
  kind: 'tool_ledger',
3719
3994
  user_id: body.user_id,
3995
+ thread_id: threadId,
3720
3996
  });
3721
3997
  }
3722
3998
  if (adv.persistToolFactsToTembory && toolCalls.length) {
3723
3999
  const facts = toolCalls.map((tool) => `Tool ${tool.name} input=${tool.input}${tool.result ? ` result=${tool.result}` : ''}`).join('\n');
3724
- await safePersistLegacyMemory(this, {
4000
+ await persistLegacyMemory({
3725
4001
  messages: [{ role: 'system', content: `Tool facts (read-only):\n${truncate(facts, 2000)}` }],
3726
4002
  infer: false,
3727
4003
  user_id: body.user_id,
@@ -3729,9 +4005,10 @@ class TemboryMemory {
3729
4005
  }, {
3730
4006
  kind: 'tool_facts',
3731
4007
  user_id: body.user_id,
4008
+ thread_id: threadId,
3732
4009
  });
3733
4010
  for (const tool of toolCalls) {
3734
- await safePersistLegacyMemory(this, {
4011
+ await persistLegacyMemory({
3735
4012
  messages: [{ role: 'system', content: encodeToolCall(tool, threadId) }],
3736
4013
  infer: false,
3737
4014
  user_id: body.user_id,
@@ -3754,9 +4031,29 @@ class TemboryMemory {
3754
4031
  kind: 'tool_history',
3755
4032
  user_id: body.user_id,
3756
4033
  tool: tool.name,
4034
+ thread_id: threadId,
3757
4035
  });
3758
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
+ });
3759
4055
  }
4056
+ await finalizeDedupeSummary();
3760
4057
  }
3761
4058
  async loadMemoryVariablesForItem(itemIndex, inputValues = {}) {
3762
4059
  var _a, _b, _c, _d, _e, _f, _g;
@@ -3808,6 +4105,11 @@ class TemboryMemory {
3808
4105
  catch { }
3809
4106
  let payload;
3810
4107
  let vectorMemories = [];
4108
+ const loadDedupeSummary = {
4109
+ enabled: true,
4110
+ load: {},
4111
+ lastSave: store.memoryDedupe[key]?.lastSummary,
4112
+ };
3811
4113
  if (vectorMemoryEnabled && (retrievalMode === 'semantic' || retrievalMode === 'semanticV2' || retrievalMode === 'hybrid')) {
3812
4114
  const body = { query: String(query || '') };
3813
4115
  // IDs
@@ -4068,6 +4370,12 @@ class TemboryMemory {
4068
4370
  connectedAi.errors.push(`persistedContext.load: ${error.message || String(error)}`);
4069
4371
  }
4070
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;
4071
4379
  if (adv.includeRecentMessages !== false) {
4072
4380
  persistedRecentMessages = persistedMemoryItems
4073
4381
  .flatMap(recentMessagesFromMemory)
@@ -4082,7 +4390,13 @@ class TemboryMemory {
4082
4390
  .sort((a, b) => new Date(a.at || 0).getTime() - new Date(b.at || 0).getTime());
4083
4391
  }
4084
4392
  const storedRecentMessages = adv.includeRecentMessages === false ? [] : (store.recentMessages[key] || []).concat(persistedRecentMessages, vectorRecentMessages);
4085
- 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
+ });
4086
4400
  const recentMessages = pruneConversationMessagesPreserveAnchors(allRecentMessages, Math.max(Number(adv.recentMessagesLastN || 8), 8));
4087
4401
  const toolHistoryFromRecentMessages = [];
4088
4402
  if (adv.includeToolHistory !== false) {
@@ -4091,7 +4405,13 @@ class TemboryMemory {
4091
4405
  }
4092
4406
  const toolHistoryFromVectorMarkers = adv.includeToolHistory === false ? [] : vectorMemories.flatMap(explicitToolHistoryItemsFromMemory);
4093
4407
  const toolHistoryFromVectorMemories = adv.includeToolHistory === false || adv.includeToolHistorySemanticFallback !== true ? [] : vectorMemories.flatMap(toolHistoryItemsFromMemory);
4094
- 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
+ });
4095
4415
  const highlights = adv.includeRecentHighlights === false ? [] : getRecentHighlights(recentMessages, toolHistory, adv.recentHighlightsMaxItems || 6);
4096
4416
  const profileFacts = adv.includeProfileFacts === false ? {} : mergeProfileFacts(store.profileFacts[key], profileFactsFromMessages(allRecentMessages), profileFactsFromMemories(vectorMemories));
4097
4417
  if (adv.includeProfileFacts !== false && Object.keys(profileFacts).length) {
@@ -4221,6 +4541,7 @@ class TemboryMemory {
4221
4541
  },
4222
4542
  connectedAi,
4223
4543
  activeSummary: summaryDiagnostics,
4544
+ dedupeSummary: cleanContextValue(loadDedupeSummary),
4224
4545
  };
4225
4546
  const contextHealth = deriveContextHealth({
4226
4547
  userId: key,
@@ -4360,6 +4681,7 @@ class TemboryMemory {
4360
4681
  })),
4361
4682
  },
4362
4683
  diagnostics,
4684
+ dedupeSummary: diagnostics.dedupeSummary,
4363
4685
  };
4364
4686
  return {
4365
4687
  response: {
@@ -4493,6 +4815,10 @@ exports.__private = {
4493
4815
  deriveOperationalState,
4494
4816
  deriveActionLedger,
4495
4817
  deriveEntityTimeline,
4818
+ buildMemoryDedupeCandidate,
4819
+ dedupeMemoryItemsForLoad,
4820
+ mergeMemoryDedupeState,
4821
+ evaluateMemoryDedupeWrite,
4496
4822
  scoreOf,
4497
4823
  scoreMetaOf,
4498
4824
  extractProfileFactsFromText,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-tembory",
3
- "version": "1.1.26",
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",