From 526d92f41c28ebefc9e5ee082421899cd4c14de4 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 9 Apr 2026 12:41:28 +0200 Subject: [PATCH] fix(memoro): diagnostic logs + loading states + transcription source label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User reported three issues after the Phase 5 + the encryption-decrypt fix landed: 1. Auto-title still doesn't appear (placeholder "Titel..." stays empty) 2. No loading state visible while transcription / title are in flight 3. Transcript should say which STT engine produced it This commit ships diagnostics for issue 1 and concrete UX for 2 + 3. Issue 1 — diagnostics (no fix yet, root cause unknown): Add console.info logs at every step of the auto-title pipeline so the next test session surfaces exactly where it breaks: - memos.svelte.ts after llmTaskQueue.enqueue() succeeds: "[memoro] enqueued title task { taskId, memoId }" - memos.svelte.ts on enqueue failure: "[memoro] failed to enqueue title task: " - memoro/llm-watcher.svelte.ts on subscribe: "[memoro-llm-watcher] starting subscription" - watcher's next handler when rows arrive: "[memoro-llm-watcher] saw N done title task(s)" - applyRow logs each step: drop / skip / write / consume Refactor: extract per-row logic into applyRow() so the next handler loop can wrap each row in try/catch — a single bad row won't crash the watcher and prevent later rows from being processed. Belt-and-suspenders startup sweep: run a one-shot manual sweep of done rows immediately after subscribing. Dexie liveQuery sometimes misses the first emission when the subscription is set up in the same microtask as a recent table update; the sweep catches any done rows that already exist from a previous tab session OR that were written between layout mount and subscription start. Encryption check fix: the previous skip-if-manual-title check read `memo.title?.trim()` after Dexie.get(), but Dexie reads return the ENCRYPTED row (no decrypt hook) — so memo.title is either null/undefined (no manual title) OR an `enc:1:...` blob (manual title set). Either way, presence-check is enough; no need to decrypt to know whether the user filled it in. The old code happened to work because trim() on a non-empty string returns truthy regardless. Comment now spells this out. Issue 2 — visible loading states: apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte Transcript area now branches on processingStatus: - processing → "Wird transkribiert…" with three pulsing dots (CSS @keyframes loadingPulse) - failed → red error message + manual retry hint - completed + transcript → the transcript itself + source label - completed + no transcript → italic "Kein Transkript vorhanden." Title input placeholder swaps to "Titel wird generiert…" while a generateTitleTask for this memo is in pending or running state. The check uses a Dexie liveQuery against llmQueueDb.tasks via the [refType+refId] compound index, returning the most recent task row. Reactive — the placeholder switches back to plain "Titel…" the moment the watcher writes the title and deletes the queue row. Issue 3 — transcription source label: Below the transcript: a small italic "Voxtral via mana-stt" label. Hardcoded to Voxtral because that's services/mana-stt's default model (DEFAULT_MODEL = "mistralai/Voxtral-Mini-3B-2507" in voxtral_service.py). If we ever route to Whisper or another model per-request, the label will need to come from the response payload rather than be hardcoded — Phase 5.5 work. After this commit lands, the test loop is: record a memo, watch the browser console for the [memoro] / [memoro-llm-watcher] log lines. Whichever step is missing identifies the broken link. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../lib/modules/memoro/llm-watcher.svelte.ts | 121 +++++++++++++----- .../lib/modules/memoro/stores/memos.svelte.ts | 6 +- .../modules/memoro/views/DetailView.svelte | 96 +++++++++++++- 3 files changed, 185 insertions(+), 38 deletions(-) diff --git a/apps/mana/apps/web/src/lib/modules/memoro/llm-watcher.svelte.ts b/apps/mana/apps/web/src/lib/modules/memoro/llm-watcher.svelte.ts index 3963e29cd..364c86715 100644 --- a/apps/mana/apps/web/src/lib/modules/memoro/llm-watcher.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/memoro/llm-watcher.svelte.ts @@ -24,6 +24,7 @@ */ import { liveQuery, type Subscription } from 'dexie'; +import type { QueuedTask } from '@mana/shared-llm'; import { llmQueueDb } from '$lib/llm-queue'; import { encryptRecord } from '$lib/data/crypto'; import { memoTable } from './collections'; @@ -35,53 +36,113 @@ export function startMemoroLlmWatcher(): void { if (subscription) return; // already running if (typeof window === 'undefined') return; // SSR-safe no-op + console.info('[memoro-llm-watcher] starting subscription'); + const observable = liveQuery(async () => llmQueueDb.tasks .where('state') .equals('done') - .and((t) => t.taskName === 'common.generateTitle' && t.refType === 'memo') + .and((t: QueuedTask) => t.taskName === 'common.generateTitle' && t.refType === 'memo') .toArray() ); subscription = observable.subscribe({ next: async (rows) => { + if (rows.length === 0) return; + console.info(`[memoro-llm-watcher] saw ${rows.length} done title task(s)`); + for (const row of rows) { - if (!row.refId || typeof row.result !== 'string') { - // Result shape didn't match — drop the queue row so we - // don't keep retrying it. - await llmQueueDb.tasks.delete(row.id); - continue; + try { + await applyRow(row); + } catch (err) { + console.warn('[memoro-llm-watcher] failed to apply row', row.id, err); + // Best-effort: mark the row consumed so we don't keep + // retrying a row that crashes the watcher every cycle. + try { + await llmQueueDb.tasks.delete(row.id); + } catch { + /* ignore */ + } } - - const memo = await memoTable.get(row.refId); - if (!memo) { - // Memo was deleted before the task finished — discard. - await llmQueueDb.tasks.delete(row.id); - continue; - } - - // Don't overwrite a manual title that the user typed - // between enqueue time and result time. - if (memo.title?.trim()) { - await llmQueueDb.tasks.delete(row.id); - continue; - } - - const diff: Partial = { - title: row.result, - updatedAt: new Date().toISOString(), - }; - await encryptRecord('memos', diff); - await memoTable.update(row.refId, diff); - - // Mark consumed - await llmQueueDb.tasks.delete(row.id); } }, error: (err) => { console.warn('[memoro-llm-watcher] subscription error:', err); }, }); + + // Belt-and-suspenders: Dexie liveQuery sometimes misses the FIRST + // emission if the subscription is set up in the same microtask as + // the table update. Trigger an immediate manual sweep on startup + // so any rows already done from a previous tab session get picked up. + void runOneSweep(); +} + +async function runOneSweep(): Promise { + try { + const rows = await llmQueueDb.tasks + .where('state') + .equals('done') + .and((t: QueuedTask) => t.taskName === 'common.generateTitle' && t.refType === 'memo') + .toArray(); + if (rows.length === 0) { + console.info('[memoro-llm-watcher] startup sweep: no pending done rows'); + return; + } + console.info(`[memoro-llm-watcher] startup sweep: applying ${rows.length} row(s)`); + for (const row of rows) { + try { + await applyRow(row); + } catch (err) { + console.warn('[memoro-llm-watcher] startup sweep failed for row', row.id, err); + } + } + } catch (err) { + console.warn('[memoro-llm-watcher] startup sweep error:', err); + } +} + +async function applyRow(row: QueuedTask): Promise { + if (!row.refId || typeof row.result !== 'string') { + console.info( + `[memoro-llm-watcher] dropping row ${row.id} — missing refId or result not a string` + ); + await llmQueueDb.tasks.delete(row.id); + return; + } + + const memo = await memoTable.get(row.refId); + if (!memo) { + console.info(`[memoro-llm-watcher] dropping row ${row.id} — memo ${row.refId} not found`); + await llmQueueDb.tasks.delete(row.id); + return; + } + + // Don't overwrite a manual title that the user typed + // between enqueue time and result time. The memo we just read + // from Dexie is still ENCRYPTED — title is either null/undefined + // (no manual title) or an `enc:1:...` blob (manual title set). + // Either way, presence-check is enough — we don't need to decrypt + // to know if the user filled it in. + if (typeof memo.title === 'string' && memo.title.trim()) { + console.info( + `[memoro-llm-watcher] memo ${row.refId} already has a title — skipping auto-title` + ); + await llmQueueDb.tasks.delete(row.id); + return; + } + + console.info(`[memoro-llm-watcher] writing title to memo ${row.refId}: "${row.result}"`); + const diff: Partial = { + title: row.result, + updatedAt: new Date().toISOString(), + }; + await encryptRecord('memos', diff); + await memoTable.update(row.refId, diff); + + // Mark consumed + await llmQueueDb.tasks.delete(row.id); + console.info(`[memoro-llm-watcher] applied + cleared row ${row.id}`); } export function stopMemoroLlmWatcher(): void { diff --git a/apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts b/apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts index d9010bb64..ad7484866 100644 --- a/apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts @@ -126,15 +126,17 @@ export const memosStore = { // none (regex-based first-sentence fallback). if (!existing.title?.trim() && transcript.length > 0) { try { - await llmTaskQueue.enqueue( + const taskId = await llmTaskQueue.enqueue( generateTitleTask, { text: transcript, language: existing.language ?? result.language ?? 'de' }, { refType: 'memo', refId: memoId, priority: 1 } ); - } catch { + console.info('[memoro] enqueued title task', { taskId, memoId }); + } catch (err) { // Don't let queue failures break the transcription path. // Worst case the memo stays untitled — the user can still // rename it manually. + console.warn('[memoro] failed to enqueue title task:', err); } } } catch (e) { diff --git a/apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte index bc87516db..088cf2773 100644 --- a/apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte +++ b/apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte @@ -4,8 +4,10 @@ -->