Skip to content

feat: voice-dictatable Daily Log in Brain (Obsidian mirror + CoS memory indexing)#145

Merged
atomantic merged 17 commits intomainfrom
feat/daily-log
Apr 17, 2026
Merged

feat: voice-dictatable Daily Log in Brain (Obsidian mirror + CoS memory indexing)#145
atomantic merged 17 commits intomainfrom
feat/daily-log

Conversation

@atomantic
Copy link
Copy Markdown
Owner

Supersedes #144 (auto-closed when its base feat/local-voice-assistant was 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

  • Click the Daily Log tab (/brain/daily-log) or say "open my daily log" — CoS navigates for you
  • Say "start dictating" — every utterance appends verbatim to today's log until you say "stop dictation". The CoS stops talking back so you can monologue.
  • Say "read me my daily log" — the log is spoken back through the Kokoro/Piper TTS pipeline
  • Say "add to my daily log: X" — one-shot append without entering dictation mode
  • Settings panel lets you point the log at an Obsidian vault; every entry mirrors to <vault>/<folder>/YYYY-MM-DD.md so iCloud backup picks it up

What landed

Backend

  • server/services/brainJournal.js — per-date entry store with segment metadata (source, timestamp), fire-and-forget Obsidian mirror
  • server/routes/brain.jsGET/PUT/DELETE /api/brain/daily-log/:date, POST /:date/append, GET/PUT /settings, POST /sync-obsidian
  • server/services/voice/tools.js — 5 new tools: daily_log_open, daily_log_start_dictation, daily_log_stop_dictation, daily_log_append, daily_log_read
  • server/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 turn
  • server/sockets/voice.js — per-socket dictation state + voice:dictation:set channel for UI control
  • server/services/brainMemoryBridge.js — new journals brain type (category daily-log); each entry vector-embeds into the CoS memory system on change so agents can semantically search across daily logs

Frontend

  • client/src/components/brain/tabs/DailyLogTab.jsx — editor with date navigator, history sidebar, quick-append bar, Dictate/Read-back buttons, Obsidian vault picker
  • client/src/components/voice/VoiceWidget.jsx — listens for voice:navigate, voice:dictation, voice:dailyLog:appended; shows a "Dictating to Daily Log" chip
  • client/src/services/voiceClient.jssetDictation() helper + sendText() now takes a source hint so Web Speech dictation routes through the spoken-input path

Tool side-effect mechanism: tools now receive a ctx with a sideEffects[] 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 pass
  • Client builds clean
  • 11 unit tests for brainJournal (append concat, segment metadata, invalid date, Obsidian create/update flow, autoSync-off skip)
  • 9 route tests for /api/brain/daily-log/* (list, today resolution, append, replace, settings GET/PUT)
  • Manual smoke:
    • Open /brain/daily-log, type into textarea, save
    • Quick-append adds a new paragraph
    • Say "open my daily log and start dictating" — verify navigation + dictation toggle
    • Speak a sentence; verify it appends with voice source metadata
    • Say "stop dictation" — verify mode exits and CoS confirms
    • Say "read me my daily log" — verify TTS reads content verbatim
    • Configure Obsidian vault; verify mirror file appears

Copilot AI review requested due to automatic review settings April 17, 2026 19:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 brainJournal service + 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.

Comment thread server/sockets/voice.js Outdated
Comment thread server/services/voice/pipeline.js Outdated
Comment thread server/services/brainJournal.js Outdated
Comment thread client/src/components/brain/tabs/DailyLogTab.jsx Outdated
Comment thread client/src/components/brain/tabs/DailyLogTab.jsx Outdated
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
- 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.
Copilot AI review requested due to automatic review settings April 17, 2026 21:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread server/services/brainJournal.js Outdated
Comment thread server/routes/brain.js
Comment thread server/sockets/voice.js
…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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread server/services/brainJournal.js
Comment thread server/services/brainJournal.js Outdated
- 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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread client/src/components/brain/tabs/DailyLogTab.jsx
Comment thread client/src/components/brain/tabs/DailyLogTab.jsx
Comment thread client/src/components/brain/tabs/DailyLogTab.jsx Outdated
Comment thread server/services/brainMemoryBridge.js Outdated
…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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@atomantic atomantic merged commit 545eb3e into main Apr 17, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants