Skip to content

fix(desk-floater): keep voice recordings until their words are delivered - #264

Merged
Amplify-Logic merged 7 commits into
mainfrom
fm/desk-voice-never-lose-g1
Oct 7, 2026
Merged

Amplify-Logic merged 7 commits into
mainfrom
fm/desk-voice-never-lose-g1

Conversation

@Amplify-Logic

@Amplify-Logic Amplify-Logic commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Intent

"Can you check the last voice note? I just did a really long one, but it seems like it didn't send to you. Can you also make sure that that can't happen again? I think it went on for too long, but it should still be stored."

Context from firstmate's check: the note did arrive, but in the desk-voice mailbox rather than the chat, and only reached firstmate about six minutes later. Separately, desk-floater/Sources/DeskFloater.swift transcribeAndDeliver deletes the recorded audio file right after transcription, before checking the result, so a recording whose transcription fails or comes back empty (shown as "No speech") is lost for good.

What Changed

  • The desk floater now records into state/desk-voice/recording/ and no longer deletes the audio as soon as transcription finishes. A recording is removed only once its words are delivered. If transcription fails (0.5 s or longer), comes back empty (2 s or longer), or delivery fails, the recording goes to a private state/desk-voice/unsent/ folder. Talk-to-Firstmate recordings are retried automatically after 20 s, 60 s and 5 min, and the floater shows "Saved - retrying". Dictation recordings show "Saved, not sent" and wait for a manual retry. When the floater launches, it saves any recordings a previous run left behind and picks up retries that still have attempts left. The new Recording, SendOutcome and RetryResult helpers live in Recordings.swift, with Swift tests.
  • bin/fm-desk-voice.sh gets three new commands. keep saves a recording to the unsent folder with a JSON record of its purpose, attempt count and last failure reason. recordings lists what is saved. retry re-transcribes saved recordings under a lock, then delivers the words, or prints them for dictation. When a message falls back to the mailbox, deliver now also rings the primary in the background on a retry schedule (FM_DESK_VOICE_RING_DELAYS), so the message doesn't wait for the primary's next turn end. The ring stops once the message is drained, once a ring is typed, when no proven primary holds the session lock, and in away or quiet mode. send now fails if it can't save to the mailbox.
  • bin/fm-deepgram-stt.sh still uploads long recordings whole, with no length cap. Each request now has limits: a 15 s connect timeout, a total timeout that grows with the file size (120 s plus 1 s per 100 kB), and two retries on timeouts, dropped connections or busy responses. The docs (docs/desk-floater.md, docs/scripts.md, the operational home layout skill), fork-surface.conf and tests/fm-deepgram-desk.test.sh are updated to cover the new folders, commands and behavior.

🤖 Generated with Claude Code

Risk Assessment

✅ Low: The fix round now sends ring_mailbox through the same ring_line → submit_ring path the ring subcommand uses, with its stash refusal and payload re-check before each Enter. The outcome mapping is unchanged: rung or rung-unconfirmed stops the ring, not-rung retries, and no proven primary (rc 1) stops it. The ring subcommand still prints the same line and exits 0, and the new mailbox race test would fail against the old code, both by submitting a draft and by typing over a stash. I found no remaining defects in the rest of the change.

Testing

I ran the focused desk-voice suite (84 checks, including the Swift recording tests; all passed), then drove the change live. Every setup was disposable: lab FM_HOMEs, a stand-in Deepgram, a real Claude primary on a private lab tmux socket, and the real build-only floater app. Those runs covered keeping and retrying a ten-minute note, dictation retry, the path guards, a failed delivery leaving the recording saved, the mailbox ring reaching a real primary and waiting out a draft, launch recovery with automatic retry (with screenshots of the floater status line), and a never-answering endpoint failing in bounded time. Everything passed. A fresh microphone capture failing while the floater runs was not driven live because it needs microphone and Accessibility permissions this session doesn't have. All lab homes, helper processes and the desk-floater/.build output were removed afterwards.

  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
A 10-minute talk-to-Firstmate note whose transcription fails is saved privately, not lost, and is delivered whole once Deepgram works again ✅ pass live cli-keep-retry-transcript.txt: keep moved the 19,200,044-byte WAV to unsent/ (folder 0700, files 0600). Retries reported 'unsent … 2 … HTTP 502', then 'unsent … 3 no speech heard', then 'delivered … m…
A saved dictation is never sent to Firstmate; retry gives back its words and then removes it ✅ pass live cli-dictation-and-guards-transcript.txt: retry printed 'transcript <path> Dear team, …'; the mailbox count stayed the same; recordings was empty afterwards. live-floater-recovery-transcript.txt: the…
Adversarial: retry/keep touch only saved audio recordings, and a failed delivery leaves the recording saved ✅ pass live cli-dictation-and-guards-transcript.txt: retry of an outside file and of a unsent/../../../ path were refused (exit 2, file left alone). keep of a .txt and of a symlink were refused. With the mailbox…
The floater, restarted after stopping mid-recording, saves the leftover capture (even one with an unfinished WAV header), drops a stray press, leaves an in-progress capture alone, and keeps retrying a… ✅ pass live live-floater-recovery-transcript.txt plus floater-saved-retrying.png and floater-after-auto-retry.png. The 30s capture with a data length of 0 in its header was saved as firstmate. The 0.2s stray was…
A note saved to the mailbox rings a busy or idle real Firstmate primary within seconds, and the primary collects it ✅ pass live live-mailbox-ring-claude-primary.txt: deliver returned at once; within 6s the lab Claude primary's chat showed '[firstmate desk-voice] a desk voice message is waiting in the mailbox…' submitted. The p…
Adversarial: the mailbox ring never types over or submits the captain's half-typed draft, and rings once the box is free ✅ pass live live-mailbox-ring-draft-guard.txt: with 'captain draft half typed thought' in the chat box, nothing was typed or submitted for 12s. After Ctrl-U cleared it, the ring landed on its next try about 21s l…
A transcription request to a Deepgram that never answers gives up in bounded time instead of hanging ✅ pass live live-stt-never-answers.txt: real curl, routed to a local socket that never answers, made 3 attempts (15s connect bound each, 3s apart), then fm-deepgram-stt exited 1 with 'curl failed talking to Deepg…
While the floater is running, a fresh microphone recording whose transcription fails (or hears no words in a recording of 2 seconds or more) is saved and shown as 'Saved - retrying' instead of being d… ⏸️ untested no This needs a real microphone capture started from the floater's talk button or hotkey. The build-only DeskFloater app has no Microphone permission, and pressing its button needs Accessibility or compu…

Floater status line after launch recovery saved a stopped capture

Evidence: Live floater recovery and auto-retry transcript

Source: Live floater recovery and auto-retry transcript

\### Real DeskFloater (build-only app) launched with FM_HOME=lab, stand-in Deepgram via FM_DEEPGRAM_CURL, no primary (mailbox path).
Seeded state/desk-voice/recording/ before launch at 10:06:04:
  A AAAAAAAA-...-firstmate.wav  30s talk capture cut off mid-recording (WAV header data length 0), 2 min old
  B BBBBBBBB-...-dictate.wav    5s dictation, 2 min old
  C CCCCCCCC-...-firstmate.wav  0.2s stray press, 2 min old
  D DDDDDDDD-...-firstmate.wav  3s capture touched just before launch (still being written)

4s after launch: recording/ held only D; unsent/ held A (firstmate, 960044 bytes) and B (dictate), mode 0700/0600; C deleted.
fm-desk-voice.sh recordings:
  unsent/20261007T080604Z-0b64eddc.wav  firstmate  1  the floater stopped before it was sent
  unsent/20261007T080604Z-93069617.wav  dictate    1  the floater stopped before it was sent
Status line: 'Saved - retrying' (floater-saved-retrying.png)

Stand-in Deepgram calls (floater automatic retries of A only; B, a dictation, is not retried automatically):
  10:06:25 
  10:07:25 
  10:08:02 
  10:06:25 Deepgram down (502) -> A kept, attempts 2, 'transcription failed: Deepgram listen failed HTTP 502'
  10:07:25 Deepgram back -> A delivered to mailbox, removed from unsent/; status 'Saved for Firstmate' (floater-after-auto-retry.png)
Mailbox message:
  {"schema": "fm-desk-voice-transcript.v1", "id": "ace5812b", "created_at": "20261007T080726Z", "source": "desk-floater-retry", "transcript": "This is the long note the floater saved when it stopped, now delivered."}

\### Relaunch at 10:07:42: D (now idle >10s) saved as firstmate and auto-retried at 10:08:02 -> mailbox 20261007T080802Z-687f8728.json

\### Manual retry of the saved dictation B:
$ fm-desk-voice.sh retry
transcript	/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080604Z-93069617.wav	Dear team, the dictated paragraph.
$ fm-desk-voice.sh recordings
(empty)
Evidence: CLI keep/recordings/retry of a 10-minute note (Deepgram down, then empty, then back)

Source: CLI keep/recordings/retry of a 10-minute note (Deepgram down, then empty, then back)

total 37504
drwxr-xr-x@ 3 larsmusic  staff        96 Oct  7 10:00 .
drwxr-xr-x@ 3 larsmusic  staff        96 Oct  7 10:00 ..
-rw-r--r--@ 1 larsmusic  staff  19200044 Oct  7 10:00 long-note-firstmate.wav
\### 1. Deepgram down: a 10-minute talk-to-Firstmate note fails to transcribe

$ bin/fm-deepgram-stt.sh /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/recording/long-note-firstmate.wav
fm-deepgram-stt: Deepgram listen failed HTTP 502
{"err_msg":"bad gateway"}
[exit 1]
\### 2. The floater hands it to keep

$ bin/fm-desk-voice.sh keep --purpose firstmate --reason transcription failed /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/recording/long-note-firstmate.wav
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080032Z-80dd53e1.wav
[exit 0]

$ ls -la /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/recording /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/recording:
total 0
drwxr-xr-x@ 2 larsmusic  staff   64 Oct  7 10:00 .
drwx------@ 6 larsmusic  staff  192 Oct  7 10:00 ..

/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent:
total 37512
drwx------@ 4 larsmusic  staff       128 Oct  7 10:00 .
drwx------@ 6 larsmusic  staff       192 Oct  7 10:00 ..
-rw-------@ 1 larsmusic  staff       151 Oct  7 10:00 20261007T080032Z-80dd53e1.json
-rw-------@ 1 larsmusic  staff  19200044 Oct  7 10:00 20261007T080032Z-80dd53e1.wav
[exit 0]

$ bin/fm-desk-voice.sh recordings
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080032Z-80dd53e1.wav	firstmate	1	20261007T080032Z	transcription failed
[exit 0]
\### 3. Retry while Deepgram is still down: kept, attempt counted

$ bin/fm-desk-voice.sh retry
unsent	/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080032Z-80dd53e1.wav	2	transcription failed: Deepgram listen failed HTTP 502
[exit 0]
\### 4. Deepgram answers with no words: still kept

$ bin/fm-desk-voice.sh retry
unsent	/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080032Z-80dd53e1.wav	3	no speech heard
[exit 0]

$ bin/fm-desk-voice.sh recordings
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080032Z-80dd53e1.wav	firstmate	3	20261007T080032Z	no speech heard
[exit 0]
\### 5. Deepgram back: retry delivers (no primary -> mailbox), recording removed

$ bin/fm-desk-voice.sh retry
fm-desk-voice: the primary's chat pane is not reachable; saving to the mailbox instead
delivered	/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080032Z-80dd53e1.wav	mailbox: /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/inbox/20261007T080033Z-ffb63975.json
[exit 0]

$ ls -la /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent
total 0
drwx------@ 2 larsmusic  staff   64 Oct  7 10:00 .
drwx------@ 6 larsmusic  staff  192 Oct  7 10:00 ..
[exit 0]

$ bin/fm-desk-voice.sh pending
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/inbox/20261007T080033Z-ffb63975.json
[exit 0]

$ bin/fm-desk-voice.sh recordings
[exit 0]
\### upload sizes Deepgram saw (whole 10-minute file each time)
10:00:31 bytes=19200044
10:00:32 bytes=19200044
10:00:32 bytes=19200044
10:00:33 bytes=19200044
   4 --max-time 312
Evidence: CLI dictation retry and guard cases

Source: CLI dictation retry and guard cases

\### Dictation: saved on 'no speech', retry returns words, never mailed to Firstmate

$ bin/fm-desk-voice.sh keep --purpose dictate --reason no speech heard /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/recording/d-dictate.wav
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080045Z-5e3947e2.wav
[exit 0]

$ bin/fm-desk-voice.sh retry
transcript	/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080045Z-5e3947e2.wav	Dear team, the release is moving to Thursday.
[exit 0]
inbox files before=1 after=1

$ bin/fm-desk-voice.sh recordings
[exit 0]
\### Guard: retry refuses a file outside unsent/, keep refuses a non-audio file and a symlink

$ bin/fm-desk-voice.sh retry /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/outside.wav
fm-desk-voice: not a saved recording in /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent: /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/outside.wav
[exit 2]

$ ls /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/outside.wav
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/outside.wav
[exit 0]

$ bin/fm-desk-voice.sh keep /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/notes.txt
fm-desk-voice: not an audio recording: /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/notes.txt
[exit 2]

$ bin/fm-desk-voice.sh keep /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/link.wav
fm-desk-voice: no such recording: /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/link.wav
[exit 2]

$ bin/fm-desk-voice.sh retry /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/../../../outside.wav
fm-desk-voice: not a saved recording in /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent: /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/../../../outside.wav
[exit 2]
\### Guard: delivery failure (mailbox cannot be written) leaves the recording saved

$ bin/fm-desk-voice.sh keep --reason transcription failed /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/recording/f-firstmate.wav
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080046Z-e3b25e26.wav
[exit 0]

$ bin/fm-desk-voice.sh retry
fm-desk-voice: the primary's chat pane is not reachable; saving to the mailbox instead
delivered	/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080046Z-e3b25e26.wav	mailbox: /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/inbox/20261007T080046Z-c43b605c.json
[exit 0]

$ bin/fm-desk-voice.sh recordings
[exit 0]

$ bin/fm-desk-voice.sh retry
[exit 0]

$ bin/fm-desk-voice.sh recordings
[exit 0]
\### (note: the chmod 500 attempt above did not block the mailbox, since deliver resets the folder to 0700; the words were really delivered. Re-done with the mailbox replaced by a plain file.)

$ bin/fm-desk-voice.sh keep --reason transcription failed /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/recording/g-firstmate.wav
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080100Z-ac52f27e.wav
[exit 0]

$ bin/fm-desk-voice.sh retry
mkdir: /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/inbox: File exists
fm-desk-voice: cannot create desk-voice mailbox dirs
[exit 1]

$ bin/fm-desk-voice.sh recordings
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080100Z-ac52f27e.wav	firstmate	1	20261007T080100Z	transcription failed
[exit 0]
\### mailbox restored: retry now delivers and removes it

$ bin/fm-desk-voice.sh retry
fm-desk-voice: the primary's chat pane is not reachable; saving to the mailbox instead
delivered	/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/unsent/20261007T080100Z-ac52f27e.wav	mailbox: /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.FCsM02/state/desk-voice/inbox/20261007T080100Z-5490bdba.json
[exit 0]

$ bin/fm-desk-voice.sh recordings
[exit 0]
Evidence: Mailbox ring reaching a real lab Claude primary, which drains the note

Source: Mailbox ring reaching a real lab Claude primary, which drains the note

\### Lab Claude primary (pid 66602) in private tmux socket fm-lab, FM_HOME=/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.wSeCDz
\### Pane before deliver:
  ⎿  SessionStart:startup hook error
  ⎿  Failed with non-blocking status code: /bin/sh: ${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd: No such file or directory
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ Try "fix lint errors"
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ auto mode on (shift+tab to cycle) · ← 2 agents

$ fm-desk-voice.sh deliver --source desk-floater 'a really long voice note ...'
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.wSeCDz/state/desk-voice/inbox/20261007T080226Z-b93f46ca.json
[deliver returned after 0s]

\### Pane 6s later:
 ▐▛███▛█   Claude Code v2.1.292
▝▜██████▀  Opus 5.5 (1M context) with high effort · Claude Max
 ▝▝   ▝▝   ~/.no-mistakes/worktrees/f569cc43ac96/01M4ANF8MPPS52V9M6QYTEW3ED
⚠ Your login expires in 1 day · run /login to renew
  ⎿  SessionStart:startup hook error
  ⎿  Failed with non-blocking status code: /bin/sh: lavish-axi: command not found
  ⎿  SessionStart:startup hook error
  ⎿  Failed with non-blocking status code: /bin/sh: gh-axi: command not found
  ⎿  SessionStart:startup hook error
  ⎿  Failed with non-blocking status code: /bin/sh: chrome-devtools-axi: command not found
  ⎿  SessionStart:startup hook error
  ⎿  Failed with non-blocking status code: /bin/sh: ${CLAUDE_PLUGIN_ROOT}/hooks/run-hook.cmd: No such file or directory
❯ [firstmate desk-voice] a desk voice message is waiting in the mailbox. Run bin/fm-wake-drain.sh now to pick it up; it stays queued until handled and
  acknowledged.
✶ Coalescing… (5s · ↓ 188 tokens · thought for 2s)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ auto mode on (shift+tab to cycle) · ← 2 agents

\### Pane after the primary acted on the ring:
❯ [firstmate desk-voice] a desk voice message is waiting in the mailbox. Run bin/fm-wake-drain.sh now to pick it up; it stays queued until handled and
  acknowledged.
⏺ Bash(pwd; git rev-parse --abbrev-ref HEAD; echo "FM_HOME=$FM_HOME"; ls; ls state 2>/dev/null | head)
  ⎿  ~/.no-mistakes/worktrees/f569cc43ac96/01M4ANF8MPPS52V9M6QYTEW3ED
     HEAD
     FM_HOME=/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.wSeCDz
     … +16 lines (ctrl+o to expand)
  ⎿  Allowed by auto mode classifier
⏺ Bash(ls -la "$FM_HOME" 2>&1; ls -la "$FM_HOME/state" 2>&1 | head -40; ps -o pid,ppid,command -p $PPID 2>/dev/null)
  ⎿  total 8
     drwx------@    8 larsmusic  staff     256 Oct  7 10:01 .
     drwx------@ 5506 larsmusic  staff  176192 Oct  7 10:02 ..
     … +16 lines (ctrl+o to expand)
  ⎿  Allowed by auto mode classifier
⏺ Bash(cd "$FM_HOME"; cat .fm-lab-home; echo; cat state/.wake-queue; echo; cat state/.lock; echo; cat state/.watcher-down; echo; find state/desk-voice -maxdepth
      3 | he…)
  ⎿  fm-lab-home v1
     1791360146 1       check   desk-voice      desk-voice:
     /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.wSeCDz/state/desk-voice/inbox/20261007T080226Z-b93f46ca
     … +10 lines (ctrl+o to expand)
  ⎿  Shell cwd was reset to ~/.no-mistakes/worktrees/f569cc43ac96/01M4ANF8MPPS52V9M6QYTEW3ED
  ⎿  Allowed by auto mode classifier
⏺ The lock is held by this session (pid 66602), so this is my home. Draining the queue as instructed.
⏺ Bash(bin/fm-wake-drain.sh 2>&1)
  ⎿  1791360146 1       check   desk-voice      desk-voice:
     /var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.wSeCDz/state/desk-voice/inbox/20261007T080226Z-b93f46ca
     .json
     WAKE_ACK_REQUIRED: after handling completes run bin/fm-wake-drain.sh --ack-through 1 --recovery-generation 91919.1791360146.rOkQ7S
  ⎿  Allowed by auto mode classifier
⏺ Per section 9, a desk-voice wake means running the desk-voice drain.
⏺ Bash(bin/fm-desk-voice.sh drain 2>&1)
  ⎿  Please check the release plan; this is the really long voice note from the desk.
  ⎿  Allowed by auto mode classifier
· Coalescing… (23s · ↓ 1.1k tokens)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ auto mode on (shift+tab to cycle) · ← 2 agents

\### mailbox inbox / processed now:
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.wSeCDz/state/desk-voice/inbox:

/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.wSeCDz/state/desk-voice/processed:
20261007T080226Z-b93f46ca.json
Evidence: Mailbox ring waits while a draft is in the chat box, then rings once it is cleared

Source: Mailbox ring waits while a draft is in the chat box, then rings once it is cleared

\### Captain has a half-typed draft in the lab primary's chat box:
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ captain draft half typed thought
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ auto mode on (shift+tab to cycle)

$ fm-desk-voice.sh deliver (ring schedule '0 2 5 10 20 30 60 60 60')
/var/folders/1g/hctp3vpn27b1zrlsn4nsfg680000gn/T//fm-lab.wSeCDz/state/desk-voice/inbox/20261007T080309Z-ce768038.json

\### 12s later - draft untouched, nothing submitted:
  running in a temporary test home that has no data and no registered projects. If you meant your main home, the note may have landed here by mistake.
✻ Cogitated for 33s · done 10:03 AM
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ captain draft half typed thought
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ auto mode on (shift+tab to cycle)

inbox: 20261007T080309Z-ce768038.json

\### Captain clears the draft at 10:03:29:
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ auto mode on (shift+tab to cycle) · ← 2 agents                                                                               Ctrl+Y to paste deleted text


\### At 10:03:50 (cleared 10:03:29) the ring has landed on its next scheduled try; ring lines in scrollback: 2
✻ Cogitated for 33s · done 10:03 AM
❯ [firstmate desk-voice] a desk voice message is waiting in the mailbox. Run bin/fm-wake-drain.sh now to pick it up; it stays queued until handled and
  acknowledged.
✻ Harmonizing…
  ⎿  Tip: Use /memory to view and manage Claude memory
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
❯ 
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ⏵⏵ auto mode on (shift+tab to cycle) · ← 2 agents

inbox: 20261007T080309Z-ce768038.json
Evidence: Transcription request against a never-answering endpoint fails in bounded time

Source: Transcription request against a never-answering endpoint fails in bounded time

\### fm-deepgram-stt.sh with real curl routed (--connect-to) to a local socket that accepts and never answers; 2s recording (64 kB) -> per-attempt bound 120s
start 10:08:35
curl: (28) SSL connection timeout
curl: (28) SSL connection timeout
curl: (28) SSL connection timeout
fm-deepgram-stt: curl failed talking to Deepgram (no answer within 120s, or the connection failed)
[exit 1] end 10:09:27, elapsed 52s
\### connections the silent endpoint saw (one per attempt):
10:08:35 connection accepted, never answering
10:08:54 connection accepted, never answering
10:09:12 connection accepted, never answering

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

🔧 **Rebase** - 2 issues found → auto-fixed ✅
  • ⚠️ .agents/skills/operational-home-layout/SKILL.md - merge conflict rebasing onto origin/main
  • ⚠️ docs/desk-floater.md - merge conflict rebasing onto origin/main

🔧 Fix applied.
✅ Re-checked - no issues remain.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ bin/fm-desk-voice.sh:328 - ring_mailbox calls PRIMARY_SUBMIT_COMPOSER=empty primary_submit &#34;$line&#34; without PRIMARY_SUBMIT_RING=1, so the delayed mailbox ring goes through the ordinary send path: fm_backend_send_text_submit, then fm_tmux_submit_core, which types and then retries Enter up to 3 times. It does not use submit_ring. That leaves out the two protections the header and docs say a ring has. (1) Before every ring Enter, including retries, the box must still show only the ring's literal payload. (2) A ring leaves a Claude stash alone; the send path types over a stash with tries=1. The bad case is likely because this ring is unsolicited and runs detached for about 4 minutes (delays 0 2 5 10 20 30 60 60 60). Sequence: a mailbox message is queued and the captain starts typing a reply in the primary chat just as a ring fires. The composer reads empty, the ring line is typed, the captain's keystrokes land in the same box, and Enter submits '[firstmate desk-voice] … <half-typed thought>' as one message. A retried Enter can also submit a draft that reappeared. docs/desk-floater.md:226 says the ring works 'the way a captain inbox note does' and is 'never over … your draft'. The inbox-note ring (bin/fm-inbox.sh:389) goes through fm-desk-voice.sh ring, which has these proofs, and test_ring_checks_the_payload_before_each_enter covers only that path. Fix: have ring_mailbox use the existing ring function (or set PRIMARY_SUBMIT_RING=1 with the same verdict handling: rung/rung-unconfirmed stop, not-rung retries, rc 1 stops). This also removes the second copy of the ring rule. Add a race case for the mailbox ring.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 7 of 8 scenarios driven live against the product
Scenario Result Live Evidence
A 10-minute talk-to-Firstmate note whose transcription fails is saved privately, not lost, and is delivered whole once Deepgram works again ✅ pass live cli-keep-retry-transcript.txt: keep moved the 19,200,044-byte WAV to unsent/ (folder 0700, files 0600). Retries reported 'unsent … 2 … HTTP 502', then 'unsent … 3 no speech heard', then 'delivered … m…
A saved dictation is never sent to Firstmate; retry gives back its words and then removes it ✅ pass live cli-dictation-and-guards-transcript.txt: retry printed 'transcript <path> Dear team, …'; the mailbox count stayed the same; recordings was empty afterwards. live-floater-recovery-transcript.txt: the…
Adversarial: retry/keep touch only saved audio recordings, and a failed delivery leaves the recording saved ✅ pass live cli-dictation-and-guards-transcript.txt: retry of an outside file and of a unsent/../../../ path were refused (exit 2, file left alone). keep of a .txt and of a symlink were refused. With the mailbox…
The floater, restarted after stopping mid-recording, saves the leftover capture (even one with an unfinished WAV header), drops a stray press, leaves an in-progress capture alone, and keeps retrying a… ✅ pass live live-floater-recovery-transcript.txt plus floater-saved-retrying.png and floater-after-auto-retry.png. The 30s capture with a data length of 0 in its header was saved as firstmate. The 0.2s stray was…
A note saved to the mailbox rings a busy or idle real Firstmate primary within seconds, and the primary collects it ✅ pass live live-mailbox-ring-claude-primary.txt: deliver returned at once; within 6s the lab Claude primary's chat showed '[firstmate desk-voice] a desk voice message is waiting in the mailbox…' submitted. The p…
Adversarial: the mailbox ring never types over or submits the captain's half-typed draft, and rings once the box is free ✅ pass live live-mailbox-ring-draft-guard.txt: with 'captain draft half typed thought' in the chat box, nothing was typed or submitted for 12s. After Ctrl-U cleared it, the ring landed on its next try about 21s l…
A transcription request to a Deepgram that never answers gives up in bounded time instead of hanging ✅ pass live live-stt-never-answers.txt: real curl, routed to a local socket that never answers, made 3 attempts (15s connect bound each, 3s apart), then fm-deepgram-stt exited 1 with 'curl failed talking to Deepg…
While the floater is running, a fresh microphone recording whose transcription fails (or hears no words in a recording of 2 seconds or more) is saved and shown as 'Saved - retrying' instead of being d… ⏸️ untested no This needs a real microphone capture started from the floater's talk button or hotkey. The build-only DeskFloater app has no Microphone permission, and pressing its button needs Accessibility or compu…
  • bash tests/fm-deepgram-desk.test.sh (focused desk-voice suite, includes the Swift RecordingTests): 84 ok, exit 0
  • Live CLI in a disposable lab home with a stand-in Deepgram (FM_DEEPGRAM_CURL): fm-desk-voice.sh keep of a 19,200,044-byte 10-minute WAV, then recordings, then retry three times (Deepgram down, empty transcript, Deepgram back) ending in a mailbox delivery
  • Live CLI: dictation keep --purpose dictate then retry printed a transcript line, sent nothing to the mailbox, and removed the recording
  • Live CLI guards: retry of a file outside unsent/ (including a ../ path) refused with exit 2; keep of a non-audio file and of a symlink refused; with the mailbox path replaced by a plain file, the retry failed and the recording stayed saved
  • Live mailbox ring: a real claude primary in a lab home on the private tmux -L fm-lab socket; fm-desk-voice.sh deliver typed the ring line within seconds and the primary ran fm-wake-drain.sh and fm-desk-voice.sh drain
  • Live adversarial ring: a draft typed with tmux send-keys -l was left alone, nothing was submitted for 12s, then after Ctrl-U cleared it the ring landed on its next try about 21s later
  • Live floater: built with bin/fm-desk-floater.sh --build-only and ran the DeskFloater binary with FM_HOME=lab. Launch recovery: the cut-off WAV and the dictation were saved, the 0.2s stray was deleted, and the in-progress capture was left alone. Automatic retries at +20s (Deepgram down) and +60s (delivered). Relaunch recovered the capture left alone earlier
  • screencapture -l &lt;floater window&gt; of the floater status line: 'Saved - retrying', then 'Saved for Firstmate'
  • Live bin/fm-deepgram-stt.sh with real curl routed through --connect-to to a local socket that accepts and never answers: three bounded attempts, then exit 1 with the bound named, after 52s
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…r mailbox notes

Keep each recording until its words are delivered: a recording whose
transcription fails, comes back empty though long enough to hold words,
or cannot be delivered is saved in the home's private
state/desk-voice/unsent/ and retried automatically, with
fm-desk-voice.sh keep/recordings/retry to save, list and transcribe it
again later. Captures are recorded under the home so a floater that stops
mid-message saves the recording at its next launch.

Bound the Deepgram request with a size-scaled timeout and two retries
instead of letting it hang, with no length cap; a ten-minute recording
transcribes whole.

A message saved to the mailbox now rings the busy primary, retrying
until its chat can take the line, so it is collected within seconds
rather than at the end of the current task, and the floater says
"Saved for Firstmate" for it.
…ull desk voice test suite passes locally. The other two failing shards hit tests in code this PR doesn't touch, so I made no change for them. **Shard 4: fm-deepgram-desk, "the unsent folder must be private" (caused by this PR, fixed).** - Invariant: the test helper `mode_of` must return a file's octal mode on both macOS (BSD `stat`) and Linux (GNU `stat`). - Cause: the PR's helper ran the BSD form `stat -f '%Lp'` first. On Linux, GNU `stat -f` reports on the filesystem instead and still exits 0, so the fallback never ran. The helper returned something that wasn't a mode, and the check for 700 failed. The checks for 600 on the saved recording and its JSON record use the same helper. - Fix: one line at `tests/fm-deepgram-desk.test.sh:1109`, now `stat -c '%a' "$1" 2>/dev/null || stat -f '%Lp' "$1"`. This is the order `tests/fm-captain-ledger.test.sh` already uses. On macOS `stat -c` fails and the BSD form runs; on Linux the GNU form works. - The other mode check in the change already picks the `stat` form by `uname`, so it was fine. **Verification:** - A full local run of `bash tests/fm-deepgram-desk.test.sh` exited 0 with 82 passing checks and no failures. These include "fm-desk-voice keep/retry: a recording is kept, private, until its words are delivered" and "desk floater: Swift tests pass". - GNU `stat` isn't on this Mac, so I couldn't run the Linux path. Only the CI re-run will confirm it. - A background watcher I started to wait on an earlier run was stopped at its time limit. It was looking for a line the suite never prints. The stop says nothing about the tests, and the full run above is the result that counts. **Shard 11: fm-watcher-lock, "dead link-lock owner did not publish its pid" (not caused by this PR, no change).** - The test gives a background lock holder about one second (50 × 0.02s) to write its pid. The PR changes no lock code, nothing in `lib/`, and not this test. This looks like a timing flake on a busy CI runner. **Shard 2: fm-supervision-host, "the host-only transition veto changed Pi's existing offer rule" (not caused by this PR, no change).** - The test calls `branchOfferForWake` in `.pi/extensions/lib/fm-branch-dispatch.ts` through node. The PR changes no Pi or supervision code, and I couldn't tie the failure to this change. If it fails again on the re-run, it needs its own look
@Amplify-Logic
Amplify-Logic force-pushed the fm/desk-voice-never-lose-g1 branch from b9e214b to 208f8c4 Compare October 7, 2026 08:11
@Amplify-Logic Amplify-Logic changed the title fix(desk-floater): never lose a voice recording and ring Firstmate for mailbox messages fix(desk-floater): keep voice recordings until their words are delivered Oct 7, 2026
@Amplify-Logic
Amplify-Logic merged commit fc0eda7 into main Oct 7, 2026
24 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.

1 participant