Skip to content

feat(trace): unified per-conversation forensic recorder for chat + search#139

Merged
quiet-node merged 14 commits intomainfrom
worktree-gentle-dancing-wave
May 7, 2026
Merged

feat(trace): unified per-conversation forensic recorder for chat + search#139
quiet-node merged 14 commits intomainfrom
worktree-gentle-dancing-wave

Conversation

@quiet-node
Copy link
Copy Markdown
Owner

Summary

  • Generalizes the search-pipeline trace recorder (PR feat(search): add forensic trace recorder #126) into a unified, per-conversation forensic recorder that captures every chat turn AND every search turn into JSONL files under app_data_dir()/traces/{chat,search}/<conversation_id>.jsonl.
  • Single dev-only toggle: [debug] trace_enabled = true (also exposed as a Settings panel switch). Off by default. Legacy field search_trace_enabled continues to work via serde(alias).
  • Output is dev-grade artifacts. Point an external agent (Claude Code, jq, a Python notebook) at the folder to study patterns and improve the system prompt. No in-app analyzer, no cloud upload, no fine-tune loop.

What changed

Backend (src-tauri/src/trace/) — new module:

  • trace::ids::ConversationId newtype + new_turn_id() (moved from search/recorder.rs).
  • trace::recorder::TraceRecorder trait (was PipelineRecorder), RecorderEvent enum (existing search variants unchanged + new chat variants: ConversationStart, UserMessage, AssistantThinking, AssistantTokens, AssistantComplete, ScreenCaptured, ConversationEnd), TraceDomain { Chat, Search }, FileRecorder::for_conversation(root, domain, conv_id), NoopRecorder, test-only MockRecorder.
  • trace::registry::RegistryRecorder — production composition: parking_lot::RwLock<HashMap<(TraceDomain, ConversationId), Arc<FileRecorder>>> with lazy insert, hot-path Arc caching, ConversationEnd-triggered flush + evict, and append-mode late-event tolerance.
  • trace::BoundRecorder — wraps Arc<dyn TraceRecorder> + ConversationId so existing recorder.record(event) callsites stay unchanged through the search pipeline.

Schema bump v1 → v2. Each JSONL line now carries top-level domain and conversation_id. Search-event variant names and fields are unchanged. File location moves from traces/turn-<id>.jsonl (one per search turn) to traces/{chat,search}/<conversation_id>.jsonl (one per conversation, per domain).

IPC contract changes. ask_ollama gains conversation_id: String, is_first_turn: bool, slash_command: Option<String>. search_pipeline gains conversation_id: String. capture_full_screen_command gains conversation_id: String. New record_conversation_end(conversation_id, reason) command lets the frontend signal user-perceived end-of-conversation (used by useOllama.reset() and useOllama.loadMessages()) so the chat-domain trace gets a clean closing line.

Frontend. useOllama mints a stable trace conversation id per session (lazy via ensureTraceConversationId), threads it through every invoke, fires record_conversation_end on reset() and loadMessages(). Settings UI label updated from "Search trace" → "Trace recording"; helper text reflects both domains.

Coverage gate. 100% lines + 100% functions across both Rust (cargo +nightly llvm-cov --fail-under-lines 100) and TypeScript (Vitest 100% threshold). 750 Rust tests + 1166 frontend tests all pass.

Design doc

~/.gstack/projects/quiet-node-thuki/logan-worktree-gentle-dancing-wave-design-20260506-135848.md (Status: APPROVED, post 2 rounds of adversarial spec review, 9/10 quality, 15 issues caught and fixed). Plus eng-review pass that surfaced the Settings-UI propagation scope.

Test plan

  • bun run validate-build clean (zero warnings, zero errors)
  • bun run test:all:coverage clean (100% line / function gate)
  • Smoke test (Logan): edit config.toml, set [debug] trace_enabled = true, launch Thuki, run a conversation that includes /search and /screen, verify:
    • traces/chat/<id>.jsonl and traces/search/<id>.jsonl files appear
    • Chat file contains conversation_start, user_message, assistant_tokens, screen_captured, assistant_complete
    • Search file contains the existing search-pipeline events with the new conversation_id field
    • Reset conversation → conversation_end line appears with reason "user_reset"
    • Loading a saved conversation from history rotates the trace file (closes old + starts new)
    • Disabling the toggle silently switches the recorder back to noop on next launch
  • Migration sanity (Logan): keep an existing [debug] search_trace_enabled = true line, launch, confirm serde(alias) accepts it (file is rewritten as trace_enabled only on next settings save)

Notes

  • Single PR per request (combines what the design doc originally split into PR1 mechanical refactor + PR2 chat-domain capture).
  • DO NOT MERGE until Logan's smoke test confirms the dev loop works end-to-end. Local /code-review will run before merge.

quiet-node added 14 commits May 6, 2026 16:53
…arch

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…ge-off commands

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…t duplicate ConversationStart

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
@quiet-node quiet-node merged commit 76f9180 into main May 7, 2026
3 checks passed
@quiet-node quiet-node deleted the worktree-gentle-dancing-wave branch May 7, 2026 06:18
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.

1 participant