personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

convey/home: adopt wave 0 error primitives for refresh (wave 1)

Migrate refreshSkills, refreshRoutines, and refreshBriefing to apiJson(...) with shape validation that raises ApiError with cause 'parse' on malformed responses so legitimate-empty and malformed states stay distinct. Skills and routines render error cards in place, while briefing uses replaceLoading(...) to preserve the existing card and surface refresh failures as a sibling error.

Co-authored-by: OpenAI Codex <codex@openai.com>

+235 -192
+235 -192
apps/home/workspace.html
··· 1605 1605 html += '<a href="/app/health">health →</a>'; 1606 1606 el.innerHTML = html; 1607 1607 }) 1608 - .catch(function(err) { console.error('home: refreshVitals failed', err); }); 1608 + .catch(function(err) { window.logError(err, { context: 'home: refreshVitals failed' }); }); 1609 1609 } 1610 1610 1611 1611 function refreshNarrative() { ··· 1627 1627 } 1628 1628 } 1629 1629 }) 1630 - .catch(function(err) { console.error('home: refreshNarrative failed', err); }); 1630 + .catch(function(err) { window.logError(err, { context: 'home: refreshNarrative failed' }); }); 1631 1631 } 1632 1632 1633 1633 function esc(s) { ··· 1636 1636 return d.innerHTML; 1637 1637 } 1638 1638 1639 - function refreshRoutines() { 1640 - fetch('/app/home/api/pulse') 1641 - .then(r => r.json()) 1642 - .then(data => { 1643 - var container = document.getElementById('pulse-routines'); 1644 - var routines = (data.routines || []).filter(function(r) { return !r.seen; }); 1645 - if (routines.length === 0) { 1646 - if (container) container.remove(); 1647 - return; 1639 + function malformedHomeResponse(url, serverMessage) { 1640 + return new window.ApiError({ 1641 + status: 200, 1642 + statusText: 'OK', 1643 + serverMessage: serverMessage, 1644 + url: url, 1645 + cause: 'parse', 1646 + }); 1647 + } 1648 + 1649 + async function refreshRoutines() { 1650 + try { 1651 + var data = await window.apiJson('/app/home/api/pulse'); 1652 + if (!Array.isArray(data.routines)) { 1653 + throw malformedHomeResponse('/app/home/api/pulse', 'Malformed pulse response'); 1654 + } 1655 + var container = document.getElementById('pulse-routines'); 1656 + var routines = data.routines.filter(function(r) { return !r.seen; }); 1657 + if (routines.length === 0) { 1658 + if (container) container.remove(); 1659 + return; 1660 + } 1661 + var wasCollapsed; 1662 + if (container) { 1663 + wasCollapsed = container.dataset.sectionCollapsed; 1664 + } else { 1665 + try { 1666 + var saved = JSON.parse(sessionStorage.getItem('pulse-section-state') || '{}'); 1667 + wasCollapsed = saved['pulse-routines'] || SECTION_DEFAULTS['pulse-routines']; 1668 + } catch(e) { 1669 + wasCollapsed = SECTION_DEFAULTS['pulse-routines']; 1648 1670 } 1649 - var wasCollapsed; 1650 - if (container) { 1651 - wasCollapsed = container.dataset.sectionCollapsed; 1671 + } 1672 + var expanded = wasCollapsed === 'false' ? 'true' : 'false'; 1673 + var summaryText = routines.length + ' new routine' + (routines.length !== 1 ? 's' : ''); 1674 + var html = '<div class="pulse-section-toggle" role="button" tabindex="0" aria-expanded="' + expanded + '" onclick="toggleSection(this.parentElement)">'; 1675 + html += '<h2 class="pulse-section-header">your routines</h2>'; 1676 + html += '<span class="pulse-section-summary">' + esc(summaryText) + '</span>'; 1677 + html += '</div>'; 1678 + html += '<div class="pulse-section-body">'; 1679 + html += '<div class="pulse-routines-list">'; 1680 + routines.forEach(function(r) { 1681 + var name = esc(r.name); 1682 + html += '<div class="pulse-routine-item" role="button" tabindex="0" data-conversation="Show me my ' + name + ' from today" data-routine-click="true"><span>'; 1683 + html += '<span class="pulse-routine-name">' + name + '</span> '; 1684 + html += '<span class="pulse-routine-time">' + esc(r.run_time_display) + '</span>'; 1685 + if (r.summary) html += ' <span class="pulse-routine-summary">— ' + esc(r.summary) + '</span>'; 1686 + html += '</span></div>'; 1687 + }); 1688 + html += '</div><div class="pulse-routines-more"><a href="#" data-conversation="Show me all my routine history">all routine history →</a></div>'; 1689 + html += '</div>'; 1690 + if (container) { 1691 + container.innerHTML = html; 1692 + container.dataset.sectionCollapsed = wasCollapsed; 1693 + } else { 1694 + var newDiv = document.createElement('div'); 1695 + newDiv.className = 'pulse-routines'; 1696 + newDiv.id = 'pulse-routines'; 1697 + newDiv.dataset.sectionCollapsed = wasCollapsed; 1698 + newDiv.innerHTML = html; 1699 + var narrative = document.getElementById('pulse-narrative'); 1700 + var today = document.getElementById('pulse-today'); 1701 + if (narrative) { 1702 + narrative.parentNode.insertBefore(newDiv, narrative.nextSibling); 1703 + } else if (today) { 1704 + today.parentNode.insertBefore(newDiv, today); 1652 1705 } else { 1653 - try { 1654 - var saved = JSON.parse(sessionStorage.getItem('pulse-section-state') || '{}'); 1655 - wasCollapsed = saved['pulse-routines'] || SECTION_DEFAULTS['pulse-routines']; 1656 - } catch(e) { 1657 - wasCollapsed = SECTION_DEFAULTS['pulse-routines']; 1658 - } 1706 + var dash = document.querySelector('.pulse-dashboard'); 1707 + if (dash) dash.appendChild(newDiv); 1659 1708 } 1660 - var expanded = wasCollapsed === 'false' ? 'true' : 'false'; 1661 - var summaryText = routines.length + ' new routine' + (routines.length !== 1 ? 's' : ''); 1662 - var html = '<div class="pulse-section-toggle" role="button" tabindex="0" aria-expanded="' + expanded + '" onclick="toggleSection(this.parentElement)">'; 1663 - html += '<h2 class="pulse-section-header">your routines</h2>'; 1664 - html += '<span class="pulse-section-summary">' + esc(summaryText) + '</span>'; 1665 - html += '</div>'; 1666 - html += '<div class="pulse-section-body">'; 1667 - html += '<div class="pulse-routines-list">'; 1668 - routines.forEach(function(r) { 1669 - var name = esc(r.name); 1670 - html += '<div class="pulse-routine-item" role="button" tabindex="0" data-conversation="Show me my ' + name + ' from today" data-routine-click="true"><span>'; 1671 - html += '<span class="pulse-routine-name">' + name + '</span> '; 1672 - html += '<span class="pulse-routine-time">' + esc(r.run_time_display) + '</span>'; 1673 - if (r.summary) html += ' <span class="pulse-routine-summary">— ' + esc(r.summary) + '</span>'; 1674 - html += '</span></div>'; 1709 + } 1710 + } catch (err) { 1711 + var routinesContainer = document.getElementById('pulse-routines'); 1712 + if (routinesContainer) { 1713 + routinesContainer.innerHTML = window.SurfaceState.errorCard({ 1714 + heading: 'Couldn\'t refresh routines', 1715 + desc: 'Reload the page to try again.', 1716 + serverMessage: err?.serverMessage || err?.message || '', 1675 1717 }); 1676 - html += '</div><div class="pulse-routines-more"><a href="#" data-conversation="Show me all my routine history">all routine history →</a></div>'; 1677 - html += '</div>'; 1678 - if (container) { 1679 - container.innerHTML = html; 1680 - container.dataset.sectionCollapsed = wasCollapsed; 1681 - } else { 1682 - var newDiv = document.createElement('div'); 1683 - newDiv.className = 'pulse-routines'; 1684 - newDiv.id = 'pulse-routines'; 1685 - newDiv.dataset.sectionCollapsed = wasCollapsed; 1686 - newDiv.innerHTML = html; 1687 - var narrative = document.getElementById('pulse-narrative'); 1688 - var today = document.getElementById('pulse-today'); 1689 - if (narrative) { 1690 - narrative.parentNode.insertBefore(newDiv, narrative.nextSibling); 1691 - } else if (today) { 1692 - today.parentNode.insertBefore(newDiv, today); 1693 - } else { 1694 - var dash = document.querySelector('.pulse-dashboard'); 1695 - if (dash) dash.appendChild(newDiv); 1696 - } 1697 - } 1698 - }) 1699 - .catch(function(err) { console.error('home: refreshRoutines failed', err); }); 1718 + } 1719 + } 1700 1720 } 1701 1721 1702 - function refreshSkills() { 1703 - fetch('/app/home/api/pulse') 1704 - .then(r => r.json()) 1705 - .then(data => { 1706 - var container = document.getElementById('pulse-skills'); 1707 - var skills = data.skills || []; 1708 - var newContent = data.skills_content || {}; 1709 - if (skills.length === 0) { 1710 - if (container) container.remove(); 1711 - return; 1722 + async function refreshSkills() { 1723 + try { 1724 + var data = await window.apiJson('/app/home/api/pulse'); 1725 + if (!Array.isArray(data.skills) || (data.skills_content && typeof data.skills_content !== 'object')) { 1726 + throw malformedHomeResponse('/app/home/api/pulse', 'Malformed pulse response'); 1727 + } 1728 + var container = document.getElementById('pulse-skills'); 1729 + var skills = data.skills; 1730 + var newContent = data.skills_content || {}; 1731 + if (skills.length === 0) { 1732 + if (container) container.remove(); 1733 + return; 1734 + } 1735 + Object.keys(newContent).forEach(function(k) { skillContent[k] = newContent[k]; }); 1736 + var wasCollapsed; 1737 + if (container) { 1738 + wasCollapsed = container.dataset.sectionCollapsed; 1739 + } else { 1740 + try { 1741 + var saved = JSON.parse(sessionStorage.getItem('pulse-section-state') || '{}'); 1742 + wasCollapsed = saved['pulse-skills'] || SECTION_DEFAULTS['pulse-skills']; 1743 + } catch(e) { 1744 + wasCollapsed = SECTION_DEFAULTS['pulse-skills']; 1712 1745 } 1713 - // Update content map 1714 - Object.keys(newContent).forEach(function(k) { skillContent[k] = newContent[k]; }); 1715 - var wasCollapsed; 1716 - if (container) { 1717 - wasCollapsed = container.dataset.sectionCollapsed; 1746 + } 1747 + var expanded = wasCollapsed === 'false' ? 'true' : 'false'; 1748 + var summaryText = data.skills_summary || (skills.length + ' skill' + (skills.length !== 1 ? 's' : '')); 1749 + var html = '<div class="pulse-section-toggle" role="button" tabindex="0" aria-expanded="' + expanded + '" onclick="toggleSection(this.parentElement)">'; 1750 + html += '<h2 class="pulse-section-header">your skills</h2>'; 1751 + html += '<span class="pulse-section-summary">' + esc(summaryText) + '</span>'; 1752 + html += '</div>'; 1753 + html += '<div class="pulse-section-body">'; 1754 + html += '<div class="pulse-skills-list">'; 1755 + skills.forEach(function(s) { 1756 + var name = esc(s.name + (s.status === 'dormant' ? ' (dormant)' : '')); 1757 + var facet = esc((s.facets && s.facets.length > 0) ? s.facets[0] : ''); 1758 + var newClass = s.seen ? '' : ' is-new'; 1759 + html += '<div class="pulse-skill-item' + newClass + '" role="button" tabindex="0" data-skill-click="true" data-skill-id="' + esc(s.id) + '"><span>'; 1760 + html += '<span class="pulse-skill-name">' + name + '</span> '; 1761 + html += '<span class="pulse-skill-badge">' + (s.observations || 0) + ' obs</span> '; 1762 + html += '<span class="pulse-skill-facet">' + facet + '</span>'; 1763 + if (s.description) html += ' <span class="pulse-skill-summary">— ' + esc(s.description) + '</span>'; 1764 + html += '</span></div>'; 1765 + html += '<div class="pulse-skill-detail" id="skill-detail-' + esc(s.id) + '" style="display: none;"></div>'; 1766 + }); 1767 + html += '</div>'; 1768 + html += '</div>'; 1769 + if (container) { 1770 + container.innerHTML = html; 1771 + container.dataset.sectionCollapsed = wasCollapsed; 1772 + } else { 1773 + var newDiv = document.createElement('div'); 1774 + newDiv.className = 'pulse-skills'; 1775 + newDiv.id = 'pulse-skills'; 1776 + newDiv.dataset.sectionCollapsed = wasCollapsed; 1777 + newDiv.innerHTML = html; 1778 + var routines = document.getElementById('pulse-routines'); 1779 + var today = document.getElementById('pulse-today'); 1780 + if (routines) { 1781 + routines.parentNode.insertBefore(newDiv, routines.nextSibling); 1782 + } else if (today) { 1783 + today.parentNode.insertBefore(newDiv, today); 1718 1784 } else { 1719 - try { 1720 - var saved = JSON.parse(sessionStorage.getItem('pulse-section-state') || '{}'); 1721 - wasCollapsed = saved['pulse-skills'] || SECTION_DEFAULTS['pulse-skills']; 1722 - } catch(e) { 1723 - wasCollapsed = SECTION_DEFAULTS['pulse-skills']; 1724 - } 1785 + var dash = document.querySelector('.pulse-dashboard'); 1786 + if (dash) dash.appendChild(newDiv); 1725 1787 } 1726 - var expanded = wasCollapsed === 'false' ? 'true' : 'false'; 1727 - var summaryText = data.skills_summary || (skills.length + ' skill' + (skills.length !== 1 ? 's' : '')); 1728 - var html = '<div class="pulse-section-toggle" role="button" tabindex="0" aria-expanded="' + expanded + '" onclick="toggleSection(this.parentElement)">'; 1729 - html += '<h2 class="pulse-section-header">your skills</h2>'; 1730 - html += '<span class="pulse-section-summary">' + esc(summaryText) + '</span>'; 1731 - html += '</div>'; 1732 - html += '<div class="pulse-section-body">'; 1733 - html += '<div class="pulse-skills-list">'; 1734 - skills.forEach(function(s) { 1735 - var name = esc(s.name + (s.status === 'dormant' ? ' (dormant)' : '')); 1736 - var facet = esc((s.facets && s.facets.length > 0) ? s.facets[0] : ''); 1737 - var newClass = s.seen ? '' : ' is-new'; 1738 - html += '<div class="pulse-skill-item' + newClass + '" role="button" tabindex="0" data-skill-click="true" data-skill-id="' + esc(s.id) + '"><span>'; 1739 - html += '<span class="pulse-skill-name">' + name + '</span> '; 1740 - html += '<span class="pulse-skill-badge">' + (s.observations || 0) + ' obs</span> '; 1741 - html += '<span class="pulse-skill-facet">' + facet + '</span>'; 1742 - if (s.description) html += ' <span class="pulse-skill-summary">— ' + esc(s.description) + '</span>'; 1743 - html += '</span></div>'; 1744 - html += '<div class="pulse-skill-detail" id="skill-detail-' + esc(s.id) + '" style="display: none;"></div>'; 1788 + } 1789 + } catch (err) { 1790 + var skillsContainer = document.getElementById('pulse-skills'); 1791 + if (skillsContainer) { 1792 + skillsContainer.innerHTML = window.SurfaceState.errorCard({ 1793 + heading: 'Couldn\'t refresh skills', 1794 + desc: 'Reload the page to try again.', 1795 + serverMessage: err?.serverMessage || err?.message || '', 1745 1796 }); 1746 - html += '</div>'; 1747 - html += '</div>'; 1748 - if (container) { 1749 - container.innerHTML = html; 1750 - container.dataset.sectionCollapsed = wasCollapsed; 1751 - } else { 1752 - var newDiv = document.createElement('div'); 1753 - newDiv.className = 'pulse-skills'; 1754 - newDiv.id = 'pulse-skills'; 1755 - newDiv.dataset.sectionCollapsed = wasCollapsed; 1756 - newDiv.innerHTML = html; 1757 - var routines = document.getElementById('pulse-routines'); 1758 - var today = document.getElementById('pulse-today'); 1759 - if (routines) { 1760 - routines.parentNode.insertBefore(newDiv, routines.nextSibling); 1761 - } else if (today) { 1762 - today.parentNode.insertBefore(newDiv, today); 1763 - } else { 1764 - var dash = document.querySelector('.pulse-dashboard'); 1765 - if (dash) dash.appendChild(newDiv); 1766 - } 1767 - } 1768 - }) 1769 - .catch(function(err) { console.error('home: refreshSkills failed', err); }); 1797 + } 1798 + } 1770 1799 } 1771 1800 1772 - function refreshBriefing() { 1773 - fetch('/app/home/api/briefing') 1774 - .then(function(r) { return r.json(); }) 1775 - .then(function(data) { 1776 - var card = document.getElementById('pulse-briefing'); 1777 - if (!data.exists && data.phase !== 'pending') { 1778 - if (card) card.style.display = 'none'; 1779 - return; 1801 + async function refreshBriefing() { 1802 + try { 1803 + var data = await window.apiJson('/app/home/api/briefing'); 1804 + if (!data || typeof data !== 'object' || typeof data.exists !== 'boolean' || typeof data.phase !== 'string') { 1805 + throw malformedHomeResponse('/app/home/api/briefing', 'Malformed briefing response'); 1806 + } 1807 + if (data.exists && (!data.sections || typeof data.sections !== 'object')) { 1808 + throw malformedHomeResponse('/app/home/api/briefing', 'Malformed briefing response'); 1809 + } 1810 + if (data.needs_deduped && !Array.isArray(data.needs_deduped)) { 1811 + throw malformedHomeResponse('/app/home/api/briefing', 'Malformed briefing response'); 1812 + } 1813 + 1814 + var card = document.getElementById('pulse-briefing'); 1815 + if (!data.exists && data.phase !== 'pending') { 1816 + if (card) card.style.display = 'none'; 1817 + return; 1818 + } 1819 + if (!card) { 1820 + window.location.reload(); 1821 + return; 1822 + } 1823 + card.style.display = ''; 1824 + card.dataset.phase = data.phase; 1825 + if (data.phase === 'morning') { 1826 + card.dataset.collapsed = 'false'; 1827 + } 1828 + var header = card.querySelector('.pulse-briefing-header'); 1829 + if (header) header.setAttribute('aria-expanded', card.dataset.collapsed === 'false' ? 'true' : 'false'); 1830 + 1831 + var summary = card.querySelector('.pulse-briefing-summary'); 1832 + if (!summary && data.summary) { 1833 + summary = document.createElement('div'); 1834 + summary.className = 'pulse-briefing-summary'; 1835 + if (header) { 1836 + header.insertAdjacentElement('afterend', summary); 1780 1837 } 1781 - if (!card) { 1782 - window.location.reload(); 1783 - return; 1784 - } 1785 - card.style.display = ''; 1786 - card.dataset.phase = data.phase; 1787 - if (data.phase === 'morning') { 1788 - card.dataset.collapsed = 'false'; 1789 - } 1790 - var header = card.querySelector('.pulse-briefing-header'); 1791 - if (header) header.setAttribute('aria-expanded', card.dataset.collapsed === 'false' ? 'true' : 'false'); 1838 + } 1839 + if (summary) { 1840 + summary.textContent = data.summary || ''; 1841 + summary.style.display = data.summary ? '' : 'none'; 1842 + } 1792 1843 1793 - var summary = card.querySelector('.pulse-briefing-summary'); 1794 - if (!summary && data.summary) { 1795 - summary = document.createElement('div'); 1796 - summary.className = 'pulse-briefing-summary'; 1797 - if (header) { 1798 - header.insertAdjacentElement('afterend', summary); 1799 - } 1800 - } 1801 - if (summary) { 1802 - summary.textContent = data.summary || ''; 1803 - summary.style.display = data.summary ? '' : 'none'; 1804 - } 1844 + var badge = card.querySelector('.pulse-briefing-badge'); 1845 + if (badge) { 1846 + badge.textContent = data.needs_badge || ''; 1847 + badge.style.display = data.needs_badge ? '' : 'none'; 1848 + } 1805 1849 1806 - var badge = card.querySelector('.pulse-briefing-badge'); 1807 - if (badge) { 1808 - badge.textContent = data.needs_badge || ''; 1809 - badge.style.display = data.needs_badge ? '' : 'none'; 1810 - } 1850 + var meta = card.querySelector('.pulse-briefing-meta'); 1851 + if (meta && data.meta && data.meta.generated) { 1852 + meta.textContent = formatBriefingTime(data.meta.generated); 1853 + } 1811 1854 1812 - var meta = card.querySelector('.pulse-briefing-meta'); 1813 - if (meta && data.meta && data.meta.generated) { 1814 - meta.textContent = formatBriefingTime(data.meta.generated); 1815 - } 1855 + var placeholder = card.querySelector('.pulse-briefing-placeholder'); 1856 + if (placeholder) { 1857 + placeholder.style.display = data.exists ? 'none' : ''; 1858 + } 1816 1859 1817 - var placeholder = card.querySelector('.pulse-briefing-placeholder'); 1818 - if (placeholder) { 1819 - placeholder.style.display = data.exists ? 'none' : ''; 1860 + if (data.exists && data.sections) { 1861 + var body = document.getElementById('pulse-briefing-body'); 1862 + if (!body) { 1863 + window.location.reload(); 1864 + return; 1820 1865 } 1821 1866 1822 - if (data.exists && data.sections) { 1823 - var body = document.getElementById('pulse-briefing-body'); 1824 - if (!body) { 1825 - window.location.reload(); 1826 - return; 1827 - } 1867 + briefingSections = data.sections; 1868 + renderBriefingSections(briefingSections); 1828 1869 1829 - briefingSections = data.sections; 1830 - renderBriefingSections(briefingSections); 1831 - 1832 - var needsBody = body.querySelector('[data-section-key="needs_attention"]'); 1833 - if (needsBody && data.needs_deduped && data.needs_deduped.length > 0) { 1834 - var items = data.needs_deduped.map(function(item) { 1835 - var escaped = item 1836 - .replace(/&/g, '&amp;') 1837 - .replace(/</g, '&lt;') 1838 - .replace(/>/g, '&gt;') 1839 - .replace(/"/g, '&quot;') 1840 - .replace(/'/g, '&#39;'); 1841 - return '<li role="button" tabindex="0" data-conversation="What&#39;s the status of: ' + escaped + '">' + escaped + '</li>'; 1842 - }); 1843 - needsBody.innerHTML = '<ul>' + items.join('') + '</ul>'; 1844 - } else if (needsBody) { 1845 - needsBody.innerHTML = ''; 1846 - } 1870 + var needsBody = body.querySelector('[data-section-key="needs_attention"]'); 1871 + if (needsBody && data.needs_deduped && data.needs_deduped.length > 0) { 1872 + var items = data.needs_deduped.map(function(item) { 1873 + var escaped = item 1874 + .replace(/&/g, '&amp;') 1875 + .replace(/</g, '&lt;') 1876 + .replace(/>/g, '&gt;') 1877 + .replace(/"/g, '&quot;') 1878 + .replace(/'/g, '&#39;'); 1879 + return '<li role="button" tabindex="0" data-conversation="What&#39;s the status of: ' + escaped + '">' + escaped + '</li>'; 1880 + }); 1881 + needsBody.innerHTML = '<ul>' + items.join('') + '</ul>'; 1882 + } else if (needsBody) { 1883 + needsBody.innerHTML = ''; 1847 1884 } 1848 - }) 1849 - .catch(function(err) { console.error('home: refreshBriefing failed', err); }); 1885 + } 1886 + } catch (err) { 1887 + window.SurfaceState.replaceLoading('pulse-briefing', window.SurfaceState.errorCard({ 1888 + heading: 'Couldn\'t refresh briefing', 1889 + desc: 'Reload the page to try again.', 1890 + serverMessage: err?.serverMessage || err?.message || '', 1891 + })); 1892 + } 1850 1893 } 1851 1894 })(); 1852 1895 </script>