n8n-nodes-tembory 1.1.26 → 1.1.28

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.
@@ -153,38 +153,39 @@ const currentInputJsonFromContext = (ctx, itemIndex = 0) => {
153
153
  return {};
154
154
  }
155
155
  };
156
- const resolveCurrentTurnQuery = (ctx, itemIndex = 0, inputValues = {}, queryParam = '') => {
156
+ const resolveCurrentTurnQueryWithSource = (ctx, itemIndex = 0, inputValues = {}, queryParam = '') => {
157
157
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
158
158
  const inputJson = currentInputJsonFromContext(ctx, itemIndex);
159
159
  const candidates = [
160
- queryParam,
161
- inputValues.query,
162
- inputValues.input,
163
- inputValues.chatInput,
164
- inputValues.text,
165
- inputValues.question,
166
- inputValues.message,
167
- inputJson.query,
168
- inputJson.lastUserMessage,
169
- inputJson.chatInput,
170
- inputJson.input,
171
- inputJson.text,
172
- inputJson.message,
173
- (_a = inputJson.body) === null || _a === void 0 ? void 0 : _a.consolidated_text,
174
- (_b = inputJson.body) === null || _b === void 0 ? void 0 : _b.chatInput,
175
- (_c = inputJson.body) === null || _c === void 0 ? void 0 : _c.message,
176
- (_d = inputJson.body) === null || _d === void 0 ? void 0 : _d.text,
177
- (_e = inputJson.body) === null || _e === void 0 ? void 0 : _e.input,
178
- (_h = (_g = (_f = inputJson.body) === null || _f === void 0 ? void 0 : _f.messages) === null || _g === void 0 ? void 0 : _g[0]) === null || _h === void 0 ? void 0 : _h.text,
179
- (_k = (_j = inputJson.body) === null || _j === void 0 ? void 0 : _j.messages) === null || _k === void 0 ? void 0 : _k[0],
160
+ ['query_param', queryParam],
161
+ ['input_values.query', inputValues.query],
162
+ ['input_values.input', inputValues.input],
163
+ ['input_values.chatInput', inputValues.chatInput],
164
+ ['input_values.text', inputValues.text],
165
+ ['input_values.question', inputValues.question],
166
+ ['input_values.message', inputValues.message],
167
+ ['input_json.query', inputJson.query],
168
+ ['input_json.lastUserMessage', inputJson.lastUserMessage],
169
+ ['input_json.chatInput', inputJson.chatInput],
170
+ ['input_json.input', inputJson.input],
171
+ ['input_json.text', inputJson.text],
172
+ ['input_json.message', inputJson.message],
173
+ ['input_json.body.consolidated_text', (_a = inputJson.body) === null || _a === void 0 ? void 0 : _a.consolidated_text],
174
+ ['input_json.body.chatInput', (_b = inputJson.body) === null || _b === void 0 ? void 0 : _b.chatInput],
175
+ ['input_json.body.message', (_c = inputJson.body) === null || _c === void 0 ? void 0 : _c.message],
176
+ ['input_json.body.text', (_d = inputJson.body) === null || _d === void 0 ? void 0 : _d.text],
177
+ ['input_json.body.input', (_e = inputJson.body) === null || _e === void 0 ? void 0 : _e.input],
178
+ ['input_json.body.messages.0.text', (_h = (_g = (_f = inputJson.body) === null || _f === void 0 ? void 0 : _f.messages) === null || _g === void 0 ? void 0 : _g[0]) === null || _h === void 0 ? void 0 : _h.text],
179
+ ['input_json.body.messages.0', (_k = (_j = inputJson.body) === null || _j === void 0 ? void 0 : _j.messages) === null || _k === void 0 ? void 0 : _k[0]],
180
180
  ];
181
- for (const candidate of candidates) {
181
+ for (const [source, candidate] of candidates) {
182
182
  const query = asSearchQuery(candidate).trim();
183
183
  if (query)
184
- return stripThreadTestPrefix(query);
184
+ return { query: stripThreadTestPrefix(query), source };
185
185
  }
186
- return '';
186
+ return { query: '', source: 'missing' };
187
187
  };
188
+ const resolveCurrentTurnQuery = (ctx, itemIndex = 0, inputValues = {}, queryParam = '') => resolveCurrentTurnQueryWithSource(ctx, itemIndex, inputValues, queryParam).query;
188
189
  const memoryText = (memory) => {
189
190
  if (!memory)
190
191
  return '';
@@ -1205,16 +1206,18 @@ const getMemoryStore = (ctx) => {
1205
1206
  data.tembory.activeSummary = data.tembory.activeSummary || {};
1206
1207
  data.tembory.connectedModelSummaryCache = data.tembory.connectedModelSummaryCache || {};
1207
1208
  data.tembory.captureState = data.tembory.captureState || {};
1209
+ data.tembory.memoryDedupe = data.tembory.memoryDedupe || {};
1208
1210
  return data.tembory;
1209
1211
  }
1210
1212
  catch {
1211
- global.__temboryMemory = global.__temboryMemory || { toolHistory: {}, recentMessages: {}, profileFacts: {}, workingMemory: {}, decisionState: {}, memoryCompression: {}, activeSummary: {}, connectedModelSummaryCache: {}, captureState: {} };
1213
+ global.__temboryMemory = global.__temboryMemory || { toolHistory: {}, recentMessages: {}, profileFacts: {}, workingMemory: {}, decisionState: {}, memoryCompression: {}, activeSummary: {}, connectedModelSummaryCache: {}, captureState: {}, memoryDedupe: {} };
1212
1214
  global.__temboryMemory.workingMemory = global.__temboryMemory.workingMemory || {};
1213
1215
  global.__temboryMemory.decisionState = global.__temboryMemory.decisionState || {};
1214
1216
  global.__temboryMemory.memoryCompression = global.__temboryMemory.memoryCompression || {};
1215
1217
  global.__temboryMemory.activeSummary = global.__temboryMemory.activeSummary || {};
1216
1218
  global.__temboryMemory.connectedModelSummaryCache = global.__temboryMemory.connectedModelSummaryCache || {};
1217
1219
  global.__temboryMemory.captureState = global.__temboryMemory.captureState || {};
1220
+ global.__temboryMemory.memoryDedupe = global.__temboryMemory.memoryDedupe || {};
1218
1221
  return global.__temboryMemory;
1219
1222
  }
1220
1223
  };
@@ -1268,6 +1271,8 @@ const mergeRemoteThreadState = (store, key, state) => {
1268
1271
  store.memoryCompression[key] = state.memoryCompression;
1269
1272
  if (state.captureState && typeof state.captureState === 'object')
1270
1273
  store.captureState[key] = { ...(store.captureState[key] || {}), ...state.captureState };
1274
+ if (state.memoryDedupe && typeof state.memoryDedupe === 'object')
1275
+ store.memoryDedupe[key] = mergeMemoryDedupeState(store.memoryDedupe[key], state.memoryDedupe);
1271
1276
  if (typeof state.activeSummary === 'string' && state.activeSummary)
1272
1277
  store.activeSummary[key] = state.activeSummary;
1273
1278
  };
@@ -1632,6 +1637,195 @@ const normalizeIntentText = (value = '') => String(value || '')
1632
1637
  .toLowerCase()
1633
1638
  .normalize('NFD')
1634
1639
  .replace(/[\u0300-\u036f]/g, '');
1640
+ const MEMORY_DEDUPE_MAX_KEYS = 600;
1641
+ const LEDGER_MEMORY_KINDS = new Set(['conversation_ledger', 'tool_ledger', 'turn_archive']);
1642
+ const normalizeDedupeText = (value = '') => normalizeIntentText(value).replace(/\s+/g, ' ').trim();
1643
+ const memoryBodyText = (body = {}) => {
1644
+ const messages = Array.isArray(body.messages) ? body.messages : [];
1645
+ if (messages.length)
1646
+ return messages.map((message) => message.content || message.text || message.message || '').filter(Boolean).join('\n');
1647
+ return body.memory || body.text || body.value || body.content || '';
1648
+ };
1649
+ const parseTurnArchiveText = (text = '') => {
1650
+ const raw = String(text || '');
1651
+ const start = raw.indexOf('{');
1652
+ if (start < 0)
1653
+ return null;
1654
+ try {
1655
+ const parsed = JSON.parse(raw.slice(start));
1656
+ return parsed && parsed.marker === 'tembory_turn_archive_v1' ? parsed : null;
1657
+ }
1658
+ catch {
1659
+ return null;
1660
+ }
1661
+ };
1662
+ const compactToolForDedupe = (tool = {}, index = 0) => ({
1663
+ id: tool.id || tool.callId || tool.call_id || undefined,
1664
+ sequence: tool.sequence || index + 1,
1665
+ name: tool.name || tool.tool || tool.toolName || '',
1666
+ input: canonicalToolInput(tool.input || tool.toolInput || tool.args || ''),
1667
+ ok: tool.ok !== false && tool.status !== 'failed',
1668
+ result: String(tool.result || tool.output || ''),
1669
+ });
1670
+ const canonicalMemoryPayloadForDedupe = (kind = 'memory', body = {}, diagnostics = {}) => {
1671
+ const meta = body.metadata || {};
1672
+ const text = memoryBodyText(body);
1673
+ if (kind === 'recent_message') {
1674
+ const marker = parseRecentMessageMarker(text);
1675
+ return stableStringify({
1676
+ role: meta.role || marker?.role || 'user',
1677
+ content: normalizeDedupeText(meta.content || marker?.content || text),
1678
+ });
1679
+ }
1680
+ if (kind === 'tool_history') {
1681
+ const marker = parseToolHistoryMarker(text);
1682
+ const tool = marker || {
1683
+ id: meta.id,
1684
+ turnId: meta.turn_id || meta.turnId,
1685
+ sequence: meta.sequence,
1686
+ name: meta.name || diagnostics.tool,
1687
+ input: meta.input,
1688
+ ok: meta.ok,
1689
+ result: meta.result,
1690
+ };
1691
+ return stableStringify(compactToolForDedupe(tool));
1692
+ }
1693
+ if (kind === 'tool_ledger') {
1694
+ return stableStringify(dedupeToolHistory(parseToolLedgerMarker(text)).map(compactToolForDedupe));
1695
+ }
1696
+ if (kind === 'conversation_ledger') {
1697
+ const messages = parseConversationLedgerMarker(text);
1698
+ return stableStringify((messages || []).map((message) => ({
1699
+ role: message.role || 'user',
1700
+ content: normalizeDedupeText(message.content || ''),
1701
+ })));
1702
+ }
1703
+ if (kind === 'turn_archive') {
1704
+ const archive = parseTurnArchiveText(text);
1705
+ if (archive) {
1706
+ return stableStringify({
1707
+ conversation: (archive.conversation || []).map((message) => ({
1708
+ role: message.role || 'user',
1709
+ content: normalizeDedupeText(message.content || ''),
1710
+ })),
1711
+ tools: (archive.tools || []).map(compactToolForDedupe),
1712
+ });
1713
+ }
1714
+ }
1715
+ return normalizeDedupeText(String(text || '')
1716
+ .replace(/"generated_at"\s*:\s*"[^"]+"/g, '"generated_at":"<ts>"')
1717
+ .replace(/"timestamp"\s*:\s*"[^"]+"/g, '"timestamp":"<ts>"'));
1718
+ };
1719
+ const inferMemoryKindFromText = (text = '') => {
1720
+ const raw = String(text || '');
1721
+ if (parseRecentMessageMarker(raw))
1722
+ return 'recent_message';
1723
+ if (parseToolHistoryMarker(raw))
1724
+ return 'tool_history';
1725
+ if (parseToolLedgerMarker(raw).length)
1726
+ return 'tool_ledger';
1727
+ if (parseConversationLedgerMarker(raw).length)
1728
+ return 'conversation_ledger';
1729
+ if (parseTurnArchiveText(raw))
1730
+ return 'turn_archive';
1731
+ return '';
1732
+ };
1733
+ const buildMemoryDedupeCandidate = (body = {}, diagnostics = {}) => {
1734
+ const meta = body.metadata || {};
1735
+ const text = memoryBodyText(body);
1736
+ const kind = String(meta.kind || diagnostics.kind || inferMemoryKindFromText(text) || 'memory');
1737
+ const thread = String(meta.thread_id || diagnostics.thread_id || body.thread_id || '');
1738
+ const project = String(meta.project || diagnostics.project || body.project || '');
1739
+ const slot = [kind, thread, project].filter(Boolean).join(':') || kind;
1740
+ const canonical = canonicalMemoryPayloadForDedupe(kind, body, diagnostics);
1741
+ const key = `${slot}:${stableHash(canonical)}`;
1742
+ return cleanContextValue({
1743
+ key,
1744
+ slot,
1745
+ kind,
1746
+ content_hash: stableHash(canonical),
1747
+ strategy: LEDGER_MEMORY_KINDS.has(kind) ? 'latest-ledger-per-content' : 'exact-canonical-content',
1748
+ });
1749
+ };
1750
+ const mergeMemoryDedupeState = (current = {}, incoming = {}) => {
1751
+ const merged = {
1752
+ seen: { ...((current || {}).seen || {}), ...((incoming || {}).seen || {}) },
1753
+ latestBySlot: { ...((current || {}).latestBySlot || {}), ...((incoming || {}).latestBySlot || {}) },
1754
+ lastSummary: (incoming || {}).lastSummary || (current || {}).lastSummary || undefined,
1755
+ updatedAt: (incoming || {}).updatedAt || (current || {}).updatedAt || undefined,
1756
+ };
1757
+ const entries = Object.entries(merged.seen);
1758
+ if (entries.length > MEMORY_DEDUPE_MAX_KEYS) {
1759
+ entries
1760
+ .sort((a, b) => String(a[1] || '').localeCompare(String(b[1] || '')))
1761
+ .slice(0, entries.length - MEMORY_DEDUPE_MAX_KEYS)
1762
+ .forEach(([key]) => delete merged.seen[key]);
1763
+ }
1764
+ return cleanContextValue(merged);
1765
+ };
1766
+ const ensureMemoryDedupeState = (store, key) => {
1767
+ store.memoryDedupe = store.memoryDedupe || {};
1768
+ store.memoryDedupe[key] = mergeMemoryDedupeState(store.memoryDedupe[key] || {}, {});
1769
+ store.memoryDedupe[key].seen = store.memoryDedupe[key].seen || {};
1770
+ store.memoryDedupe[key].latestBySlot = store.memoryDedupe[key].latestBySlot || {};
1771
+ return store.memoryDedupe[key];
1772
+ };
1773
+ const createDedupeSummary = () => ({
1774
+ enabled: true,
1775
+ write: { candidates: 0, persisted: 0, skipped: 0, byKind: {} },
1776
+ load: {},
1777
+ });
1778
+ const recordDedupeWriteSummary = (summary, candidate, decision) => {
1779
+ if (!summary || !candidate)
1780
+ return;
1781
+ summary.write.candidates += 1;
1782
+ summary.write[decision.persist ? 'persisted' : 'skipped'] += 1;
1783
+ summary.write.byKind[candidate.kind] = summary.write.byKind[candidate.kind] || { candidates: 0, persisted: 0, skipped: 0 };
1784
+ summary.write.byKind[candidate.kind].candidates += 1;
1785
+ summary.write.byKind[candidate.kind][decision.persist ? 'persisted' : 'skipped'] += 1;
1786
+ };
1787
+ const evaluateMemoryDedupeWrite = (state, candidate, at = nowIso()) => {
1788
+ if (!candidate || !candidate.key)
1789
+ return { persist: true, reason: 'no_dedupe_key' };
1790
+ state.seen = state.seen || {};
1791
+ state.latestBySlot = state.latestBySlot || {};
1792
+ if (state.seen[candidate.key])
1793
+ return { persist: false, reason: 'duplicate_memory_key' };
1794
+ state.seen[candidate.key] = at;
1795
+ state.latestBySlot[candidate.slot] = candidate.key;
1796
+ state.updatedAt = at;
1797
+ return { persist: true, reason: 'new_memory_key' };
1798
+ };
1799
+ const dedupeMemoryItemsForLoad = (items = []) => {
1800
+ const list = Array.isArray(items) ? items : [];
1801
+ const seen = new Set();
1802
+ const seenSlots = new Set();
1803
+ const out = [];
1804
+ for (const item of [...list].reverse()) {
1805
+ const candidate = buildMemoryDedupeCandidate({ messages: [{ role: 'system', content: memoryText(item) }], metadata: metadataOf(item) }, {});
1806
+ const slotKey = LEDGER_MEMORY_KINDS.has(candidate.kind) ? candidate.slot : candidate.key;
1807
+ const key = LEDGER_MEMORY_KINDS.has(candidate.kind) ? slotKey : candidate.key;
1808
+ if (LEDGER_MEMORY_KINDS.has(candidate.kind)) {
1809
+ if (seenSlots.has(slotKey))
1810
+ continue;
1811
+ seenSlots.add(slotKey);
1812
+ }
1813
+ else if (seen.has(key)) {
1814
+ continue;
1815
+ }
1816
+ seen.add(key);
1817
+ out.push(item);
1818
+ }
1819
+ out.reverse();
1820
+ return {
1821
+ items: out,
1822
+ summary: cleanContextValue({
1823
+ input: list.length,
1824
+ kept: out.length,
1825
+ removed: Math.max(0, list.length - out.length),
1826
+ }),
1827
+ };
1828
+ };
1635
1829
  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
1830
  const isConversationRecallQuery = (value = '') => {
1637
1831
  const text = normalizeIntentText(value);
@@ -2287,6 +2481,7 @@ const compactSaveAuditForAgent = (capture = {}) => cleanContextValue({
2287
2481
  tool_history_after_save: capture.last_save_tool_history_after_save,
2288
2482
  thread_state_saved: capture.last_save_thread_state_saved,
2289
2483
  backend_memory_persistence: capture.last_save_backend_memory_persistence,
2484
+ dedupe: capture.last_save_dedupe_summary,
2290
2485
  },
2291
2486
  });
2292
2487
  const compactEntityTimelineForAgent = (timeline = [], maxItems = 8) => pruneByLimit(timeline || [], maxItems).map((item) => cleanContextValue({
@@ -2431,8 +2626,8 @@ const compactToolAuditForSideChannel = (tool = {}) => cleanContextValue({
2431
2626
  status: tool.status || (tool.ok === false ? 'failed' : 'ok'),
2432
2627
  at: tool.at || tool.timestamp,
2433
2628
  timestamp: tool.timestamp || tool.at,
2434
- input: truncate(String(tool.input || tool.tool_args || tool.normalized_args || ''), 500) || undefined,
2435
- output: compactToolResult(tool.result !== undefined ? tool.result : tool.output !== undefined ? tool.output : tool.observation, 1200),
2629
+ input: truncate(String(tool.input || tool.tool_args || tool.normalized_args || ''), 300) || undefined,
2630
+ output: compactToolResult(tool.result !== undefined ? tool.result : tool.output !== undefined ? tool.output : tool.observation, 800),
2436
2631
  });
2437
2632
  const compactMessageForSideChannel = (message = {}) => {
2438
2633
  const type = messageTypeOf(message) || String(message.role || 'message').toLowerCase();
@@ -2456,7 +2651,7 @@ const normalizeConversationRoleForSideChannel = (role = '') => {
2456
2651
  return 'tool';
2457
2652
  return normalized || 'message';
2458
2653
  };
2459
- const compactConversationTimelineForSideChannel = (conversation = {}, maxItems = 12) => {
2654
+ const compactConversationTimelineForSideChannel = (conversation = {}, maxItems = 4, includeFull = false) => {
2460
2655
  const chronological = Array.isArray(conversation.conversation_history_chronological)
2461
2656
  ? conversation.conversation_history_chronological
2462
2657
  : [];
@@ -2472,13 +2667,12 @@ const compactConversationTimelineForSideChannel = (conversation = {}, maxItems =
2472
2667
  at,
2473
2668
  chars: content.length,
2474
2669
  preview,
2475
- content: preview,
2476
- message: {
2670
+ message: includeFull ? {
2477
2671
  role,
2478
2672
  at,
2479
2673
  content: role === 'system' ? '[system context hidden]' : truncate(content, 2000),
2480
2674
  truncated: content.length > 2000,
2481
- },
2675
+ } : undefined,
2482
2676
  });
2483
2677
  });
2484
2678
  };
@@ -2508,11 +2702,11 @@ const compactMemoryEventPayload = (payload = {}) => {
2508
2702
  compact.toolNames = compact.toolCalls
2509
2703
  .map((tool) => (tool && (tool.name || tool.tool_name || tool.tool)) || '')
2510
2704
  .filter(Boolean)
2511
- .slice(0, 20);
2705
+ .slice(0, 12);
2512
2706
  compact.toolEvents = compact.toolCalls
2513
2707
  .map((tool) => compactToolAuditForSideChannel(tool || {}))
2514
2708
  .filter((tool) => tool && Object.keys(tool).length)
2515
- .slice(0, 20);
2709
+ .slice(0, 8);
2516
2710
  delete compact.toolCalls;
2517
2711
  }
2518
2712
  for (const key of ['input', 'output', 'values', 'chatHistory', 'context', 'contextText', 'diagnostics']) {
@@ -2522,6 +2716,49 @@ const compactMemoryEventPayload = (payload = {}) => {
2522
2716
  }
2523
2717
  return compact;
2524
2718
  };
2719
+ const compactLastSaveForSideChannel = (lastSave = {}) => cleanContextValue({
2720
+ saved: lastSave.saved,
2721
+ status: lastSave.status,
2722
+ at: lastSave.at,
2723
+ inputChars: lastSave.input_chars,
2724
+ outputChars: lastSave.output_chars,
2725
+ toolCallsCaptured: lastSave.tool_calls_captured,
2726
+ toolNames: lastSave.tool_names,
2727
+ conversationMessagesAfterSave: lastSave.conversation_messages_after_save,
2728
+ toolHistoryAfterSave: lastSave.tool_history_after_save,
2729
+ threadStateSaved: lastSave.thread_state_saved,
2730
+ backendPersistence: lastSave.backend_memory_persistence,
2731
+ });
2732
+ const compactDedupeForSideChannel = (dedupe = {}) => {
2733
+ const load = dedupe.load || {};
2734
+ const removedOnLoad = Object.values(load).reduce((sum, item) => sum + Number((item || {}).removed || 0), 0);
2735
+ const write = dedupe.lastSave?.write || dedupe.write || {};
2736
+ return cleanContextValue({
2737
+ enabled: dedupe.enabled,
2738
+ removedOnLoad,
2739
+ skippedWrites: Number(write.skipped || 0),
2740
+ persistedWrites: Number(write.persisted || 0),
2741
+ candidates: Number(write.candidates || 0),
2742
+ });
2743
+ };
2744
+ const loadedSectionsForSideChannel = (parsed = {}, memoryAudit = {}) => cleanContextValue({
2745
+ conversation: Boolean(parsed.conversation),
2746
+ summary: Boolean(parsed.summary),
2747
+ activeSummary: Boolean(parsed.activeSummary),
2748
+ connectedModelSummary: Boolean(parsed.connectedModelSummary),
2749
+ workingMemory: Boolean(parsed.workingMemory),
2750
+ decisionState: Boolean(parsed.decisionState),
2751
+ memoryCompression: Boolean(parsed.memoryCompression),
2752
+ operationalState: Boolean(parsed.operationalState),
2753
+ actionLedger: Array.isArray(parsed.actionLedger),
2754
+ turnBrief: Boolean(parsed.turnBrief || parsed.turn_brief),
2755
+ memoryAudit: Boolean(memoryAudit && Object.keys(memoryAudit).length),
2756
+ entityTimeline: Array.isArray(parsed.entityTimeline),
2757
+ vectorMemories: Array.isArray(parsed.vectorMemories),
2758
+ graph: Array.isArray(parsed.graph),
2759
+ recentHighlights: Array.isArray(parsed.recentHighlights),
2760
+ contextHealth: Boolean(parsed.contextHealth),
2761
+ });
2525
2762
  const summarizeMemoryMessagesForSideChannel = (messages = []) => {
2526
2763
  const list = Array.isArray(messages) ? messages : [];
2527
2764
  const summary = { messages: list.length };
@@ -2538,20 +2775,45 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
2538
2775
  const toolItems = Array.isArray(tools.items) ? tools.items : [];
2539
2776
  const memoryAudit = parsed.memoryAudit || parsed.memory_audit || {};
2540
2777
  const summaryText = parsed.summary?.slm || parsed.summary || parsed.connectedModelSummary || parsed.activeSummary || '';
2778
+ const chronological = Array.isArray(conversation.conversation_history_chronological)
2779
+ ? conversation.conversation_history_chronological
2780
+ : [];
2781
+ const allUserMessages = Array.isArray(conversation.all_user_messages_chronological)
2782
+ ? conversation.all_user_messages_chronological
2783
+ : chronological.filter((message) => normalizeConversationRoleForSideChannel(message.role || message.type) === 'user');
2784
+ const lastUser = [...chronological].reverse().find((message) => normalizeConversationRoleForSideChannel(message.role || message.type) === 'user');
2785
+ const lastAgent = [...chronological].reverse().find((message) => normalizeConversationRoleForSideChannel(message.role || message.type) === 'agent');
2786
+ const includeDebug = Boolean(parsed.options?.includeDiagnostics || parsed.diagnostics?.includeDiagnostics || parsed.diagnostics?.include_diagnostics);
2787
+ const lastSave = compactLastSaveForSideChannel(memoryAudit.last_save || parsed.state?.last_save || {});
2788
+ const fullDedupeSummary = parsed.dedupeSummary || parsed.diagnostics?.dedupeSummary || undefined;
2789
+ const loadedSections = loadedSectionsForSideChannel(parsed, memoryAudit);
2541
2790
  summary.userId = parsed.userId;
2542
2791
  summary.project = parsed.project || undefined;
2543
2792
  summary.retrievalMode = parsed.retrievalMode;
2544
2793
  summary.payloadFormat = parsed.payloadFormat;
2545
- summary.options = cleanContextValue(parsed.options || {});
2546
2794
  summary.intent = parsed.observations?.inferred_intent?.label || parsed.workingMemory?.last_user_intent || parsed.decisionState?.current_intent || undefined;
2547
2795
  const parsedTurnBrief = parsed.turnBrief || parsed.turn_brief || undefined;
2796
+ summary.currentTurn = parsed.currentTurn || parsed.current_turn || parsed.diagnostics?.currentTurn || undefined;
2548
2797
  summary.currentUserMessage = conversation.current_user_message ? truncate(conversation.current_user_message, 180) : undefined;
2549
- summary.recentMessages = Array.isArray(conversation.conversation_history_chronological) ? conversation.conversation_history_chronological.length : undefined;
2550
- summary.conversationTimeline = compactConversationTimelineForSideChannel(conversation, 12);
2798
+ summary.recentMessages = chronological.length || undefined;
2799
+ summary.conversation = cleanContextValue({
2800
+ messages: chronological.length,
2801
+ userMessages: allUserMessages.length,
2802
+ currentUserMessage: summary.currentUserMessage,
2803
+ lastUser: lastUser ? {
2804
+ at: lastUser.at || lastUser.timestamp || lastUser.created_at || lastUser.createdAt,
2805
+ preview: truncate(String(lastUser.content || lastUser.text || lastUser.message || ''), 240),
2806
+ } : undefined,
2807
+ lastAgent: lastAgent ? {
2808
+ at: lastAgent.at || lastAgent.timestamp || lastAgent.created_at || lastAgent.createdAt,
2809
+ preview: truncate(String(lastAgent.content || lastAgent.text || lastAgent.message || ''), 240),
2810
+ } : undefined,
2811
+ });
2812
+ summary.conversationTimeline = compactConversationTimelineForSideChannel(conversation, 4);
2551
2813
  summary.toolCount = toolItems.length || tools.count || parsed.operationalState?.tool_counts?.total || undefined;
2552
- summary.toolNames = toolItems.map((tool) => tool.name || tool.tool_name).filter(Boolean).slice(0, 20);
2814
+ summary.toolNames = toolItems.map((tool) => tool.name || tool.tool_name).filter(Boolean).slice(0, 12);
2553
2815
  summary.toolEvents = toolItems
2554
- .slice(-6)
2816
+ .slice(-3)
2555
2817
  .map((tool) => compactToolAuditForSideChannel(tool || {}))
2556
2818
  .filter((tool) => tool && Object.keys(tool).length);
2557
2819
  summary.lastTool = tools.last_successful_tool
@@ -2571,8 +2833,8 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
2571
2833
  })
2572
2834
  : parsedTurnBrief;
2573
2835
  summary.counts = cleanContextValue({
2574
- conversationMessages: Array.isArray(conversation.conversation_history_chronological) ? conversation.conversation_history_chronological.length : undefined,
2575
- userMessages: Array.isArray(conversation.all_user_messages_chronological) ? conversation.all_user_messages_chronological.length : undefined,
2836
+ conversationMessages: chronological.length || undefined,
2837
+ userMessages: allUserMessages.length || undefined,
2576
2838
  toolHistory: toolItems.length,
2577
2839
  actionLedger: Array.isArray(parsed.actionLedger) ? parsed.actionLedger.length : undefined,
2578
2840
  entityTimeline: Array.isArray(parsed.entityTimeline) ? parsed.entityTimeline.length : undefined,
@@ -2580,25 +2842,8 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
2580
2842
  graph: Array.isArray(parsed.graph) ? parsed.graph.length : undefined,
2581
2843
  recentHighlights: Array.isArray(parsed.recentHighlights) ? parsed.recentHighlights.length : undefined,
2582
2844
  });
2583
- summary.lastSave = memoryAudit.last_save || parsed.state?.last_save || undefined;
2584
- summary.loadedSections = cleanContextValue({
2585
- conversation: Boolean(parsed.conversation),
2586
- summary: Boolean(parsed.summary),
2587
- activeSummary: Boolean(parsed.activeSummary),
2588
- connectedModelSummary: Boolean(parsed.connectedModelSummary),
2589
- workingMemory: Boolean(parsed.workingMemory),
2590
- decisionState: Boolean(parsed.decisionState),
2591
- memoryCompression: Boolean(parsed.memoryCompression),
2592
- operationalState: Boolean(parsed.operationalState),
2593
- actionLedger: Array.isArray(parsed.actionLedger),
2594
- turnBrief: Boolean(parsed.turnBrief || parsed.turn_brief),
2595
- memoryAudit: Boolean(memoryAudit && Object.keys(memoryAudit).length),
2596
- entityTimeline: Array.isArray(parsed.entityTimeline),
2597
- vectorMemories: Array.isArray(parsed.vectorMemories),
2598
- graph: Array.isArray(parsed.graph),
2599
- recentHighlights: Array.isArray(parsed.recentHighlights),
2600
- contextHealth: Boolean(parsed.contextHealth),
2601
- });
2845
+ summary.lastSave = Object.keys(lastSave).length ? lastSave : undefined;
2846
+ summary.dedupe = fullDedupeSummary ? compactDedupeForSideChannel(fullDedupeSummary) : undefined;
2602
2847
  summary.workingMemory = cleanContextValue({
2603
2848
  currentGoal: parsed.workingMemory?.current_goal,
2604
2849
  currentGoalReason: parsed.workingMemory?.current_goal_reason,
@@ -2619,8 +2864,14 @@ const summarizeMemoryMessagesForSideChannel = (messages = []) => {
2619
2864
  doNotRepeatTools: parsed.decisionState?.do_not_repeat_tools,
2620
2865
  });
2621
2866
  summary.quality = parsed.contextHealth?.quality_score || parsed.contextQualityScore || undefined;
2622
- summary.contextSize = parsed.diagnostics?.contextSize || undefined;
2623
- summary.summaryChars = typeof summaryText === 'string' ? summaryText.length : safeStringify(summaryText).length;
2867
+ summary.debug = includeDebug ? cleanContextValue({
2868
+ options: parsed.options,
2869
+ loadedSections,
2870
+ contextSize: parsed.diagnostics?.contextSize,
2871
+ summaryChars: typeof summaryText === 'string' ? summaryText.length : safeStringify(summaryText).length,
2872
+ dedupeSummary: fullDedupeSummary,
2873
+ conversationTimelineFull: compactConversationTimelineForSideChannel(conversation, 12, true),
2874
+ }) : undefined;
2624
2875
  return Object.fromEntries(Object.entries(summary).filter(([, value]) => value !== undefined));
2625
2876
  }
2626
2877
  catch {
@@ -2837,6 +3088,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
2837
3088
  current_user_request: focus.current_user_request,
2838
3089
  instruction: focus.instruction,
2839
3090
  }),
3091
+ currentTurn: diagnostics?.currentTurn,
2840
3092
  action_directive: directive ? cleanContextValue({
2841
3093
  required_tool: directive.required_tool,
2842
3094
  next_expected_action: directive.next_expected_action,
@@ -2856,6 +3108,7 @@ const buildContextMessages = ({ payloadFormat, query, userId, profileFacts, work
2856
3108
  profile: sectionValue('profile_facts'),
2857
3109
  tools: compactToolLedger,
2858
3110
  memoryAudit,
3111
+ dedupeSummary: diagnostics?.dedupeSummary,
2859
3112
  });
2860
3113
  const renderCompactSection = (section) => {
2861
3114
  if (section.value === null || section.value === undefined)
@@ -3375,6 +3628,58 @@ class TemboryMemory {
3375
3628
  const output = cleanAssistantTranscriptText(rawOutput);
3376
3629
  const toolCalls = extractToolCalls(outputValues);
3377
3630
  const saveAt = nowIso();
3631
+ const dedupeState = ensureMemoryDedupeState(store, key);
3632
+ const dedupeSummary = createDedupeSummary();
3633
+ const persistLegacyMemory = async (legacyBody, diagnostics = {}) => {
3634
+ const candidate = buildMemoryDedupeCandidate(legacyBody, diagnostics);
3635
+ const decision = evaluateMemoryDedupeWrite(dedupeState, candidate, saveAt);
3636
+ recordDedupeWriteSummary(dedupeSummary, candidate, decision);
3637
+ if (!decision.persist)
3638
+ return { skipped: true, reason: decision.reason, dedupeKey: candidate.key };
3639
+ const bodyWithDedupe = {
3640
+ ...legacyBody,
3641
+ metadata: cleanContextValue({
3642
+ ...(legacyBody.metadata || {}),
3643
+ memory_dedupe_key: candidate.key,
3644
+ memory_dedupe_slot: candidate.slot,
3645
+ memory_dedupe_strategy: candidate.strategy,
3646
+ }),
3647
+ };
3648
+ return await safePersistLegacyMemory(this, bodyWithDedupe, {
3649
+ ...diagnostics,
3650
+ memory_dedupe_key: candidate.key,
3651
+ memory_dedupe_slot: candidate.slot,
3652
+ });
3653
+ };
3654
+ const finalizeDedupeSummary = async () => {
3655
+ dedupeState.lastSummary = dedupeSummary;
3656
+ dedupeState.updatedAt = nowIso();
3657
+ store.memoryDedupe[key] = mergeMemoryDedupeState(store.memoryDedupe[key], dedupeState);
3658
+ store.captureState[key] = cleanContextValue({
3659
+ ...(store.captureState[key] || {}),
3660
+ last_save_dedupe_summary: dedupeSummary,
3661
+ });
3662
+ const finalThreadStateSaved = await saveThreadState(this, key, threadId, project, {
3663
+ kind: 'tembory.thread_state.v1',
3664
+ threadId,
3665
+ project: project || undefined,
3666
+ updatedAt: nowIso(),
3667
+ recentMessages: recentForTurn,
3668
+ toolHistory: toolHistoryForTurn,
3669
+ profileFacts: store.profileFacts[key] || {},
3670
+ workingMemory: workingMemoryForTurn,
3671
+ decisionState: decisionStateForTurn,
3672
+ memoryCompression: compressionForTurn,
3673
+ operationalState: operationalStateForTurn,
3674
+ captureState: store.captureState[key],
3675
+ memoryDedupe: store.memoryDedupe[key],
3676
+ activeSummary: store.activeSummary[key] || '',
3677
+ });
3678
+ store.captureState[key] = cleanContextValue({
3679
+ ...(store.captureState[key] || {}),
3680
+ last_save_thread_state_saved: (store.captureState[key] || {}).last_save_thread_state_saved || finalThreadStateSaved,
3681
+ });
3682
+ };
3378
3683
  store.captureState[key] = {
3379
3684
  last_save_status: 'started',
3380
3685
  last_save_saved: false,
@@ -3388,6 +3693,7 @@ class TemboryMemory {
3388
3693
  last_save_tool_calls_captured: toolCalls.length,
3389
3694
  last_save_tool_names: toolCalls.map((tool) => tool.name).filter(Boolean).slice(0, 20),
3390
3695
  last_save_capture_sources: Array.from(new Set(toolCalls.map((tool) => tool.source).filter(Boolean))),
3696
+ last_save_dedupe_summary: dedupeSummary,
3391
3697
  };
3392
3698
  const recentForTembory = [];
3393
3699
  const profileFromTurn = extractProfileFactsFromText(input, 'user_message', nowIso());
@@ -3488,6 +3794,7 @@ class TemboryMemory {
3488
3794
  : adv.useVectorMemory === false
3489
3795
  ? 'thread_state_only'
3490
3796
  : 'enabled',
3797
+ last_save_dedupe_summary: dedupeSummary,
3491
3798
  });
3492
3799
  const threadStateSaved = await saveThreadState(this, key, threadId, project, {
3493
3800
  kind: 'tembory.thread_state.v1',
@@ -3502,6 +3809,7 @@ class TemboryMemory {
3502
3809
  memoryCompression: compressionForTurn,
3503
3810
  operationalState: operationalStateForTurn,
3504
3811
  captureState: nextCaptureState,
3812
+ memoryDedupe: dedupeState,
3505
3813
  activeSummary: store.activeSummary[key] || '',
3506
3814
  });
3507
3815
  store.captureState[key] = cleanContextValue({
@@ -3513,10 +3821,14 @@ class TemboryMemory {
3513
3821
  globalData.__dataChanged = true;
3514
3822
  }
3515
3823
  catch { }
3516
- if (adv.persistBackendMemories === false)
3824
+ if (adv.persistBackendMemories === false) {
3825
+ await finalizeDedupeSummary();
3517
3826
  return;
3518
- if (adv.useVectorMemory === false)
3827
+ }
3828
+ if (adv.useVectorMemory === false) {
3829
+ await finalizeDedupeSummary();
3519
3830
  return;
3831
+ }
3520
3832
  const connectedEmbedding = await getConnectedEmbedding(this, itemIndex);
3521
3833
  if (connectedEmbedding) {
3522
3834
  const ids = { user_id: body.user_id, agent_id: body.agent_id, run_id: body.run_id };
@@ -3529,21 +3841,32 @@ class TemboryMemory {
3529
3841
  memoryCompression: compressionForTurn,
3530
3842
  operationalState: operationalStateForTurn,
3531
3843
  });
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);
3844
+ const archiveMetadata = {
3845
+ kind: 'turn_archive',
3846
+ thread_id: threadId,
3847
+ project: project || undefined,
3848
+ source: 'n8n_connected_embedding',
3849
+ generated_at: nowIso(),
3850
+ message_count: recentForTurn.length,
3851
+ tool_count: toolHistoryForTurn.length,
3852
+ latest_tool: toolHistoryForTurn.at(-1)?.name || null,
3853
+ };
3854
+ const archiveCandidate = buildMemoryDedupeCandidate({ messages: [{ role: 'system', content: archiveText }], metadata: archiveMetadata }, { kind: 'turn_archive', user_id: body.user_id });
3855
+ const archiveDecision = evaluateMemoryDedupeWrite(dedupeState, archiveCandidate, saveAt);
3856
+ recordDedupeWriteSummary(dedupeSummary, archiveCandidate, archiveDecision);
3857
+ if (archiveDecision.persist) {
3858
+ await saveClientVectorMemories(this, [
3859
+ await createClientVectorMemory(connectedEmbedding, archiveText, {
3860
+ ...archiveMetadata,
3861
+ memory_dedupe_key: archiveCandidate.key,
3862
+ memory_dedupe_slot: archiveCandidate.slot,
3863
+ memory_dedupe_strategy: archiveCandidate.strategy,
3864
+ }, ids),
3865
+ ], ids);
3866
+ }
3544
3867
  if (adv.includeRecentMessages !== false && recentForTembory.length) {
3545
3868
  for (const recent of recentForTembory) {
3546
- await safePersistLegacyMemory(this, {
3869
+ await persistLegacyMemory({
3547
3870
  messages: [{ role: 'system', content: encodeRecentMessage(recent, threadId) }],
3548
3871
  infer: false,
3549
3872
  user_id: body.user_id,
@@ -3561,9 +3884,10 @@ class TemboryMemory {
3561
3884
  }, {
3562
3885
  kind: 'recent_message',
3563
3886
  user_id: body.user_id,
3887
+ thread_id: threadId,
3564
3888
  });
3565
3889
  }
3566
- await safePersistLegacyMemory(this, {
3890
+ await persistLegacyMemory({
3567
3891
  messages: [{ role: 'system', content: encodeConversationLedger(recentForTurn, threadId) }],
3568
3892
  infer: false,
3569
3893
  user_id: body.user_id,
@@ -3579,11 +3903,12 @@ class TemboryMemory {
3579
3903
  }, {
3580
3904
  kind: 'conversation_ledger',
3581
3905
  user_id: body.user_id,
3906
+ thread_id: threadId,
3582
3907
  });
3583
3908
  }
3584
3909
  if (adv.includeToolHistory !== false && toolCalls.length) {
3585
3910
  for (const tool of toolCalls) {
3586
- await safePersistLegacyMemory(this, {
3911
+ await persistLegacyMemory({
3587
3912
  messages: [{ role: 'system', content: encodeToolCall(tool, threadId) }],
3588
3913
  infer: false,
3589
3914
  user_id: body.user_id,
@@ -3607,9 +3932,10 @@ class TemboryMemory {
3607
3932
  kind: 'tool_history',
3608
3933
  user_id: body.user_id,
3609
3934
  tool: tool.name,
3935
+ thread_id: threadId,
3610
3936
  });
3611
3937
  }
3612
- await safePersistLegacyMemory(this, {
3938
+ await persistLegacyMemory({
3613
3939
  messages: [{ role: 'system', content: encodeToolLedger(toolHistoryForTurn, threadId) }],
3614
3940
  infer: false,
3615
3941
  user_id: body.user_id,
@@ -3625,18 +3951,21 @@ class TemboryMemory {
3625
3951
  }, {
3626
3952
  kind: 'tool_ledger',
3627
3953
  user_id: body.user_id,
3954
+ thread_id: threadId,
3628
3955
  });
3629
3956
  }
3957
+ await finalizeDedupeSummary();
3630
3958
  return;
3631
3959
  }
3632
3960
  if (messages.length)
3633
- await safePersistLegacyMemory(this, body, {
3961
+ await persistLegacyMemory(body, {
3634
3962
  kind: 'conversation_messages',
3635
3963
  user_id: body.user_id,
3964
+ thread_id: threadId,
3636
3965
  });
3637
3966
  if (adv.includeRecentMessages !== false && recentForTembory.length) {
3638
3967
  for (const recent of recentForTembory) {
3639
- await safePersistLegacyMemory(this, {
3968
+ await persistLegacyMemory({
3640
3969
  messages: [{ role: 'system', content: encodeRecentMessage(recent, threadId) }],
3641
3970
  infer: false,
3642
3971
  user_id: body.user_id,
@@ -3653,9 +3982,10 @@ class TemboryMemory {
3653
3982
  }, {
3654
3983
  kind: 'recent_message',
3655
3984
  user_id: body.user_id,
3985
+ thread_id: threadId,
3656
3986
  });
3657
3987
  }
3658
- await safePersistLegacyMemory(this, {
3988
+ await persistLegacyMemory({
3659
3989
  messages: [{ role: 'system', content: encodeConversationLedger(recentForTurn, threadId) }],
3660
3990
  infer: false,
3661
3991
  user_id: body.user_id,
@@ -3671,11 +4001,12 @@ class TemboryMemory {
3671
4001
  }, {
3672
4002
  kind: 'conversation_ledger',
3673
4003
  user_id: body.user_id,
4004
+ thread_id: threadId,
3674
4005
  });
3675
4006
  }
3676
4007
  if (adv.includeToolHistory !== false && !adv.persistToolFactsToTembory && toolCalls.length) {
3677
4008
  for (const tool of toolCalls) {
3678
- await safePersistLegacyMemory(this, {
4009
+ await persistLegacyMemory({
3679
4010
  messages: [{ role: 'system', content: encodeToolCall(tool, threadId) }],
3680
4011
  infer: false,
3681
4012
  user_id: body.user_id,
@@ -3699,9 +4030,10 @@ class TemboryMemory {
3699
4030
  kind: 'tool_history',
3700
4031
  user_id: body.user_id,
3701
4032
  tool: tool.name,
4033
+ thread_id: threadId,
3702
4034
  });
3703
4035
  }
3704
- await safePersistLegacyMemory(this, {
4036
+ await persistLegacyMemory({
3705
4037
  messages: [{ role: 'system', content: encodeToolLedger(toolHistoryForTurn, threadId) }],
3706
4038
  infer: false,
3707
4039
  user_id: body.user_id,
@@ -3717,11 +4049,12 @@ class TemboryMemory {
3717
4049
  }, {
3718
4050
  kind: 'tool_ledger',
3719
4051
  user_id: body.user_id,
4052
+ thread_id: threadId,
3720
4053
  });
3721
4054
  }
3722
4055
  if (adv.persistToolFactsToTembory && toolCalls.length) {
3723
4056
  const facts = toolCalls.map((tool) => `Tool ${tool.name} input=${tool.input}${tool.result ? ` result=${tool.result}` : ''}`).join('\n');
3724
- await safePersistLegacyMemory(this, {
4057
+ await persistLegacyMemory({
3725
4058
  messages: [{ role: 'system', content: `Tool facts (read-only):\n${truncate(facts, 2000)}` }],
3726
4059
  infer: false,
3727
4060
  user_id: body.user_id,
@@ -3729,9 +4062,10 @@ class TemboryMemory {
3729
4062
  }, {
3730
4063
  kind: 'tool_facts',
3731
4064
  user_id: body.user_id,
4065
+ thread_id: threadId,
3732
4066
  });
3733
4067
  for (const tool of toolCalls) {
3734
- await safePersistLegacyMemory(this, {
4068
+ await persistLegacyMemory({
3735
4069
  messages: [{ role: 'system', content: encodeToolCall(tool, threadId) }],
3736
4070
  infer: false,
3737
4071
  user_id: body.user_id,
@@ -3754,9 +4088,29 @@ class TemboryMemory {
3754
4088
  kind: 'tool_history',
3755
4089
  user_id: body.user_id,
3756
4090
  tool: tool.name,
4091
+ thread_id: threadId,
3757
4092
  });
3758
4093
  }
4094
+ await persistLegacyMemory({
4095
+ messages: [{ role: 'system', content: encodeToolLedger(toolHistoryForTurn, threadId) }],
4096
+ infer: false,
4097
+ user_id: body.user_id,
4098
+ agent_id: body.agent_id,
4099
+ run_id: body.run_id,
4100
+ metadata: {
4101
+ kind: 'tool_ledger',
4102
+ thread_id: threadId,
4103
+ project: project || undefined,
4104
+ source: 'tembory_transcript',
4105
+ generated_at: nowIso(),
4106
+ },
4107
+ }, {
4108
+ kind: 'tool_ledger',
4109
+ user_id: body.user_id,
4110
+ thread_id: threadId,
4111
+ });
3759
4112
  }
4113
+ await finalizeDedupeSummary();
3760
4114
  }
3761
4115
  async loadMemoryVariablesForItem(itemIndex, inputValues = {}) {
3762
4116
  var _a, _b, _c, _d, _e, _f, _g;
@@ -3773,7 +4127,8 @@ class TemboryMemory {
3773
4127
  const store = getMemoryStore(this);
3774
4128
  const key = userKeyFrom(threadId, adv, project);
3775
4129
  const queryParam = this.getNodeParameter('query', itemIndex, '');
3776
- const query = resolveCurrentTurnQuery(this, itemIndex, inputValues, queryParam);
4130
+ const currentTurn = resolveCurrentTurnQueryWithSource(this, itemIndex, inputValues, queryParam);
4131
+ const query = currentTurn.query;
3777
4132
  const remoteThreadState = await loadThreadState(this, key, threadId, project);
3778
4133
  mergeRemoteThreadState(store, key, remoteThreadState);
3779
4134
  let connectedLanguageModel;
@@ -3802,12 +4157,18 @@ class TemboryMemory {
3802
4157
  retrievalMode,
3803
4158
  requestedRetrievalMode,
3804
4159
  hasQuery: String(query || '').trim().length > 0,
4160
+ querySource: currentTurn.source,
3805
4161
  connectedAi,
3806
4162
  });
3807
4163
  }
3808
4164
  catch { }
3809
4165
  let payload;
3810
4166
  let vectorMemories = [];
4167
+ const loadDedupeSummary = {
4168
+ enabled: true,
4169
+ load: {},
4170
+ lastSave: store.memoryDedupe[key]?.lastSummary,
4171
+ };
3811
4172
  if (vectorMemoryEnabled && (retrievalMode === 'semantic' || retrievalMode === 'semanticV2' || retrievalMode === 'hybrid')) {
3812
4173
  const body = { query: String(query || '') };
3813
4174
  // IDs
@@ -4068,6 +4429,12 @@ class TemboryMemory {
4068
4429
  connectedAi.errors.push(`persistedContext.load: ${error.message || String(error)}`);
4069
4430
  }
4070
4431
  }
4432
+ const vectorDedupe = dedupeMemoryItemsForLoad(vectorMemories);
4433
+ vectorMemories = vectorDedupe.items;
4434
+ loadDedupeSummary.load.vectorMemories = vectorDedupe.summary;
4435
+ const persistedDedupe = dedupeMemoryItemsForLoad(persistedMemoryItems);
4436
+ persistedMemoryItems = persistedDedupe.items;
4437
+ loadDedupeSummary.load.persistedMemories = persistedDedupe.summary;
4071
4438
  if (adv.includeRecentMessages !== false) {
4072
4439
  persistedRecentMessages = persistedMemoryItems
4073
4440
  .flatMap(recentMessagesFromMemory)
@@ -4082,7 +4449,13 @@ class TemboryMemory {
4082
4449
  .sort((a, b) => new Date(a.at || 0).getTime() - new Date(b.at || 0).getTime());
4083
4450
  }
4084
4451
  const storedRecentMessages = adv.includeRecentMessages === false ? [] : (store.recentMessages[key] || []).concat(persistedRecentMessages, vectorRecentMessages);
4085
- const allRecentMessages = dedupeRecentMessages(appendCurrentUserMessage(storedRecentMessages, query));
4452
+ const appendedRecentMessages = appendCurrentUserMessage(storedRecentMessages, query);
4453
+ const allRecentMessages = dedupeRecentMessages(appendedRecentMessages);
4454
+ loadDedupeSummary.load.recentMessages = cleanContextValue({
4455
+ input: appendedRecentMessages.length,
4456
+ kept: allRecentMessages.length,
4457
+ removed: Math.max(0, appendedRecentMessages.length - allRecentMessages.length),
4458
+ });
4086
4459
  const recentMessages = pruneConversationMessagesPreserveAnchors(allRecentMessages, Math.max(Number(adv.recentMessagesLastN || 8), 8));
4087
4460
  const toolHistoryFromRecentMessages = [];
4088
4461
  if (adv.includeToolHistory !== false) {
@@ -4091,7 +4464,13 @@ class TemboryMemory {
4091
4464
  }
4092
4465
  const toolHistoryFromVectorMarkers = adv.includeToolHistory === false ? [] : vectorMemories.flatMap(explicitToolHistoryItemsFromMemory);
4093
4466
  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);
4467
+ const combinedToolHistory = (store.toolHistory[key] || []).concat(persistedToolHistory, toolHistoryFromRecentMessages, toolHistoryFromVectorMarkers, toolHistoryFromVectorMemories);
4468
+ const toolHistory = adv.includeToolHistory === false ? [] : applyToolHistoryWindow(combinedToolHistory, adv.toolHistoryTTLSeconds, adv.toolHistoryLastN || 15);
4469
+ loadDedupeSummary.load.toolHistory = cleanContextValue({
4470
+ input: adv.includeToolHistory === false ? 0 : combinedToolHistory.length,
4471
+ kept: toolHistory.length,
4472
+ removed: Math.max(0, (adv.includeToolHistory === false ? 0 : combinedToolHistory.length) - toolHistory.length),
4473
+ });
4095
4474
  const highlights = adv.includeRecentHighlights === false ? [] : getRecentHighlights(recentMessages, toolHistory, adv.recentHighlightsMaxItems || 6);
4096
4475
  const profileFacts = adv.includeProfileFacts === false ? {} : mergeProfileFacts(store.profileFacts[key], profileFactsFromMessages(allRecentMessages), profileFactsFromMemories(vectorMemories));
4097
4476
  if (adv.includeProfileFacts !== false && Object.keys(profileFacts).length) {
@@ -4221,6 +4600,8 @@ class TemboryMemory {
4221
4600
  },
4222
4601
  connectedAi,
4223
4602
  activeSummary: summaryDiagnostics,
4603
+ dedupeSummary: cleanContextValue(loadDedupeSummary),
4604
+ currentTurn,
4224
4605
  };
4225
4606
  const contextHealth = deriveContextHealth({
4226
4607
  userId: key,
@@ -4292,6 +4673,7 @@ class TemboryMemory {
4292
4673
  userId: key,
4293
4674
  project: project || undefined,
4294
4675
  query,
4676
+ currentTurn,
4295
4677
  retrievalMode,
4296
4678
  requestedRetrievalMode: requestedRetrievalMode === retrievalMode ? undefined : requestedRetrievalMode,
4297
4679
  payloadFormat,
@@ -4360,6 +4742,7 @@ class TemboryMemory {
4360
4742
  })),
4361
4743
  },
4362
4744
  diagnostics,
4745
+ dedupeSummary: diagnostics.dedupeSummary,
4363
4746
  };
4364
4747
  return {
4365
4748
  response: {
@@ -4476,6 +4859,7 @@ exports.__private = {
4476
4859
  flattenAdvancedGroups,
4477
4860
  asSearchQuery,
4478
4861
  currentInputJsonFromContext,
4862
+ resolveCurrentTurnQueryWithSource,
4479
4863
  resolveCurrentTurnQuery,
4480
4864
  safePersistLegacyMemory,
4481
4865
  stripThreadTestPrefix,
@@ -4493,6 +4877,10 @@ exports.__private = {
4493
4877
  deriveOperationalState,
4494
4878
  deriveActionLedger,
4495
4879
  deriveEntityTimeline,
4880
+ buildMemoryDedupeCandidate,
4881
+ dedupeMemoryItemsForLoad,
4882
+ mergeMemoryDedupeState,
4883
+ evaluateMemoryDedupeWrite,
4496
4884
  scoreOf,
4497
4885
  scoreMetaOf,
4498
4886
  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.28",
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",