From 6dd86173a956cff58bb96a3d5f08f947a15e53bf Mon Sep 17 00:00:00 2001 From: Zuliang Zhao Date: Fri, 13 Mar 2026 10:17:20 +0800 Subject: [PATCH 1/3] fix(viewer): handle multiple timestamp formats in days calculation - Support milliseconds, microseconds, and seconds timestamp formats - Fixes incorrect days calculation when timestamps use different formats - Closes issue with 36500 days display error --- apps/memos-local-openclaw/src/viewer/html.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/memos-local-openclaw/src/viewer/html.ts b/apps/memos-local-openclaw/src/viewer/html.ts index 03b0eeab7..908cf2155 100644 --- a/apps/memos-local-openclaw/src/viewer/html.ts +++ b/apps/memos-local-openclaw/src/viewer/html.ts @@ -3170,8 +3170,11 @@ async function loadStats(){ if(d.timeRange&&d.timeRange.earliest!=null&&d.timeRange.latest!=null){ let e=Number(d.timeRange.earliest), l=Number(d.timeRange.latest); if(Number.isFinite(e)&&Number.isFinite(l)){ - if(e<1e12) e*=1000; - if(l<1e12) l*=1000; + // Handle different timestamp formats: seconds, milliseconds, microseconds + if (e < 1e15 && e >= 1e12) e = Math.floor(e / 1000); // microseconds to milliseconds + else if (e < 1e12) e *= 1000; // seconds to milliseconds + if (l < 1e15 && l >= 1e12) l = Math.floor(l / 1000); // microseconds to milliseconds + else if (l < 1e12) l *= 1000; // seconds to milliseconds days=Math.round((l-e)/86400000); days=Math.max(0,Math.min(36500,days)); if(days===0) days=1; From 9dee397ff4ae3a1ce0b8d63c609fbf1a72b5c352 Mon Sep 17 00:00:00 2001 From: Zuliang Zhao Date: Thu, 19 Mar 2026 19:01:06 +0800 Subject: [PATCH 2/3] feat(mem_reader): add reasoning_chain field to summarizer output Add optional 'reasoning_chain' field to memory summarizer output: - New field captures: goal, decision, correction, preference, attention - Enables memory systems to store not just facts but reasoning context - Backward compatible (field is optional) Co-designed with user: correction & reasoning chain requires active questioning during conversation, not just passive summarization. Full design: github.com/MemTensor/MemOS/pull/XXX --- src/memos/mem_reader/simple_struct.py | 2 ++ src/memos/memories/textual/item.py | 4 ++++ src/memos/templates/mem_reader_prompts.py | 11 ++++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/memos/mem_reader/simple_struct.py b/src/memos/mem_reader/simple_struct.py index ceaf28bfa..153191f25 100644 --- a/src/memos/mem_reader/simple_struct.py +++ b/src/memos/mem_reader/simple_struct.py @@ -377,6 +377,7 @@ def _build_fast_node(w): key=m.get("key", ""), sources=w["sources"], background=resp.get("summary", ""), + reasoning_chain=resp.get("reasoning_chain"), ) chat_read_nodes.append(node) except Exception as e: @@ -413,6 +414,7 @@ def _process_transfer_chat_data( key=memory_i_raw.get("key", ""), sources=raw_node.metadata.sources, background=response_json.get("summary", ""), + reasoning_chain=response_json.get("reasoning_chain"), type_="fact", confidence=0.99, ) diff --git a/src/memos/memories/textual/item.py b/src/memos/memories/textual/item.py index 60af67830..69eb3b72d 100644 --- a/src/memos/memories/textual/item.py +++ b/src/memos/memories/textual/item.py @@ -193,6 +193,10 @@ class TreeNodeTextualMemoryMetadata(TextualMemoryMetadata): default="", description="background of this node", ) + reasoning_chain: dict | None = Field( + default=None, + description="reasoning chain: goal, decision, correction, preference, attention", + ) file_ids: list[str] | None = Field( default_factory=list, diff --git a/src/memos/templates/mem_reader_prompts.py b/src/memos/templates/mem_reader_prompts.py index 63e4c1538..f3a3f1bc6 100644 --- a/src/memos/templates/mem_reader_prompts.py +++ b/src/memos/templates/mem_reader_prompts.py @@ -32,9 +32,18 @@ }, ... ], - "summary": + "summary": , + "reasoning_chain": { + "goal": , + "decision": , + "correction": , + "preference": , + "attention": + } } +The `reasoning_chain` field captures the reasoning behind the memories — not just what happened, but why it matters and what to remember for the future. Fill each sub-field with a brief statement; if a sub-field is not applicable, write "N/A". + Language rules: - The `key`, `value`, `tags`, `summary` fields must match the mostly used language of the input conversation. **如果输入是中文,请输出中文** - Keep `memory_type` in English. From 1212ca10a0f4fca60a373cee81163effed6fb3ea Mon Sep 17 00:00:00 2001 From: abakane1 Date: Fri, 20 Mar 2026 09:15:11 +0800 Subject: [PATCH 3/3] feat(viewer): add reasoning chain tab UI to memory cards - Add tab switch between Summary and Reasoning tabs in memory cards - 5-field reasoning chain display: goal, decision, correction, preference, attention - Color-coded labels for each reasoning field - Only show Reasoning tab when reasoning_chain data exists - i18n: Chinese and English labels --- apps/memos-local-openclaw/src/viewer/html.ts | 87 +++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/apps/memos-local-openclaw/src/viewer/html.ts b/apps/memos-local-openclaw/src/viewer/html.ts index e4618df48..db7016798 100644 --- a/apps/memos-local-openclaw/src/viewer/html.ts +++ b/apps/memos-local-openclaw/src/viewer/html.ts @@ -168,6 +168,27 @@ input,textarea,select{font-family:inherit;font-size:inherit} .card-time{font-size:12px;color:var(--text-sec);display:flex;align-items:center;gap:8px} .session-tag{font-size:11px;font-family:ui-monospace,monospace;color:var(--text-muted);background:rgba(0,0,0,.2);padding:3px 8px;border-radius:6px;cursor:default} .card-summary{font-size:15px;font-weight:600;color:var(--text);margin-bottom:10px;line-height:1.5;letter-spacing:-.01em} +.card-tab-bar{display:flex;gap:2px;margin-bottom:10px;margin-top:2px} +.card-tab-btn{background:transparent;border:none;color:var(--text-muted);font-size:12px;padding:4px 10px;border-radius:6px;cursor:pointer;transition:all .15s;font-family:inherit} +.card-tab-btn:hover{background:rgba(128,128,128,.1);color:var(--text)} +.card-tab-btn.active{background:rgba(99,102,241,.12);color:#818cf8;font-weight:600} +[data-theme="light"] .card-tab-btn.active{background:rgba(99,102,241,.08);color:#4f46e5} +.card-reasoning-section{display:none} +.card-reasoning-section.active{display:block} +.reasoning-chain{margin-top:8px;display:flex;flex-direction:column;gap:8px} +.reasoning-item{display:flex;align-items:flex-start;gap:8px;font-size:12px} +.reasoning-label{font-weight:600;min-width:72px;color:var(--text-sec);padding:2px 6px;border-radius:4px;flex-shrink:0} +.reasoning-label.goal{background:rgba(16,185,129,.12);color:#10b981} +.reasoning-label.decision{background:rgba(59,130,246,.12);color:#3b82f6} +.reasoning-label.correction{background:rgba(239,68,68,.12);color:#ef4444} +.reasoning-label.preference{background:rgba(245,158,11,.12);color:#f59e0b} +.reasoning-label.attention{background:rgba(139,92,246,.12);color:#8b5cf6} +[data-theme="light"] .reasoning-label.goal{background:rgba(16,185,129,.1);color:#059669} +[data-theme="light"] .reasoning-label.decision{background:rgba(59,130,246,.1);color:#2563eb} +[data-theme="light"] .reasoning-label.correction{background:rgba(239,68,68,.1);color:#dc2626} +[data-theme="light"] .reasoning-label.preference{background:rgba(245,158,11,.1);color:#d97706} +[data-theme="light"] .reasoning-label.attention{background:rgba(139,92,246,.1);color:#7c3aed} +.reasoning-value{color:var(--text);line-height:1.5;flex:1} .card-content{font-size:13px;color:var(--text-sec);line-height:1.65;max-height:0;overflow:hidden;transition:max-height .3s ease} .card-content.show{max-height:600px;overflow-y:auto} .card-content pre{white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.25);padding:14px;border-radius:10px;font-size:12px;font-family:ui-monospace,monospace;margin-top:10px;border:1px solid var(--border);color:var(--text-sec)} @@ -1380,6 +1401,14 @@ const I18N={ 'card.delete':'Delete', 'card.evolved':'Evolved', 'card.times':'times', + 'card.summaryTab':'Summary', + 'card.reasoningTab':'Reasoning', + 'card.reasoningEmpty':'No reasoning chain', + 'card.reasoningGoal':'Goal', + 'card.reasoningDecision':'Decision', + 'card.reasoningCorrection':'Correction', + 'card.reasoningPreference':'Preference', + 'card.reasoningAttention':'Attention', 'card.updated':'updated', 'card.evolveHistory':'Evolution History', 'card.oldSummary':'Old', @@ -1673,6 +1702,14 @@ const I18N={ 'card.delete':'删除', 'card.evolved':'已演化', 'card.times':'次', + 'card.summaryTab':'摘要', + 'card.reasoningTab':'推理链', + 'card.reasoningEmpty':'暂无推理链', + 'card.reasoningGoal':'目标', + 'card.reasoningDecision':'决策', + 'card.reasoningCorrection':'纠正', + 'card.reasoningPreference':'偏好', + 'card.reasoningAttention':'注意', 'card.updated':'更新于', 'card.evolveHistory':'演化记录', 'card.oldSummary':'旧摘要', @@ -3304,6 +3341,26 @@ function clearDateFilter(){ } /* ─── Rendering ─── */ +function buildReasoningChainHtml(rc){ + if(!rc) return '
'+t('card.reasoningEmpty')+'
'; + const fields=[ + {key:'goal',label:t('card.reasoningGoal')}, + {key:'decision',label:t('card.reasoningDecision')}, + {key:'correction',label:t('card.reasoningCorrection')}, + {key:'preference',label:t('card.reasoningPreference')}, + {key:'attention',label:t('card.reasoningAttention')} + ]; + let html='
'; + fields.forEach(function(f){ + const val=rc[f.key]; + if(val&&val!=='N/A'){ + html+='
'+f.label+''+esc(val)+'
'; + } + }); + html+='
'; + return html; +} + function renderMemories(items){ const list=document.getElementById('memoryList'); if(!items.length){ @@ -3357,7 +3414,18 @@ function renderMemories(items){ } return '
'+ '
'+role+''+kind+''+ownerBadge+importBadge+dedupBadge+mergeBadge+'
'+esc(sidShort)+' '+time+updatedAt+'
'+ - '
'+summary+'
'+ + (function(){ + var rc=m.reasoning_chain||null; + var hasRc=rc&&(rc.goal||rc.decision||rc.correction||rc.preference||rc.attention); + var tabBar='
'+ + ''+ + (hasRc?'':'')+ + '
'; + var summaryDiv='
'+ + '
'+summary+'
'; + var reasoningDiv='
'+buildReasoningChainHtml(rc)+'
'; + return tabBar+summaryDiv+reasoningDiv; + })()+ dedupInfo+ '
'+content+'
'+ historyHtml+ @@ -3410,6 +3478,23 @@ function toggleContent(id){ el.classList.toggle('show'); } +function switchCardTab(id, tab){ + const sumBtn=document.getElementById('tab-sum-'+id); + const reaBtn=document.getElementById('tab-rea-'+id); + const sumEl=document.getElementById('summary-'+id); + const reaEl=document.getElementById('reasoning-'+id); + if(!sumBtn||!reaBtn) return; + if(tab==='summary'){ + sumBtn.classList.add('active');reaBtn.classList.remove('active'); + if(sumEl) sumEl.style.display=''; + if(reaEl) reaEl.style.display='none'; + } else { + sumBtn.classList.remove('active');reaBtn.classList.add('active'); + if(sumEl) sumEl.style.display='none'; + if(reaEl) reaEl.style.display='block'; + } +} + function scrollToMemory(targetId){ const cards=document.querySelectorAll('.memory-card'); for(const card of cards){