diff --git a/hindsight-docs/blog/2026-02-09-resolving-memory-conflicts.md b/hindsight-docs/blog/2026-02-09-resolving-memory-conflicts.md index 96f77efc..2a8db62f 100644 --- a/hindsight-docs/blog/2026-02-09-resolving-memory-conflicts.md +++ b/hindsight-docs/blog/2026-02-09-resolving-memory-conflicts.md @@ -1,4 +1,14 @@ +--- +title: How We Solved Memory Conflicts in Hindsight +description: Learn how Hindsight handles contradictory information by tracking temporal evolution and preserving history in its memory consolidation system. +authors: [hindsight] +tags: [engineering, memory-systems, conflict-resolution] +image: /img/blog/2026-02-09/consolidation-pipeline.png +date: 2026-02-09 +--- + # How We Solved Memory Conflicts in Hindsight + One of the hardest problems we tackled in Hindsight was dealing with contradictions. When you're building a memory system for AI agents, reality isn't static. It evolves. A CRM agent might learn that "Acme Corp is a key prospect" in January, then encounter "Acme Corp is now a paying customer" in March. Naive approaches either lose the history or drown in duplicate facts. diff --git a/hindsight-docs/src/components/CopyPageButton/index.tsx b/hindsight-docs/src/components/CopyPageButton/index.tsx new file mode 100644 index 00000000..3147af7f --- /dev/null +++ b/hindsight-docs/src/components/CopyPageButton/index.tsx @@ -0,0 +1,162 @@ +import React, { useState, useCallback } from 'react'; +import styles from './styles.module.css'; + +export default function CopyPageButton(): JSX.Element | null { + const [copied, setCopied] = useState(false); + + const copyPageAsMarkdown = useCallback(async () => { + try { + // Get the page content + const contentElement = document.querySelector('.markdown'); + if (!contentElement) return; + + // Convert HTML to markdown-like text + let markdown = ''; + + // Add title + const title = document.querySelector('h1')?.textContent; + if (title) { + markdown += `# ${title}\n\n`; + } + + // Extract text content from the markdown container + const extractMarkdown = (element: Element): string => { + let text = ''; + + const processNode = (node: Node): string => { + if (node.nodeType === Node.TEXT_NODE) { + return node.textContent || ''; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + const el = node as Element; + const tagName = el.tagName.toLowerCase(); + const children = Array.from(el.childNodes).map(processNode).join(''); + + switch (tagName) { + case 'h1': + return `# ${children}\n\n`; + case 'h2': + return `## ${children}\n\n`; + case 'h3': + return `### ${children}\n\n`; + case 'h4': + return `#### ${children}\n\n`; + case 'h5': + return `##### ${children}\n\n`; + case 'h6': + return `###### ${children}\n\n`; + case 'p': + return `${children}\n\n`; + case 'ul': + return `${children}\n`; + case 'ol': + return `${children}\n`; + case 'li': + const parent = el.parentElement; + const isOrdered = parent?.tagName.toLowerCase() === 'ol'; + if (isOrdered) { + const index = Array.from(parent?.children || []).indexOf(el) + 1; + return `${index}. ${children}\n`; + } + return `- ${children}\n`; + case 'code': + const isBlock = el.parentElement?.tagName.toLowerCase() === 'pre'; + if (isBlock) { + const lang = el.className.replace('language-', ''); + return `\`\`\`${lang}\n${children}\n\`\`\`\n\n`; + } + return `\`${children}\``; + case 'pre': + return children; // Already handled by code block + case 'blockquote': + return children.split('\n').map(line => `> ${line}`).join('\n') + '\n\n'; + case 'a': + const href = el.getAttribute('href') || ''; + return `[${children}](${href})`; + case 'strong': + case 'b': + return `**${children}**`; + case 'em': + case 'i': + return `*${children}*`; + case 'br': + return '\n'; + case 'hr': + return '---\n\n'; + case 'table': + return `${children}\n`; + case 'thead': + case 'tbody': + return children; + case 'tr': + return `${children}|\n`; + case 'th': + case 'td': + return `| ${children} `; + case 'img': + const src = el.getAttribute('src') || ''; + const alt = el.getAttribute('alt') || ''; + return `![${alt}](${src})`; + default: + return children; + } + } + + return ''; + }; + + Array.from(element.childNodes).forEach(node => { + text += processNode(node); + }); + + return text; + }; + + // Skip the title h1 if it's already added + const contentToCopy = Array.from(contentElement.children) + .filter(child => !(child.tagName === 'H1' && child.textContent === title)) + .map(child => extractMarkdown(child)) + .join(''); + + markdown += contentToCopy; + + // Clean up excessive newlines + markdown = markdown.replace(/\n{3,}/g, '\n\n').trim(); + + // Copy to clipboard + await navigator.clipboard.writeText(markdown); + + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (error) { + console.error('Failed to copy page content:', error); + } + }, []); + + return ( + + ); +} \ No newline at end of file diff --git a/hindsight-docs/src/components/CopyPageButton/styles.module.css b/hindsight-docs/src/components/CopyPageButton/styles.module.css new file mode 100644 index 00000000..7db51752 --- /dev/null +++ b/hindsight-docs/src/components/CopyPageButton/styles.module.css @@ -0,0 +1,55 @@ +.copyPageButton { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + background: transparent; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 6px; + color: var(--ifm-font-color-base); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.copyPageButton:hover { + background-color: var(--ifm-color-emphasis-100); + border-color: var(--ifm-color-emphasis-400); +} + +.copyPageButton:active { + transform: translateY(1px); +} + +.copyPageButton.copied { + background-color: var(--ifm-color-success-contrast-background); + border-color: var(--ifm-color-success); + color: var(--ifm-color-success-darkest); +} + +.copyPageButton.copied:hover { + background-color: var(--ifm-color-success-contrast-background); + border-color: var(--ifm-color-success); +} + +.buttonText { + margin: 0 4px; +} + +/* Dark mode adjustments */ +[data-theme='dark'] .copyPageButton { + border-color: var(--ifm-color-emphasis-400); +} + +[data-theme='dark'] .copyPageButton:hover { + background-color: var(--ifm-color-emphasis-200); + border-color: var(--ifm-color-emphasis-500); +} + +[data-theme='dark'] .copyPageButton.copied { + background-color: var(--ifm-color-success-dark); + border-color: var(--ifm-color-success); + color: var(--ifm-color-success-contrast-foreground); +} \ No newline at end of file diff --git a/hindsight-docs/src/theme/DocItem/Content/index.tsx b/hindsight-docs/src/theme/DocItem/Content/index.tsx new file mode 100644 index 00000000..97a084e5 --- /dev/null +++ b/hindsight-docs/src/theme/DocItem/Content/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import DocItemContent from '@theme-original/DocItem/Content'; +import type DocItemContentType from '@theme/DocItem/Content'; +import type { WrapperProps } from '@docusaurus/types'; +import CopyPageButton from '@site/src/components/CopyPageButton'; +import styles from './styles.module.css'; + +type Props = WrapperProps; + +export default function DocItemContentWrapper(props: Props): JSX.Element { + return ( + <> +
+
+ +
+
+ + + ); +} \ No newline at end of file diff --git a/hindsight-docs/src/theme/DocItem/Content/styles.module.css b/hindsight-docs/src/theme/DocItem/Content/styles.module.css new file mode 100644 index 00000000..da75a9b0 --- /dev/null +++ b/hindsight-docs/src/theme/DocItem/Content/styles.module.css @@ -0,0 +1,20 @@ +.docItemHeader { + display: flex; + justify-content: flex-end; + align-items: center; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.docItemActions { + display: flex; + gap: 8px; + align-items: center; +} + +@media (max-width: 768px) { + .docItemHeader { + margin-bottom: 0.75rem; + } +} \ No newline at end of file