feat: voice-dictatable Daily Log in Brain (Obsidian mirror + CoS memory indexing)#145
feat: voice-dictatable Daily Log in Brain (Obsidian mirror + CoS memory indexing)#145
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new Daily Log feature under Brain that supports typed entries, hands-free voice dictation (LLM-short-circuit), optional Obsidian mirroring, and automatic indexing into the CoS memory system for semantic search.
Changes:
- Introduces
brainJournalservice + REST API for date-keyed daily log CRUD/append and settings (Obsidian + bulk resync). - Extends the voice system with Daily Log tools, dictation state, and a side-effects channel (navigate/dictation) applied by the voice pipeline.
- Adds a new Brain UI tab for Daily Log and updates the Voice widget/client to support dictation routing and UI indicators.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| server/sockets/voice.js | Adds per-socket dictation state, passes source/state into the pipeline, and exposes voice:dictation:set. |
| server/services/voice/tools.js | Adds 5 Daily Log voice tools and updates tool dispatch to accept a context for side-effects. |
| server/services/voice/pipeline.js | Implements dictation-mode LLM short-circuit, stop-phrase handling, and applies tool side-effects (navigate/dictation). |
| server/services/brainMemoryBridge.js | Adds journals brain type and sync/event handlers to embed daily log content into memory backend. |
| server/services/brainJournal.test.js | New unit tests covering append/replace behavior and Obsidian mirror flow. |
| server/services/brainJournal.js | New daily log store, segment metadata, settings persistence, and Obsidian mirroring/resync. |
| server/routes/brain.test.js | Adds route-level tests for /api/brain/daily-log/* endpoints and invalid-date handling. |
| server/routes/brain.js | Adds daily log routes (list/get/append/replace/delete/settings/sync-obsidian) and date validation. |
| server/lib/brainValidation.js | Adds Zod schema for daily log settings payload. |
| client/src/services/voiceClient.js | Adds source hint to sendText() and introduces setDictation() socket helper. |
| client/src/services/apiBrain.js | Adds client API helpers for daily log endpoints. |
| client/src/pages/Brain.jsx | Wires the new daily-log tab route to DailyLogTab. |
| client/src/components/voice/VoiceWidget.jsx | Listens for navigate/dictation/appended events and surfaces a dictation indicator chip. |
| client/src/components/brain/tabs/DailyLogTab.jsx | New Daily Log UI: date navigation, editor, history, quick-append, dictation toggle, read-back, and Obsidian settings. |
| client/src/components/brain/constants.js | Adds Daily Log to the Brain tab list with icon. |
| .changelog/NEXT.md | Documents the new Daily Log feature and its voice/Obsidian/memory-indexing integration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Adds a Daily Log tab to the Brain section for free-form typed or dictated daily entries. One entry per date, stored in data/brain/journals.json with per-segment source metadata. Voice integration: - 5 new CoS tools: daily_log_open, daily_log_start_dictation, daily_log_stop_dictation, daily_log_append, daily_log_read - Tools emit client-side side effects (voice:navigate, voice:dictation) via a new ctx.sideEffects channel in the tool dispatcher - Dictation mode short-circuits the LLM/TTS pipeline at the server — every voice utterance appends straight to the log until "stop dictation" - Web Speech STT mode honored via source='voice' hint on voice:text Obsidian mirror: - Optional per-date markdown file under <vault>/<folder>/YYYY-MM-DD.md, reusing the existing Notes vault registry (iCloud backup comes for free) - Obsidian writes are fire-and-forget so dictation isn't blocked on iCloud CoS memory bridge: - New journals brain type (daily-log category) in brainMemoryBridge.js - Each entry vector-embeds into the memory system on change so agents can semantically search across daily logs
Pre-merge review caught: - pipeline.js runTurn redeclared the 'source' function parameter as a 'const' on line 232 for the turn-log prefix — SyntaxError under ESM strict mode. Tests masked it because no test imports pipeline.js. Rename to 'inputKind'. - DailyLogTab imported FolderOpen and X from lucide-react; neither is used in the file.
- pipeline: default dictation date to today when missing, don't throw - pipeline: propagate source hint to voice:transcript for typed turns - brainJournal: reset segments on full content replace so metadata stays consistent - brainJournal.test: fix beforeEach to recreate TEMP_ROOT (mkdirSync, not mkdtempSync) - DailyLogTab: only toast success + clear state when DELETE actually succeeds - brain route: validate :date as 'today' or real ISO YYYY-MM-DD (400 otherwise)
…ion edge cases - brainJournal: isIsoDate now validates real calendar days (rejects 2026-02-30) - brainJournal: emit journals:deleted so memory bridge can archive the memory - brainMemoryBridge: handle journals:deleted; clarify bulk-sync backfill comment - pipeline: short-circuit empty dictation transcripts (no bogus append event) - pipeline: clear dictation date when disabling via tool side-effect - voice socket: normalize/validate date in voice:dictation:set - routes/brain.test: cover DELETE, sync-obsidian, and 400-on-malformed-date paths
…s input - brainJournal: drop unused readFile import; remove unused filenamePattern setting - brainJournal: correct scheduleObsidianSync comment to match persistObsidianPath's reload-and-write behavior - routes/brain: reuse journal.isIsoDate rather than duplicating date validator - routes/brain: validate PUT /daily-log/settings via strict Zod schema (dailyLogSettingsSchema) - routes/brain: reject whitespace-only append text with 400 instead of silent no-op - pipeline: keep voice:transcript.source='text' for voice:text path; expose caller's origin as inputSource so Web Speech transcripts aren't duplicated in chat history - brainJournal.test: test syncToObsidian() directly to avoid fire-and-forget timing flake - changelog: drop hardcoded '14 tools' total (Daily Log added 5 more)
…ne-aware client - voice socket: harden voice:dictation:set against null/non-object payloads - brainJournal: emit per-entry 'journals:upserted' on append/setContent so memory bridge re-embeds only the changed day (was O(totalDays) per segment) - brainMemoryBridge: listen on journals:upserted + journals:deleted; drop whole-store journals:changed handler - DailyLogTab: fetch backend 'today' via GET /daily-log/today so remote/VPN users in different browser timezones land on the correct day - brain route: clamp /daily-log list pagination (limit>=1, offset>=0) - persistObsidianPath: clarify single-user one-shot semantics
…hange - DailyLogTab: guard setSettings updater against null settings (pre-fetch state) - brainJournal.syncToObsidian: persist obsidianPath whenever it differs, not only when missing — so a folder rename or manual vault move doesn't leave deleteJournal() pointing at a stale path
brainMemoryBridge.syncAllBrainData: skip already-mapped journal records in dry-run
too, so dry-run output matches the comment ('already-mapped records are skipped')
and produces the same stats as a real run instead of claiming to re-sync every day.
…ian sync - DailyLogTab.onDictation: only toast on false->true transition so the server echo doesn't stack a second toast on top of toggleDictation()'s local one - brainJournal.persistObsidianPath: expand comment to explicitly state why no mutex is needed (single-user, single-instance, single-threaded Node loop per CLAUDE.md; obsidianPath is a set-once/rename-rare field and caller already persisted authoritative content before this runs)
…idian resync
- voice socket: harden voice:text handler against null/non-object payloads (same pattern as voice:dictation:set)
- brainJournal.syncToObsidian: add {force:true} option so manual 'Re-sync all entries now' bypasses the autoSync check — users who disable auto-sync can still trigger a one-shot bulk sync
- resyncAllToObsidian: pass force:true through
- test: cover the force bypass path
…mary
- DailyLogTab: re-sync server dictation target when date changes mid-dictation so prev/next/day-picker nav routes new utterances to the currently-viewed day, not the one active when dictation started
- daily_log_read tool: keep summary short ('Daily log for YYYY-MM-DD (N segments).') instead of embedding the full content — JSON-stringified tool results end up in subsequent LLM message history, so duplicating the log body there doubled token cost for no benefit. Full content is still returned once in content.
…tale comment - DailyLogTab: don't clobber unsaved textarea edits when a voice segment arrives for the current day — keep the user's in-progress content and toast a notice instead (entry state still updates so segment count reflects the append) - brainMemoryBridge: update stale 'journals:changed' comment to reference the actual 'journals:upserted'/'journals:deleted' wiring - pipeline: report real synth latency on the stop-dictation TTS emit instead of hardcoded 0 so client timing stats are accurate
99e1958 to
70bd566
Compare
- voice:dictation:set now honors the voice.enabled gate; a blocked enable emits voice:error and forces dictation off so the UI can't drift into "dictating" while the server rejects turns. - voice:dailyLog:appended ships only the delta (date/text/segment/ segmentCount/updatedAt) instead of the whole growing entry. Client patches local state from the delta, keeping wire size O(segment) rather than O(entry) over a long dictation session. - Journal store writes (append/set/delete/persistObsidianPath) now share an async mutex, so a fire-and-forget Obsidian path persist can't clobber segments written by a concurrent appendJournal. - Read-back button switches to the daily_log_read tool when content would exceed the socket's 4000-char cap, preventing silent rejection of long logs. - Removed the duplicate "Dictation on" toast from the voice:dictation server-echo handler — the initiator (UI toggle or voice tool confirmation) owns the user feedback; under rapid toggles the echo could otherwise fire stale toasts.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…esh socket header - server/services/brainJournal.js: remove unused withStore() helper — all writers already use storeMutex() directly, so this was dead code. Add SEGMENT_SOURCES allowlist + normalizeSource() so unknown/non-string sources from callers (HTTP body, socket payload) can't corrupt on-disk segment metadata. Unknown values fall back to 'text'. - server/sockets/voice.js: header comment now enumerates the full inbound/outbound event surface, including voice:dictation:set, voice:dictation, voice:navigate, and voice:dailyLog:appended.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- brainJournal.js listJournals() defaults to slim summaries
(id/date/updatedAt/segmentCount/obsidianPath/obsidianVaultId). Opt in
to full entries via { includeContent: true } for internal callers
(resyncAllToObsidian, brainMemoryBridge bulk sync) and the HTTP route
(?includeContent=1). Sidebar payloads are now O(records) not
O(records × content-size).
- Persist entry.obsidianVaultId alongside entry.obsidianPath.
deleteJournal()/removeFromObsidian() now refuse to delete when the
entry's recorded vault doesn't match the currently-configured vault —
otherwise a vault swap would delete an unrelated note at the same
relative path in the new vault. Stale note gets a warning; user
handles it manually.
- DailyLogTab sidebar + onAppend handler updated to read from
segmentCount (authoritative) with fallback to legacy segments.length,
and to only patch sidebar-relevant fields (metadata) rather than
synthesizing fake segments to match the count.
- Added 3 tests covering summary-mode default, includeContent opt-in,
and the vault-swap delete-refusal.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…asts, handler rejection - DailyLogTab.jsx loadEntry() now uses a monotonic request counter and ignores responses whose id doesn't match the latest issued request — prevents an older in-flight fetch from overwriting entry/content with stale data for the wrong date when the user mashes prev/next or when the server-today fetch resolves after a manual date pick. - History state now holds slim summaries (id/date/updatedAt/ obsidianPath/segmentCount) instead of whatever shape happened to be handed to applyEntry(). toHistorySummary() coerces full entries into sidebar shape so memory and render cost stay O(records), not O(records × content-size). - Dictation toggle defers its toast until the server echoes the change via voice:dictation, and reverts state + surfaces an error toast if voice:error stage='dictation' arrives while the toggle is pending (e.g. voice mode disabled). No more optimistic 'Dictation on' toast when the server actually refused. - brainMemoryBridge.js journals:deleted listener now wraps the async handleJournalDeleted() in .catch() — matches the pattern used by the other brain bridge handlers and prevents an unhandled rejection if loadBridgeMap() fails.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Supersedes #144 (auto-closed when its base
feat/local-voice-assistantwas deleted after #143 merged). Rebased onto main so this now targets the correct base and the diff is scoped to the Daily Log work only.Summary
Adds a Daily Log tab to the Brain section — a date-indexed diary you can type into or dictate into hands-free via the voice Chief-of-Staff.
User flow
/brain/daily-log) or say "open my daily log" — CoS navigates for you<vault>/<folder>/YYYY-MM-DD.mdso iCloud backup picks it upWhat landed
Backend
server/services/brainJournal.js— per-date entry store with segment metadata (source, timestamp), fire-and-forget Obsidian mirrorserver/routes/brain.js—GET/PUT/DELETE /api/brain/daily-log/:date,POST /:date/append,GET/PUT /settings,POST /sync-obsidianserver/services/voice/tools.js— 5 new tools:daily_log_open,daily_log_start_dictation,daily_log_stop_dictation,daily_log_append,daily_log_readserver/services/voice/pipeline.js— dictation mode short-circuits the LLM, routes audio straight into the daily log; stop-phrase regex falls through to a normal confirmation turnserver/sockets/voice.js— per-socket dictation state +voice:dictation:setchannel for UI controlserver/services/brainMemoryBridge.js— newjournalsbrain type (categorydaily-log); each entry vector-embeds into the CoS memory system on change so agents can semantically search across daily logsFrontend
client/src/components/brain/tabs/DailyLogTab.jsx— editor with date navigator, history sidebar, quick-append bar, Dictate/Read-back buttons, Obsidian vault pickerclient/src/components/voice/VoiceWidget.jsx— listens forvoice:navigate,voice:dictation,voice:dailyLog:appended; shows a "Dictating to Daily Log" chipclient/src/services/voiceClient.js—setDictation()helper +sendText()now takes asourcehint so Web Speech dictation routes through the spoken-input pathTool side-effect mechanism: tools now receive a
ctxwith asideEffects[]channel. Tools push{ type: 'navigate', path }/{ type: 'dictation', enabled, date }— the pipeline applies server-state changes and forwards client-facing effects over the socket.Review history
Previously went through 10 rounds of Copilot review in #144 before the retargeting. Known-good commits preserved via rebase. Resolved conflicts (sockets + voiceClient) where the voice branch's hardening PRs (ensureEnabled gate, text length cap, typed audio guards) overlapped my dictation/source additions — kept the stricter guards and threaded dictation/source through them.
Test plan
server: vitest run— 2447 / 2447 passbrainJournal(append concat, segment metadata, invalid date, Obsidian create/update flow, autoSync-off skip)/api/brain/daily-log/*(list,todayresolution, append, replace, settings GET/PUT)/brain/daily-log, type into textarea, save