personal memory agent
0
fork

Configure Feed

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

convey/chat: add stalled-talent timeout to chat listener (wave 2)

Switch the chat websocket listener to the options overload.
Track spawned talent cards, mark stalled cards after 3 minutes, and align server-rendered talent statuses.

+48 -4
+1 -1
apps/chat/_chat_event.html
··· 22 22 <button type="button" 23 23 class="chat-talent-card chat-talent-card--finished" 24 24 data-talent-use-id="{{ ev.use_id }}" 25 - data-talent-status="completed"> 25 + data-talent-status="finished"> 26 26 <span class="chat-talent-card-label">{{ ev.name }} finished</span> 27 27 {% if ev.summary %}<span class="chat-talent-card-summary">{{ ev.summary }}</span>{% endif %} 28 28 </button>
+47 -3
apps/chat/workspace.html
··· 34 34 {% endif %} 35 35 </section> 36 36 37 + <style> 38 + .chat-talent-card--stalled { 39 + border-left: 3px solid var(--warn-amber, #d39b2c); 40 + opacity: 0.78; 41 + } 42 + 43 + .chat-talent-card-stalled-note { 44 + color: var(--warn-amber, #a66d00); 45 + font-size: 0.9em; 46 + margin-top: 4px; 47 + } 48 + </style> 49 + 37 50 <script> 38 51 (function () { 39 52 const root = document.querySelector('.chat-app'); ··· 51 64 hour: 'numeric', 52 65 minute: '2-digit' 53 66 }); 67 + const CHAT_TALENT_STALL_MS = 3 * 60 * 1000; 54 68 let searchTimer = null; 69 + let chatEventsCleanup = null; 55 70 56 71 insertTimeSeparators(transcript); 57 72 decorateBubbles(transcript); 58 73 59 74 if (isToday && window.appEvents) { 60 - const off = window.appEvents.listen('chat', (msg) => { 75 + chatEventsCleanup = window.appEvents.listen('chat', { 76 + schema: ['kind'], 77 + correlationKey: 'use_id', 78 + timeout: CHAT_TALENT_STALL_MS, 79 + onDrop: (err) => window.logError(err, { context: 'chat: listener drop' }), 80 + onTimeout: handleChatTalentTimeout, 81 + }, (msg) => { 61 82 appendEventFromLive(msg, transcript); 62 83 }); 63 - window.addEventListener('beforeunload', off, { once: true }); 84 + window.addEventListener('beforeunload', chatEventsCleanup, { once: true }); 64 85 } 65 86 66 87 transcript.addEventListener('click', (event) => { ··· 161 182 162 183 list.querySelector('.chat-empty')?.remove(); 163 184 list.appendChild(item); 185 + if (kind === 'talent_spawned' && msg.use_id) { 186 + chatEventsCleanup?.pending?.track(msg.use_id); 187 + } 164 188 decorateBubbles(list); 165 189 insertTimeSeparators(list); 166 190 list.scrollTop = list.scrollHeight; ··· 202 226 return buildTalentCard(event.name + ' started', event.task || '', event.use_id, 'active', 'chat-talent-card--spawned'); 203 227 } 204 228 if (event.kind === 'talent_finished') { 205 - return buildTalentCard(event.name + ' finished', event.summary || '', event.use_id, 'completed', 'chat-talent-card--finished'); 229 + return buildTalentCard(event.name + ' finished', event.summary || '', event.use_id, 'finished', 'chat-talent-card--finished'); 206 230 } 207 231 if (event.kind === 'talent_errored') { 208 232 return buildTalentCard(event.name + ' errored', event.reason || '', event.use_id, 'errored', 'chat-talent-card--errored'); ··· 256 280 } 257 281 258 282 return card; 283 + } 284 + 285 + function handleChatTalentTimeout(useId) { 286 + if (!useId) return; 287 + const cards = transcript.querySelectorAll(`[data-talent-use-id="${CSS.escape(useId)}"]`); 288 + cards.forEach((card) => { 289 + if (card.dataset.talentStatus === 'finished' || card.dataset.talentStatus === 'errored') { 290 + return; 291 + } 292 + card.dataset.talentStatus = 'stalled'; 293 + card.classList.remove('chat-talent-card--spawned'); 294 + card.classList.add('chat-talent-card--stalled'); 295 + let note = card.querySelector('.chat-talent-card-stalled-note'); 296 + if (!note) { 297 + note = document.createElement('span'); 298 + note.className = 'chat-talent-card-stalled-note'; 299 + card.appendChild(note); 300 + } 301 + note.textContent = 'Talent stopped responding — reload to retry'; 302 + }); 259 303 } 260 304 261 305 function buildReflectionCard(dayValue, url) {