Skip to content

feat(dictation): per-session debug-dump JSONL - #68

Merged
Juliusolsson05 merged 2 commits into
mainfrom
feat-dictation-debug-dump
May 11, 2026
Merged

Juliusolsson05 merged 2 commits into
mainfrom
feat-dictation-debug-dump

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Summary

Every dictation press now writes a FAT append-only JSONL under
<userData>/dictation-debug/<id>.dictation.jsonl capturing the
whole pipeline:

  • META: session created, recorder config, final outcome
  • DEVICE: full mic enumeration + which one we selected + the
    labels/muted/readyState of the granted track
  • RECORDER: MediaRecorder lifecycle (start, dataavailable, stop, error)
  • CHUNK: paired renderer:produced (with sha8) and
    main:received (with sha8) — match by chunkIndex to detect
    drops, reorders, or corruption across IPC
  • AUDIO_LEVEL: 7-band analyser samples at ~10Hz with peak —
    the data behind the sine-wave indicator
  • IPC: every stream-start / stream-stop / chunk-push round-trip
  • PROVIDER: batch upload start/end (streaming trace ready to
    wire when streaming returns)
  • TRANSCRIPT: every live preview callback + the final committed text
  • OUTCOME: success / no-speech / error / cancel

Mirrors the ghostJournal shape exactly: 100ms batched writes,
registry flushed on before-quit, 14-day startup prune. Privacy
contract honored — never logs raw audio bytes or API keys; transcript
text is logged because the file is local user-private.

The goal is to diagnose the two symptoms reported: silent sine-wave
indicator and missing live-streaming preview, both happening while
the final batch transcript still comes back fine. The file lets us
distinguish "AnalyserNode reading silence" vs "meter never started"
vs "live-preview path dead (batch fallback)" without needing to
repro on a debugger.

Two commits:

  1. Infrastructure (types, journal, IPC channel, preload bridge,
    main-side instrumentation, plan doc)
  2. Renderer instrumentation + 10Hz audio-level sampler

Test plan

  • Hold Fn in a composer, dictate one sentence, release. A
    <uuid>.dictation.jsonl appears under
    ~/Library/Application Support/cc-shell/dictation-debug/.
  • Renderer CHUNK:renderer:produced sha8 values match main's
    CHUNK:main:received sha8 values for the same chunkIndex.
  • AUDIO_LEVEL:sample events emit at ~10Hz with peak > 0
    while speaking.
  • OUTCOME:success lands with the committed transcript text
    (truncated to 4 KB) at the end of a successful press.
  • Cancel a press (short Fn tap): OUTCOME:cancel is the
    terminal event for that session's journal.
  • Files older than 14 days are pruned on next app start.

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits May 11, 2026 10:05
Add a FAT per-press JSONL writer at
<userData>/dictation-debug/<id>.dictation.jsonl, mirroring the
ghostJournal shape (100ms batched appends, registry flushed on
before-quit, 14-day startup prune). Wires the dictation:debug-event
IPC channel and instruments every main-side site
([dictation:trace] handlers, batch upload start/end, OUTCOME
success/no-speech/error/cancel) with paired CHUNK:main:received
events carrying sha8 fingerprints for cross-process correlation
against the renderer.

Renderer instrumentation lands in the follow-up commit. Plan doc
included for future readers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mint a debugSessionId at recorder construction and route every
existing debug() call site through the per-session journal in
addition to console.debug. Each call site is tagged with a
DictationDebugLayer so the JSONL can be filtered by RECORDER /
CHUNK / IPC / DEVICE / ERROR.

New emission sites:
  - DEVICE: enumerate:audioinput + select:built-in / fallback
  - CHUNK:renderer:produced with sha8 (pairs against main's
    CHUNK:main:received for cross-process correlation — the
    primary lever for chunk-drop / chunk-reorder investigations)
  - AUDIO_LEVEL meter:start (track labels + muted + readyState),
    sample at ~10Hz (peak + per-band levels), meter:stop
  - TRANSCRIPT preview:interim / preview:final / committed

Thread debugSessionId through startDictationStream so main can
key its events to the same file. Microtask-clear the ref in
cleanup so late callbacks (transcript IPC, OUTCOME from main)
still resolve a journal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 merged commit af40431 into main May 11, 2026
Juliusolsson05 added a commit that referenced this pull request May 11, 2026
…irks docs (#71)

* feat(paste): event-driven submit + per-paste debug dump + headless quirks docs

Bundles three coordinated changes addressing the long-standing
"first Enter on a multi-line paste does nothing; second Enter
submits" intermittent in Claude composers.

Track A — documentation in claude-code-headless submodule:
  - Thick WHY comment above HeadlessTerminal.scheduleFlush explaining
    that the 'screen' event silently stops firing under Claude's
    synchronized-output protocol (pendingWrites stays > 0 with
    @xterm/headless under sync-out pressure). Consumers needing
    correctness should poll snapshotPlain() directly.
  - Stronger warning on the activity-event debouncer that the
    SPINNER_VERB_RE walk produces false negatives during
    tool-result rows and transient headers, and the 'activity'
    event must NOT be used as a binary "did this submit?" verdict.
    The paste-submit harness lost 2/10 iterations on a spurious
    spinner read before we figured this out.

Track B — per-paste debug dump:
  Mirror of the dictation-debug subsystem from PR #68. Every Enter
  in a Claude composer mints a pasteId and emits
  RENDER/IPC/PTY/SCREEN/OUTCOME events into
  <userData>/paste-debug/<pasteId>.paste.jsonl. Renderer and main
  both write into the same file keyed by pasteId; payloads are
  sha8-fingerprinted so the IPC and PTY events can be paired across
  the renderer↔main boundary. 100 ms batched, 14-day prune.

  This is the diagnostic that will finally answer "why does the
  PTY-isolated harness pass 10/10 with the production 125 ms timer
  but the user still hits the bug live?" Hypotheses include IPC
  variance shrinking the effective gap, a double-fire on Enter
  where a second \r races the first paste's payload, or a React
  state mutation between paste and submit — the dump shape lets us
  pick the right one from a real failing session.

Track C — event-driven paste-submit:
  Replaces the unconditional 125 ms wall-clock wait between
  bracketed paste and \r with a poll for Claude's `[Pasted text #N]`
  placeholder. The PTY-isolated harness shows event-driven is
  10/10 reliable with an average wait of ~58 ms vs. the timer's
  125 ms unconditional floor. Falls back to the timer after a 2 s
  safety bound if the placeholder never materializes (future-Claude
  rename insurance). Gated by settings.eventDrivenPasteSubmit,
  default ON. Setting also exposed in Settings → Workspace.

Submodule bump:
  packages/claude-code-headless → feat/paste-submit-headless-fix
  e59235a — adds ClaudeCodeHeadless.awaitPastePlaceholder and the
  two doc comments above. Will need to be merged to submodule main
  before this cc-shell PR can merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump claude-code-headless to main (PR #7 merged)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 deleted the feat-dictation-debug-dump branch July 7, 2026 21:56
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