Status: Archived audit. It records the evidence available during app-shell prototyping; source-line references and listed gaps are not maintained as the production event model changes.
Build Activity as a projection over a fixed, bounded window of facts, not as a
second log format. Human rows should summarize command tasks, dictation
captures, meeting lifecycles, and actionable health failures. Expanding a row
should show the contributing VoiceEvent records and, for meetings, the
MeetingManifest facts.
The current data supports useful summaries, but it does not support the prototype's context-mismatch, audio-drop, model-state, meeting-detection, or "pasted into Slack" explanations. Those claims should remain absent until the small additions below exist.
| Human explanation | Current evidence and limit |
|---|---|
| "Voice Control session started" | VoiceEvent::SessionStarted is an exact boundary (src/events.rs:10, emitted at src/recognition.rs:73). The following State normally supplies VoiceState and device. Sound settings are not present. |
| "Voice Control is listening/sleeping/dictating/transcribing/stopping on DEVICE" | Exact VoiceEvent::State facts (src/events.rs:13-17). The event does not record what caused the transition, so only adjacent command/dictation facts may explain it. |
| "Heard TEXT" | A completed Transcript records text and inference latency_ms (src/events.rs:18-23, src/recognition.rs:280). Started and Updated are provisional facts, not separate user actions. |
| "TEXT woke Voice Control" / "put Voice Control to sleep" | CommandOutcome::Woke and Slept are exact resolver outcomes and are paired with mode.wake / mode.sleep (src/recognition.rs:401-412). |
| "TEXT matched COMMAND and was submitted/executed" | Command has heard, command id, outcome, and the decision-time context label (src/events.rs:24-33). Submitted means queued, not successful. A later Executed means the action returned success, not that an external application visibly changed. |
| "COMMAND failed: ERROR" | CommandOutcome::Failed(String) retains the returned error (src/events.rs:75-83, src/recognition.rs:432-460). It does not distinguish queue submission failure from action execution failure without interpreting the error text. |
| "TEXT was ignored" | CommandOutcome::Ignored is exact. No current fact says why it was ignored. |
| "Dictation started/transcribed/pasted/logged/repasted/was discarded/cancelled/failed" | DictationPhase records those lifecycle facts and final text where available (src/events.rs:61-72, src/recognition.rs:611-652). Destination application, capture duration, trigger, transcription target, and failure stage are absent. |
| "Foreground context changed to APPLICATION / browser URL" | VoiceEvent::Context records application and URL (src/events.rs:39-44, src/recognition.rs:98-105). window_title is captured in ContextSnapshot but omitted from the event (src/context.rs:11-15, src/context.rs:68-76). |
| "Meeting recording began at TIME and capture ended at TIME" | MeetingManifest.created_at_ms and ended_at_ms support those statements (src/meeting.rs:21-34). Current/final MeetingStatus, duration, error, per-track files, sample rates, timing, sample counts, and dropped_packets are also available from the manifest (src/meeting.rs:37-55). Intermediate status transition times are not. |
- Ignored-command reason:
Decision::Ignoreconflates non-wake speech whileMode::Sleeping, no phrase match while listening, and a phrase whoseContextPredicatedid not match (src/commands.rs:329-370). The emitted command hascommand: None; it cannot support "requires x.com, current context Slack" or name a rejected candidate. - Historical command context: the command stores only
ContextSnapshot::label(). The nearestContextevent is useful, but capture failures are only warnings, the previous snapshot remains active, and updates may be dropped when the channel is full (src/context.rs:23-46). The event does not expose context freshness orwindow_title. - One command task: asynchronous actions emit
Submittedand then anotherCommandwithExecutedorFailed(src/recognition.rs:432-460). There is no task id, so those records cannot be joined reliably when ids, text, and context repeat. - One dictation capture: phase records have no capture id. They cannot be
joined safely across queued worker results, voice controls, Option capture,
Captain's Log, paste-last, or repaste.
Failed(String)also collapses worker rejection, model failure, transcription failure, and paste failure. - Audio health: microphone queue drops are counted in
AudioInputbut only reported throughtracingonce per second (src/audio.rs:14-15,src/audio.rs:57-59,src/recognition.rs:87-96). CoreAudio stream errors are also tracing-only (src/audio.rs:98-100). NoVoiceEventsupports an audio drop row, affected interval, or current stream-health claim. - Model state: successful loading, model identity/version, loading time, and
Moonshine failure are absent from
VoiceEvent.WorkerEvent::ModelFailedis flattened intoDictationPhase::Failed(src/recognition.rs:636-642), while startup failures can occur beforeSessionStartedis emitted. - Meeting chronology:
ControllerEvent::{Offer, RecordingStarted, Transcribing, Finished, Failed}is transient UI state and is never appended tologs/live.ndjson(src/meeting_watcher.rs:38-44,src/meeting_watcher.rs:351-374). Offer acceptance/dismissal, provider source, and status transition timestamps therefore disappear after the process exits. - Meeting detection ended: while recording,
controller_loopdoes not poll microphone applications (src/meeting_watcher.rs:449-466). The prototype's "microphone-process activity disappeared; recording is still active" fact is not currently observed. - Why a meeting is interrupted:
meeting::listprojects any persistedRecordingmanifest asInterrupted(src/meeting.rs:554-556). Capture setup/start/stop/writer errors after the initial manifest can leave that same state without persistingerror, so the cause cannot be recovered.
- Add a
task_idtoVoiceEvent::Command, generated before resolution and preserved fromSubmittedthroughExecuted/Failed. Add acapture_idtoVoiceEvent::Dictationand to command records that control that capture. These two ids are enough to form stable command and dictation rows; transcript updates do not need ids for the first slice. - Add a typed ignored reason produced by
CommandConfig::resolve, rather than reconstructed by the UI:Sleeping,NoPhraseMatch, orContextMismatch { command, required: CommandScope }. For a mismatch, record every same-phrase rejected contextual command if more than one exists. Also record the evaluated application/browser host and context observation time; do not parse the displaycontextlabel. - Add a failure stage to failed command and dictation facts. The minimum useful
values are command
Submission/Executionand dictationCapture/Model/Transcription/Paste/Log. Keep the existing error string. - Append meeting lifecycle facts to the same bounded chronology: offered,
dismissed, recording, transcribing, complete, and failed, each with timestamp,
one meeting task id, title,
MeetingCandidate.source, and eventual meeting id/error. Keep track counters and file names inMeetingManifest; do not copy them into every activity event. - Persist
MeetingStatus::Failedpluserrorfor every failure after manifest creation. Reserve the read-timeInterruptedprojection for an abandonedRecordingmanifest; never present it as a known crash explanation.
Add only three domain-specific issue facts, emitted on change or failure rather
than periodically: microphone dropped chunks (count and interval), microphone
stream failure (device and error), and foreground-context capture failure (error
and last successful observation time). Add explicit Moonshine and Parakeet
loading / ready / failed facts only if the pane promises model health.
This is sufficient for the named questions and avoids a generic metrics or
observability schema.
Do not emit normal audio levels, per-chunk events, spans, counters with arbitrary
names, or copies of process.log into VoiceEvent.
- One command row per
task_id: completedheardtext, matched command id, final outcome, and a typed explanation. ShowSubmittedonly while pending. - One dictation row per
capture_id: start through pasted/logged/discarded/ cancelled/failed, with final text where appropriate. Do not name a destination application until it is recorded at the paste action. - One meeting row per meeting task id: offer/user decision, recording,
transcribing, and final status. Surface nonzero
dropped_packetsas a warning after the manifest is finalized. - One row for actionable audio, context-capture, or model failures. Normal context changes and normal model/audio state stay secondary.
SessionStartedis a divider, not an action row.Stoppingcan close a session; without it, label the prior session "end not recorded" rather than inferring a crash.
- All contributing NDJSON records, including
TranscriptPhase::Started/Updated, latency,Statetransitions, exact timestamps, device, context URL, intermediateSubmitted, and exact error strings. - The selected meeting's manifest fields, especially per-track timing, sample
counts, and
dropped_packets. - Context changes may be browsable as raw facts, but "commands are now available" is only historical truth if the resolver decision/candidate set was recorded at that time.
Apply the fixed limit to raw events, then group only records inside that window.
Mark a task as beginning before the window when its opening fact is absent. Keep
every SessionStarted inside the window as a visible divider. The current
dashboard instead drains everything before the latest session
(src/dashboard.rs:89-100), so its read_events behavior should not define the
app-shell projection.
CommandConfig::catalog already provides the read-only source of phrase,
aliases, description, id, and CommandScope (src/commands.rs:375-430). It has
no task/group field. Add one task label to resolver definitions and carry it
through CommandInfo; do not derive grouping from id prefixes or prose
descriptions. Use catalog() for the full grouped list and current resolver
state to annotate availability. available_catalog() alone hides contextual
commands whose predicate does not currently match (src/commands.rs:432-447).