@gotcos/glasses-server 6.92.0 → 6.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.env.example +30 -1
  2. package/CHANGELOG.md +57 -29
  3. package/README.md +58 -0
  4. package/docs/active-query-jobs.md +1 -1
  5. package/docs/test-plan-6.71.7.md +1 -1
  6. package/managed-runtime-contract.json +2 -0
  7. package/package.json +2 -1
  8. package/server/lib/ask-session-id.ts +2 -1
  9. package/server/lib/embedding-eviction.ts +60 -2
  10. package/server/lib/fuzzy-correct.ts +1 -1
  11. package/server/lib/hallucination-filter.ts +1 -1
  12. package/server/lib/imported-library-rows.ts +4 -2
  13. package/server/lib/live-cos-ask.ts +3 -2
  14. package/server/lib/mac-live-voices.ts +57 -0
  15. package/server/lib/mac-recording-asr.ts +16 -8
  16. package/server/lib/mac-speaker-projection.ts +211 -0
  17. package/server/lib/mac-stream-diarizer.ts +185 -0
  18. package/server/lib/mac-voice-assignment.ts +361 -0
  19. package/server/lib/mac-voice-labels.ts +70 -8
  20. package/server/lib/mac-window-replay.ts +271 -0
  21. package/server/lib/mac-window-voices.ts +390 -0
  22. package/server/lib/meeting-actions.ts +1 -1
  23. package/server/lib/meeting-ask-owner.ts +4 -4
  24. package/server/lib/meeting-ask-transcript-sources.ts +41 -3
  25. package/server/lib/meeting-audio-archive.ts +4 -0
  26. package/server/lib/meeting-batch-transcribe.ts +7 -1
  27. package/server/lib/meeting-context-keys.ts +6 -3
  28. package/server/lib/meeting-corrections.ts +4 -4
  29. package/server/lib/meeting-fact-check.ts +3 -3
  30. package/server/lib/meeting-parse.ts +1 -1
  31. package/server/lib/meeting-relabel-enrolment.ts +8 -8
  32. package/server/lib/meeting-relabel.ts +3 -3
  33. package/server/lib/meeting-scribe-content.ts +1 -1
  34. package/server/lib/meeting-speaker-review.ts +8 -8
  35. package/server/lib/meeting-store.ts +19 -9
  36. package/server/lib/nemotron-final.ts +114 -9
  37. package/server/lib/nemotron-live.ts +58 -6
  38. package/server/lib/phone-recording.ts +114 -0
  39. package/server/lib/prompt-tail-guard.ts +1 -1
  40. package/server/lib/speaker-embeddings.ts +154 -28
  41. package/server/lib/speaker-name.ts +1 -1
  42. package/server/lib/stranded-session-actions.ts +7 -2
  43. package/server/lib/training-audio-provenance.ts +3 -2
  44. package/server/lib/transcript-cleanup-context.ts +3 -3
  45. package/server/lib/transcript-cleanup-model.ts +1 -1
  46. package/server/lib/transcript-cleanup-spans.ts +10 -10
  47. package/server/lib/transcript-cleanup-text.ts +4 -4
  48. package/server/lib/transcript-cleanup.ts +1 -1
  49. package/server/lib/unsaved-audio-quarantine.ts +10 -1
  50. package/server/lib/voice-directory.ts +1 -1
  51. package/server/lib/voice-evidence-guard.ts +2 -1
  52. package/server/lib/voice-profile-store.ts +11 -5
  53. package/server/lib/whisper-local.ts +1 -1
  54. package/server/routes/health.ts +8 -0
  55. package/server/routes/meeting.ts +189 -19
  56. package/server/routes/meetings.ts +9 -5
  57. package/server/routes/transcribe-stream.ts +140 -15
  58. package/server/routes/voice.ts +12 -4
  59. package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +225 -0
  60. package/server/scripts/mac-window-voices-6930-mutation-gate.mjs +193 -0
package/.env.example CHANGED
@@ -64,9 +64,38 @@ BIND_HOST=0.0.0.0
64
64
  # pass over the whole meeting labels its voices Voice 1, Voice 2, ... (unnamed,
65
65
  # stable for the meeting). No voice profile is read or written, and renaming a
66
66
  # Voice N is text only. Crowded or unclear rooms stay Ext. On by default; 0 turns
67
- # it off and Mac recordings stay all Ext.
67
+ # it off (and both live modes below) and Mac recordings stay all Ext.
68
68
  # COS_MAC_VOICE_LABELS=0
69
69
 
70
+ # 6.92.1 / 6.93.0 live Mac voices, ON by default as the window replay. COS_MAC_LIVE_VOICES=off
71
+ # (also 0, false, no, none, disable, disabled, never) makes every live Mac chunk read Ext, exactly
72
+ # as 6.92.0 (the after-save pass above still numbers the voices). Any other value that is not
73
+ # window or anchors still means window and logs mac_live_voices_unrecognised once. One switch, three values:
74
+ # off the opt-out: 6.92.0.
75
+ # anchors opt-in, the 6.92.1 per-chunk path: a chunk reads Voice N after two clean, agreeing
76
+ # observations. On natural audio it rarely fires.
77
+ # window the default (6.93.0): each live chunk is diarized inside the last five durable
78
+ # chunks (30 s), so a voice keeps its number across 6 s boundaries. A carried
79
+ # voice is used only when the chunk diarized alone agrees (one extra short run).
80
+ # That catches a merge of two voices speaking in the same chunk; it does not yet
81
+ # catch a clean hand-over to a new voice at a chunk boundary: that speaker can
82
+ # read the previous number. The saved meeting keeps live numbers, so it keeps that
83
+ # number too; assigning such a Voice N to a profile is refused (409) unless forced.
84
+ # Window runs get a timeout sized from their length and fall back to the
85
+ # per-chunk run on a timeout; 2 of the last 3 window timeouts stop the window for that capture. Unlabeled speech stays Speaker.
86
+ # No names, no enrollment, no training on their own (only an explicit assignment in COS Control
87
+ # adds samples, and it is gated). COS_MAC_VOICE_LABELS=0 turns both off. Restart the server
88
+ # after changing it; GET /api/health shows capabilities.macLiveVoices.mode.
89
+ # COS_MAC_LIVE_VOICES=off
90
+
91
+ # 6.93.0 iPhone microphone meetings, OFF by default. 1 lets the server accept a meeting
92
+ # recorded from the iPhone microphone (COS Glasses 6.10.655 and COS Control 0.5.351 or
93
+ # newer). It also needs the HQ engine set to Parakeet Ultra with Parakeet installed;
94
+ # GET /api/health then reports capabilities.phoneMicrophoneMeetings.enabled true.
95
+ # Phone meetings are anonymous (every line is Ext) and never train a voice profile.
96
+ # Not iOS system audio: it cannot record Zoom, YouTube or calls playing on the phone.
97
+ # COS_IPHONE_MICROPHONE_MEETINGS=1
98
+
70
99
  # 6.91.0 dictation polish. Prompt v4 is the default on Sonnet (Haiku and Luna keep
71
100
  # v3): self-corrections first ("two, actually three" keeps three), restarts, grammar
72
101
  # in your own words, no added em dashes. 3 (or COS_DICTATION_POLISH_PROMPT=v3) puts
package/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## 6.93.0 - Live Mac speaker numbering on by default, assign a Mac voice to an existing voice profile, and iPhone microphone meetings (opt-in), plus the unpublished 6.92.1
2
+
3
+ 2026-10-26. Pairs with COS Control 0.5.351. Includes everything in 6.92.1 (never published on its own): the per-chunk anonymous anchors (`COS_MAC_LIVE_VOICES=anchors`), final-pass crosswalk of live numbers, the body-parser floor, and the isolated continuous-speaker protocol modules (still unwired).
4
+
5
+ - **Switches in this release:** `COS_MAC_LIVE_VOICES` is **on by default** (Miles 2026-10-26): unset runs the window replay below. `COS_MAC_LIVE_VOICES=off` (also `0`, `false`, `no`, `none`, `disable`, `disabled`, `never`) is the opt-out and gives exactly 6.92.0 for a Mac recording: every live chunk is `Ext` and the after-save pass numbers every confident voice. `COS_MAC_LIVE_VOICES=anchors` keeps the 6.92.1 per-chunk anchors. Any other value (including an empty one) is the default, so a typo never silently turns it off; an unrecognised non-empty value logs `mac_live_voices_unrecognised` once so it is not silent either. The environment is read when the server starts, so a change needs a restart. `COS_MAC_VOICE_LABELS` keeps its 6.90.0 meaning (the after-save pass, on by default; `0` turns off everything `Voice N`, live and saved). `COS_IPHONE_MICROPHONE_MEETINGS` stays off by default.
6
+ - **Publish order:** ship COS Control 0.5.351 on the appcast before (or together with) this server on npm `latest`. With the live window on by default, a Mac recording now sends `Voice N` speaker labels to every Control. 0.5.349 and 0.5.350 predate the anonymous Voice chip: their live meeting strip shows each `Voice N` as a named participant with the initial V. 0.5.351 draws it as an unnamed speaker numbered N. The server cannot tell which Control is asking (Mac chunk uploads carry no client version), so it does not gate on it; an installed 0.5.349 or 0.5.350 can set `COS_MAC_LIVE_VOICES=off` until it updates. The assignment bullet below also needs 0.5.351: older Controls never send `assignVoiceProfile`, so nothing changes for them.
7
+ - **Known limit, now reaching everyone (R1):** a clean hand-over between two people exactly at a chunk boundary, where a new voice speaks alone in the next chunk, can give the new speaker the previous speaker's number until the overlap ages out. The chunk alone shows one clean voice, so the agreement check cannot see it. The save-time pass keeps every live number (it does not renumber the meeting), so the merge stays in the saved meeting. A `Voice N` is never a person and never trains a profile on its own. It can become a wrong name only through an explicit assignment, which the next bullet gates: a group under the 0.55 floor, a mixed group, or too little audio is refused unless the person forces it. The only way to avoid the wrong number itself is `COS_MAC_LIVE_VOICES=off`.
8
+ - **Assign a Mac voice to an existing voice profile (new):** `POST /api/meeting/:sessionId/relabel` accepts `assignVoiceProfile: true` for a Mac recording. `from` must be an unnamed `Voice N` and `to` must be an already enrolled profile name (exact match; a placeholder or unknown name is `404 unknown_profile`, and this path never creates a profile). **The wearer's own profile is never a target: `409 owner_profile`, not overridable.** A Mac recording banks no per-chunk embeddings, so the server extracts them now from the chunk WAVs in the 7-day meeting-audio archive: whole chunks where this voice is the only one, or this voice's turns of at least 3 s (trimmed 0.15 s at each end) where another voice also speaks, using the saved pass's turns when present. At most 40 audio units are embedded, spread across the meeting, then the dominant mutually coherent group is kept (floor 0.55) and at most 10 diverse samples are chosen. A dry run (`dryRun: true`, or no `confirm`) writes nothing and returns an `enrolment` block: the person, the most samples that would be added, `attempted`, `clusterSkipped` (units thrown out as a different voice), `groupSize` and `groupSec`, the cosine of this voice's group against the person's stored centroid with `belowFloor`, and `gates`. **Three gates, each `409` on a confirm unless the request carries `force: true`:** `below_floor` (the group is under 0.55 of the stored profile, the same floor `merge-profiles` refuses at), `mixed_voice` (units were thrown out and the kept group is under two thirds of what was embedded, so the label holds more than one voice), `too_little_audio` (one coherent unit under 6 s). COS Control offers `force` only as a separate "Add anyway" after showing the counts. A confirm that would add nothing (`audio_not_retained`, `no_usable_audio`, `profile_full`, no speaker model) is `422 nothing_to_enrol` and changes nothing, rename included. Samples are tagged `mac-assign:<sessionId>`, not `correction:`: they rank as assisted, are not protected, never vouch for identity in held-voice suggestions, are capped at 10 per profile and never above the profile's other samples (a profile with 2 other samples holds at most 2), and may only ever replace another Mac-assigned sample (a full profile with none to replace refuses them), so they cannot evict a G2 correction, a `g2-training` or a `fireflies` sample. They are also the weakest evidence a profile holds: when any other kind of sample needs room they go first, before an `auto:`, unknown or `fireflies` one. `merge-profiles` holds the merged profile to the same cap and drops surplus Mac-assigned samples oldest first. `GET /api/voice/profiles` lists them as one `mac-assign` bucket, not one key per meeting. "Not in this meeting" still retracts them by session. **It does not stall the server:** the plan yields to the event loop between units, stops at a 12 s deadline (`COS_MAC_ASSIGN_DEADLINE_MS`, 1 to 25 s; the vectors already made are kept, so asking again continues), reuses the preview's vectors on the apply, and writes the profile store once instead of once per sample. It is refused with `409 capture_active` while any meeting capture (glasses, Mac or iPhone) is live, `409 assign_in_progress` for a second request on the same meeting, and `409 meeting_changed` if the meeting's bytes or `expectedRevision` moved while the audio was read. Without the flag a Mac rename is still text only. `server/scripts/mac-voice-assignment-6930-mutation-gate.mjs` is the mutation gate for this path. Limit: the rename is durable before the samples are written, and the Voice N is gone after it, so a store that cannot be written reports `enrolled: 0` and `skipped: store_unavailable` and the rename stays; to retry, rename that person back to `Voice N` and assign again (the store's 0.95 near-duplicate gate skips what already landed).
9
+ - **Final polish (speaker profiles):** `POST /api/voice/merge-profiles` now refuses, with code `merge_would_empty_profile` (409, nothing changed, same in a preview), a merge that would leave the profile with no samples, for example two profiles that hold only Mac-assigned samples. The relabel preview no longer names an API field when assigning is not possible: it says the speaker can still be renamed in the transcript without adding voice samples. A correction arriving at its quota still replaces the oldest correction before any Mac-assigned sample is touched (now pinned). Test fixtures use fictional names only.
10
+ - **iPhone microphone meetings (new, off by default):** with `COS_IPHONE_MICROPHONE_MEETINGS=1`, the HQ engine on Parakeet Ultra and Parakeet installed, `GET /api/health` and `/api/models` report `capabilities.phoneMicrophoneMeetings` (`enabled`, `protocolVersion` 1, `sourceType` `iphone_microphone`, `speakerPolicy` `anonymous`, `asrModel` `parakeet-ultra`, `dictionary`, `reason`). COS Glasses 6.10.655 then records from the iPhone microphone through the same indexed `POST /api/transcribe-stream` upload as a glasses meeting, tagged `sourceType=iphone_microphone&speakerPolicy=anonymous`. IDs are `phone_<epoch ms>_<uuid>`. Admission writes a durable registry before audio or ASR; every accepted index gets a hash receipt, an exact retry is idempotent and conflicting bytes are 409. Live and saved transcription are strict Parakeet Ultra with the existing profile dictionary corrections: no Whisper, cloud or preview fallback, and a decode failure is a retryable `503 phone_parakeet_<reason>` that keeps the audio. Every line is `Ext`: no wearer label, no voice matching, no embeddings, no enrollment, no training. Phone meetings skip the glasses merge engine and the operations hand-off, key Ask and attached files on `phone:<captureId>`, and export as `iPhone Microphone`. Remote Stop from Control is refused with `phone_capture_stop_on_iphone`; the phone stops itself. Repeated or non-canonical query fields (`sessionId`, `chunkIndex`, `elapsed`, `startTime`) are refused with 400 before admission. Microphone only: it does not record iOS system audio, Zoom, YouTube or calls. Switch: `COS_IPHONE_MICROPHONE_MEETINGS`, default off, forwarded by the managed runtime contract. Switch it on only after COS Control 0.5.351 is the installed Control, because 0.5.350 does not understand phone captures. See `docs/iphone-microphone-protocol.md`.
11
+ - **Why:** live Mac chunks were diarized one 6 s file at a time, so the model forgot every voice at each boundary. On a natural 36 s capture the 6.92.1 anchors produced no live labels at all (6 of 6 chunks `Ext`).
12
+ - **What:** by default (`COS_MAC_LIVE_VOICES` unset or `window`, and `COS_MAC_VOICE_LABELS` not off), each Mac chunk is diarized inside a window of the durable chunks before it (at most 5 chunks, 30 s), with the same pinned CLI. Window slots are carried to capture tracks only through frames this capture already attributed: one to one, at least 1 s of exclusive overlap, at least 80% agreement, **and only if the chunk diarized alone agrees** (it places at least half of the slot's exclusive frames, at least 90% of those on one channel; one extra short run, only when a slot is carried). This catches a window model that merged two voices which BOTH speak in the current chunk: that slot is not carried and its frames are stored untracked, so the error does not feed the next window. It does **not** catch a clean hand-over at a chunk boundary (voice A through chunk k-1, a new voice C alone in chunk k, the window model putting C on A's old slot): the chunk alone shows one clean voice and agrees, so C can read A's number until the overlap ages out. That case is not yet caught; it needs evidence from more than one chunk (an embedding against the track's proof audio). This is now the default (see the known limit above). A track reads `Voice N` after clean proof in two distinct chunks with distinct audio (2 s alone, purity 0.9). Numbers only grow. A track no remembered frame refers to is evicted and its id reused, so a long meeting cannot spend the 12 ids. Corrupt state, or Voice labels with no window state, withhold labels instead of restarting at Voice 1, and say so once in the log.
13
+ - **Timeouts:** the window run's timeout is sized from its length (1.5 s per 6 s of audio: 7.5 s for 30 s), not the per-chunk 1.5 s. Two of the last three window timeouts stop the window for the rest of that capture (per-chunk runs only, logged once, counted in `capabilities.macLiveVoices.window.disabledCaptures`), so a slow Mac pays the long wait twice, not on every chunk (a rolling count, so alternating timeouts also trip it). A window timeout never counts toward the breaker shared with G2 and every other capture; it, and a refused window geometry (reported as its own reason, not `bad_output`), fall back to the plain per-chunk run for that chunk.
14
+ - **Limit:** a voice silent for longer than the window cannot be carried. It reads `Ext` live, or a new number after its own two proofs; the carry rules do not hand it another voice's number (but see the clean hand-over limit above). Settled chunks are not relabeled live. The save-time pass keeps every live number and, for a confident final channel the live path never proved and no live turn contradicts, now numbers it above the live numbers (it used to leave it `Ext`, which could leave a saved meeting with fewer numbered voices than 6.92.0). Words that already carry a live number are never changed by it. With no live `Voice N` at all (`COS_MAC_LIVE_VOICES=off`) the pass is exactly the 6.92.0 pass.
15
+ - **Unchanged:** strict named identity (0.88 floor), G2 and phone paths. A Mac rename is text only unless the request carries `assignVoiceProfile: true` (above); nothing else enrolls or trains from a Mac recording. No audio, embedding or probability matrix is stored: only per-frame track ids for the last 4 chunks in the private active-session file.
16
+ - **Cost (measured on the pinned CLI, warm, before the agreement run was added):** about 0.26 s wall (max 0.30 s) and 0.1 s CPU per 6 s chunk for a 30 s window, about 46 MB peak RSS for the CLI process. The agreement run is one ordinary per-chunk run, sequential, only when a slot is carried: measured on the same fixtures, median 0.15 s and max 0.16 s wall over 19 runs (the window runs: median 0.26 s, max 0.29 s over 22), so about 0.4 s of CLI time per chunk with a carried voice, and it holds a second live-lane slot while it runs. If that run is refused as busy, carried voices are withheld for that chunk (they show Speaker).
17
+ - **Switch:** `COS_MAC_LIVE_VOICES`, default `window`; `off` is the opt-out, `anchors` is the 6.92.1 per-chunk path (this path replaces the anchors). Forwarded by the managed runtime contract. `GET /api/health` reports `capabilities.macLiveVoices.mode` and, for `window`, counts only. One counts-only log line per window run.
18
+ - **Qualification (private fixtures, counts only):** natural 6 chunk capture: 2 voices, 75% of speech labeled live, 100% agreement with one continuous pass on labeled frames. Synthetic A B A B (known voices): 0% wrong-voice frames, 2 of 2 returns kept, 2 of 3 true speaker switches detected live, 24.9% of speech unlabeled live (first chunk of each voice). Synthetic return after 31.5 s of silence: 0% wrong-voice, the returning voice stays unlabeled (48.2% of that fixture's speech unlabeled live). These were measured before the agreement run was added; the synthetic voices are far apart (cross-voice cosine 0.10 to 0.17), so they are a floor. No independently annotated natural meeting exists yet, so natural accuracy is not established. `server/scripts/mac-window-voices-6930-mutation-gate.mjs` kills every listed mutant from a green baseline.
19
+
20
+ ## 6.92.1 - Conservative anonymous voice continuity for live Mac meetings
21
+
22
+ - Successful live Nemotron tracks can carry capture-local Voice N labels after two independent clean audio observations agree. (In 6.93.0 these are `COS_MAC_LIVE_VOICES=anchors`; the default is the window replay; 6.92.1 was never published.) Uncertain, overlapping or short evidence remains Ext. The existing named-identity threshold, ASR choices and feature settings are unchanged.
23
+ - Private continuity anchors survive capture recovery, reject duplicate audio as independent support, and never enter the global profile store or public DTOs.
24
+ - Finalization preserves established live voice numbers and withholds channel mappings that contradict mixed live turns. Mac meeting-only renames still do not train profiles.
25
+ - Synthetic real-model qualification distinguishes two known voices at unchanged thresholds; six short natural archived chunks remained unidentified. This is conservative continuity, not a general speaker-accuracy guarantee.
26
+ - The published runtime requires body-parser 2.4.0 or newer, including fresh npm consumer installs, to fix compressed-request trailing-byte denial of service (GHSA-27r3-jxmh-p828). Express remains on its compatible 5.x API.
27
+ - Companion and old Control DTOs remain compatible. Control 0.5.350 displays distinct anonymous participant badges. The iPhone microphone capability (6.93.0) is advertised only with `COS_IPHONE_MICROPHONE_MEETINGS=1`.
28
+
1
29
  ## 6.92.0 - Dictation parity, Wave A (server): breakers that tell an abort from a failure, bounded prompt edit, a filler pass that leaves other languages and paths alone
2
30
 
3
31
  2026-10-25, plan r3 ("dictation parity", Wave A; pairs with COS Control 0.5.345). Zero `claude -p` calls: every
@@ -750,7 +778,7 @@ dictation is honored by the AI polish, and a mapping like that works from then o
750
778
  never a line break or a spaced dash,
751
779
  case-insensitive, with punctuation around it ("pricing,"). A key goes ahead of any earlier key whose words it
752
780
  contains, so "forward slash build and price" is not cut short by "forward slash build"; chains a profile relies on
753
- ("Myles", then "Miles Ukoma") keep their order. A written form is inserted literally ("$&" is not a pattern).
781
+ ("Myles", then "Miles Ukmya") keep their order. A written form is inserted literally ("$&" is not a pattern).
754
782
  Same pass on every path that already ran it: dictation finalize, prompt drafts, G2 live lines, meeting batch
755
783
  transcripts, previews. It runs before the AI polish.
756
784
  - **Spoken paths, deterministic.** For messages and prompt drafts (never meeting lines), "forward slash" plus one word
@@ -1153,10 +1181,10 @@ Pairs with COS Glasses companion 6.10.645. Includes 6.77.0. No COS Control chang
1153
1181
 
1154
1182
  Includes 6.76.2. Off until Miles turns it on. No COS Control or companion change is required; Control can add the toggle, the screen feed and the Dictionary suggestions later against the contracts below.
1155
1183
 
1156
- - Miles, 2026-10-17: after a transcription (Parakeet Ultra or Whisper), run Haiku or Sonnet over the passages where the engine was unsure, with screen and textual awareness, so word hallucinations and misheard names ("Grace Ukoma", "Yukoma", "eucoma", "U coma" for Grace Ukaoma) are resolved.
1184
+ - Miles, 2026-10-17: after a transcription (Parakeet Ultra or Whisper), run Haiku or Sonnet over the passages where the engine was unsure, with screen and textual awareness, so word hallucinations and misheard names ("Faith Ukmya", "Yukmya", "eucmya", "U cmya" for Faith Ukamya) are resolved.
1157
1185
  - Where it runs. Once per saved meeting, in the durable finalization job after the HQ batch text and the Nemotron relabel are written, and before the meeting is handed to COS operations or summarised, so both read the cleaned text. Orphan recovery runs it too. Never during live transcription: it is a separate module no transcription file imports (pinned by a test), and a meeting whose session is still live is skipped. It never fails a save.
1158
- - Selection, no model (`transcript-cleanup-spans.ts`): low confidence (Parakeet below 0.50, Whisper below 0.40, from the batch words in the sidecar, aligned to the saved transcript); a word near a known name when the engine was unsure of it (below 0.9: one edit past fuzzy-correct's limit for a capitalised word, or the same consonant skeleton and onset, so eucoma reads like Ukaoma); two or three short pieces that join into a name ("U coma"), never function words; the leading-y rule at any confidence ("Yukoma"); an out-of-vocabulary capital mid-sentence beside a known name; a phrase repeated three times or more. Spans stay inside one speaker turn and one clause, hold at most 6 words (12 for a repeat), carry 12 words of context each side, and are cut to 40 per meeting with names first.
1159
- - Deterministic fixes first, free (fuzzy-correct's own rule, exported as `fuzzyReplacementFor`): only capitalised words that are not already a known spelling; this meeting's own screen and file terms as a temporary per-meeting vocabulary at any confidence; the curated dictionary (vocabulary and corrections) and the owner only where the engine was unsure; never an inflection ("Setting" is never a "Settings" button). Measured read-only on 68 real meetings (38.5 hours): rewriting lowercase words, or using every enrolled name as a target, swapped everyday words and real names for other names (giant to grant, Kelsey to Kelley, Slack to Black), so neither is done; with these rules the same meetings got 27 fixes, all real (Manuj to Manoj, MarketPOS to MarktPOS).
1186
+ - Selection, no model (`transcript-cleanup-spans.ts`): low confidence (Parakeet below 0.50, Whisper below 0.40, from the batch words in the sidecar, aligned to the saved transcript); a word near a known name when the engine was unsure of it (below 0.9: one edit past fuzzy-correct's limit for a capitalised word, or the same consonant skeleton and onset, so eucmya reads like Ukamya); two or three short pieces that join into a name ("U cmya"), never function words; the leading-y rule at any confidence ("Yukmya"); an out-of-vocabulary capital mid-sentence beside a known name; a phrase repeated three times or more. Spans stay inside one speaker turn and one clause, hold at most 6 words (12 for a repeat), carry 12 words of context each side, and are cut to 40 per meeting with names first.
1187
+ - Deterministic fixes first, free (fuzzy-correct's own rule, exported as `fuzzyReplacementFor`): only capitalised words that are not already a known spelling; this meeting's own screen and file terms as a temporary per-meeting vocabulary at any confidence; the curated dictionary (vocabulary and corrections) and the owner only where the engine was unsure; never an inflection ("Setting" is never a "Settings" button). Measured read-only on 68 real meetings (38.5 hours): rewriting lowercase words, or using every enrolled name as a target, swapped everyday words and real names for other names (giant to grant, Kelsey to Kelley, Slack to Black), so neither is done; with these rules the same meetings got 27 fixes, all real (Mohun to Mohan, MarketPOS to MarktPOS).
1160
1188
  - Context pack: meeting title, date and attendees; COS people (enrolled speaker names); the owner card (6.76.0); the profile vocabulary and heard-to-written corrections (the COS dictionary dictation uses); text of files dropped on the meeting (the Meeting Ask file list); up to 6 local notes hits for the names the spans are near (memories and earlier meetings by keyword, and the memory bridge listing when memory search finds nothing, as the Live COS Ask does; never this meeting; nothing leaves the Mac; 3 s budget); and up to 3 screen lines per span, nearest in time (45 s) or naming the span's name. Every text is bounded and passed through the secret redactor.
1161
1189
  - The call: one per meeting, batched, through the shared Claude CLI runner (`--tools ""`, no MCP servers, no hooks, empty scratch workspace), pinned `claude-haiku-5-5` by default or `claude-sonnet-5-5`, low effort, 1,500 output tokens, 75 s timeout, 24,000-character prompt, token audit source `transcript-cleanup`. Never an SDK. The reply is strict JSON by span id (`keep` or `replace`); a fenced reply or one followed by prose still parses.
1162
1190
  - Guards on every edit (`transcript-cleanup-model.ts`): unknown or duplicate span id, bad action, no-op, a bracket or line break (a forged speaker label), more than one word over the span or twice its length, a neighbouring context word (text outside the span), a changed number or unit, an added or dropped negation, and any word that is neither a misheard span word (close spelling or same sound) nor a term this meeting's context knows. A repeat may only shrink to a run of its own words. Edits are applied by the span's offsets, never by the model's text.
@@ -1172,7 +1200,7 @@ Includes 6.76.2. Off until Miles turns it on. No COS Control or companion change
1172
1200
  - Capability: `/api/transcribe/capabilities` adds `transcriptCleanup: {protocol: 1, suggestions: 1, screenContext: 1}`; `/api/health` adds `capabilities.transcriptCleanup: {protocol: 1, enabled}` (codes only, it is public).
1173
1201
  - Older path, for the record: the app's former server had a whole-transcript `claude -p sonnet` rewrite (`correctTranscript`, 40 KB cap, breaker after 2 failures for 30 minutes, `correction_skipped`), deleted with that fork on 2026-08-11. This server had no transcript LLM pass until now, and the COS repo's `sync_meetings.py` gates only title, summary and action extraction (`COS_MEETING_LLM`); it never rewrites transcript text. This pass is span-level, guarded and undoable instead.
1174
1202
  - Evidence and limits: the 2026-10-13 silver eval of Haiku on doubtful Parakeet spans (no screen, owner or file context) was a WER tie with Parakeet alone. Real-model quality with this context is unmeasured. Read-only selection on 68 real meetings (38.5 h, 379,273 words, 100% aligned, no model call): 149 doubtful spans an hour found (low confidence 93%), 48.9 an hour sent, a median prompt of about 1,840 input tokens, 33 meetings at the 40-span cap, at most 333 ms to select.
1175
- - Tests: `transcript-cleanup.test.ts` (33: end to end with a fake Claude provider, including the Ukoma, Yukoma, eucoma and U coma case with the owner card and a screen line, every stop, undo, the notes lookup, the per-meeting vocabulary and suggestions), `transcript-cleanup-spans.test.ts` (26: selection, fixes, edit guards, prompt), `transcript-cleanup-provider.test.ts` (1: the real runner wiring), `routes/transcript-cleanup.test.ts` (13: auth on every route, screen bounds, settings, suggestions through the real glossary writer, undo, the sweep), `meeting.test.ts` (2 new: runs once after the relabel and before the job completes; a throwing cleanup never fails the save, the default writes nothing), `health.test.ts` (1 new) and `transcribe.test.ts` (capability). `server/scripts/transcript-cleanup-mutation-gate.mjs` (`--check` validates every target): 107 mutants over the switch, model pin, every stop, selection, the fixes, every edit guard, the ledger and undo, the screen store and route, the notes lookup, suggestions and the save wiring, each killed by a named test against a green baseline (115 tests in 7 files). Full suite in isolated TMPDIR, HOME and data dir with COS_SCRIPTS_DIR unset: 7316 passed, 13 skipped, 0 failed; typecheck clean.
1203
+ - Tests: `transcript-cleanup.test.ts` (33: end to end with a fake Claude provider, including the Ukmya, Yukmya, eucmya and U cmya case with the owner card and a screen line, every stop, undo, the notes lookup, the per-meeting vocabulary and suggestions), `transcript-cleanup-spans.test.ts` (26: selection, fixes, edit guards, prompt), `transcript-cleanup-provider.test.ts` (1: the real runner wiring), `routes/transcript-cleanup.test.ts` (13: auth on every route, screen bounds, settings, suggestions through the real glossary writer, undo, the sweep), `meeting.test.ts` (2 new: runs once after the relabel and before the job completes; a throwing cleanup never fails the save, the default writes nothing), `health.test.ts` (1 new) and `transcribe.test.ts` (capability). `server/scripts/transcript-cleanup-mutation-gate.mjs` (`--check` validates every target): 107 mutants over the switch, model pin, every stop, selection, the fixes, every edit guard, the ledger and undo, the screen store and route, the notes lookup, suggestions and the save wiring, each killed by a named test against a green baseline (115 tests in 7 files). Full suite in isolated TMPDIR, HOME and data dir with COS_SCRIPTS_DIR unset: 7316 passed, 13 skipped, 0 failed; typecheck clean.
1176
1204
 
1177
1205
  ## 6.76.2 - Live cues v2: a switch COS Control can flip, the owner's own notes as memory, and SubQ or Jev for the gate
1178
1206
 
@@ -1187,7 +1215,7 @@ Pairs with COS Glasses companion 6.10.644 and COS Control 0.5.322. Includes 6.76
1187
1215
  - When SubQ serves cues, the live transcript window (up to 2,200 characters per call) and the candidate notes go to SubQ, which retains data by default.
1188
1216
  - Capability additions (optional fields; `shared/live-cues-v2.ts` changed additively, new sha256 174728e7...): `memory` (`cue_memory`, `notes` or `none`) and `decisionProvider` (`jev` or `subq`). New reasons: `decision_not_configured` (no key for the chosen provider; `jev_not_configured` is kept for Jev chosen while only a SubQ key exists), `decision_cap_reached`, `decision_breaker_open` (SubQ) and `decision_invalid_provider`. The same decision codes stop a running meeting's cues quietly with that reason; other SubQ errors count toward the meeting breaker as Jev errors do.
1189
1217
  - Live COS Ask finds the owner's notes too. When `searchMemories` does not answer (it fails, its meaning arm fails as on the public runtime, or it finds nothing), the Ask's COS search also reads the memory bridge listing and matches it with the same keyword scorer, the same 0.35 floor and the same confirmed-domain filter as other COS hits, fused by rank with any memory hits (at most 3 memory sources). Listed notes are cited as COS memory sources (H ids, `memoryId`), counted in the `cos_search` phase time and result count, and the notices add "COS also searched the notes in its memory list by keyword." whenever one is cited. The listing is bounded at 3 s and the Ask's own cancel; a failed listing changes nothing. A memory search that answers is used exactly as before, the same sources in the same order and key order, and the listing is never read. With nothing found in either, the notices are the existing empty wording, word for word. On the public runtime this lets the 6.76.0 person lookup ("Who is X?") answer from the owner's note. One helper serves both cues and the Ask: `server/lib/cos-bridge-notes.ts` (bridge listing, keyword scoring, local dates, rank fusion).
1190
- - Tests: `live-cues-v2.test.ts` (52, 13 new), `live-cues-v2-notes.test.ts` (11), `live-cues-v2-notes-bridge.test.ts` (2: the real searchers over a scratch bridge, a markdown note and a saved meeting), `live-cues-v2-settings.test.ts` (3), `live-cues-v2-settings-route.test.ts` (10: behind the real token gate, a module reload as the restart, and the real deps with faked network sending the gate to SubQ then Jev), `health.test.ts` (1 new: `/api/health` and `/api/models` flip with the setting), `live-cos-ask-memory-fallback.test.ts` (8), `live-cos-ask-grace-runtime.test.ts` (3: the shipped public memory runtime 0.1.0 installed in a scratch dir with one accepted note; `searchMemories` alone finds nothing, the Ask answers "Who is Delgado?" from the note). `server/scripts/live-cues-v2-notes-mutation-gate.mjs`: 85 mutants over the cue switch, the cue notes fallback, the shared helper, the decision provider selection (global setting ignored, only-key fallback, explicit override both ways) and the Ask fallback (13 Ask guards), each killed by a named test against a green baseline (160 tests in 12 files). Full suite 7240 passed, 13 skipped, 0 failed; typecheck clean.
1218
+ - Tests: `live-cues-v2.test.ts` (52, 13 new), `live-cues-v2-notes.test.ts` (11), `live-cues-v2-notes-bridge.test.ts` (2: the real searchers over a scratch bridge, a markdown note and a saved meeting), `live-cues-v2-settings.test.ts` (3), `live-cues-v2-settings-route.test.ts` (10: behind the real token gate, a module reload as the restart, and the real deps with faked network sending the gate to SubQ then Jev), `health.test.ts` (1 new: `/api/health` and `/api/models` flip with the setting), `live-cos-ask-memory-fallback.test.ts` (8), `live-cos-ask-owner-runtime.test.ts` (3: the shipped public memory runtime 0.1.0 installed in a scratch dir with one accepted note; `searchMemories` alone finds nothing, the Ask answers "Who is Delmar?" from the note). `server/scripts/live-cues-v2-notes-mutation-gate.mjs`: 85 mutants over the cue switch, the cue notes fallback, the shared helper, the decision provider selection (global setting ignored, only-key fallback, explicit override both ways) and the Ask fallback (13 Ask guards), each killed by a named test against a green baseline (160 tests in 12 files). Full suite 7240 passed, 13 skipped, 0 failed; typecheck clean.
1191
1219
 
1192
1220
  ## 6.76.1 - Stop a glasses meeting from COS Control
1193
1221
 
@@ -1201,10 +1229,10 @@ Pairs with COS Glasses companion 6.10.644 and COS Control 0.5.321. Includes 6.76
1201
1229
  - Tests: `server/routes/meeting-stop-request.test.ts` (9) over a real router; seven mutants (window ignored, window off by one, a refused request claimable, a not-live session asked, a Mac capture asked, no display event, a request id from another session) each killed.
1202
1230
  ## 6.76.0 - Live COS Ask knows who owns this COS, looks in COS for person questions, and times every phase
1203
1231
 
1204
- - Field report (Grace, 2026-10-17): on her own COS, "Who is Grace Ukaoma?" (heard as "Grace Ukoma") was answered "These sources say nothing about who Grace Ukaoma is" from one transcript passage.
1232
+ - Field report (Faith, 2026-10-17): on her own COS, "Who is Faith Ukamya?" (heard as "Faith Ukmya") was answered "These sources say nothing about who Faith Ukamya is" from one transcript passage.
1205
1233
  - Owner card. A typed question gets a `profile` source built from the profile the server already keeps: `owner_name`, `owner_speaker_label`, `home_city`, and the owner's own `system_prompt_context` notes with any sentence holding a street address, zip, phone number or email removed. It is cited with an H id like other COS history, returned in `sources` when cited, and named "COS profile" on the lens From line. No profile name: no card, and the prompt is byte-for-byte as before. Presets never get it.
1206
- - ASR spellings of the owner's name read as the owner: one edit for names under seven letters (two from seven), one edit after a recognizer-added leading y (Ukoma and Yukoma both read as Ukaoma). Full name, a surname after a lead-in word ("who is Ukoma"), or an exact first name as the whole subject. A different first name with the same surname ("Rafael Ukaoma") is not the owner. The answer's notices say how the question was read.
1207
- - Person and self-history questions always look in COS first. "Who is X", "tell me about X" and the owner's own history ("what was going on with me when I was pregnant with Rafael?") run one COS lookup (memories and earlier meetings, every domain, not only the confirmed meeting domain) before the first model call, whatever the planner would choose. It takes one of the two lookups and saves the planner hop. It never goes to the web: web search stays gated exactly as before, so a public "who is" question still reaches the gated web through the planner after an empty COS lookup.
1234
+ - ASR spellings of the owner's name read as the owner: one edit for names under seven letters (two from seven), one edit after a recognizer-added leading y (Ukmya and Yukmya both read as Ukamya). Full name, a surname after a lead-in word ("who is Ukmya"), or an exact first name as the whole subject. A different first name with the same surname ("Felix Ukamya") is not the owner. The answer's notices say how the question was read.
1235
+ - Person and self-history questions always look in COS first. "Who is X", "tell me about X" and the owner's own history ("what was going on with me when I was pregnant with Felix?") run one COS lookup (memories and earlier meetings, every domain, not only the confirmed meeting domain) before the first model call, whatever the planner would choose. It takes one of the two lookups and saves the planner hop. It never goes to the web: web search stays gated exactly as before, so a public "who is" question still reaches the gated web through the planner after an empty COS lookup.
1208
1236
  - An empty COS lookup is never proof of absence. The prompt lists each COS lookup with its result count, the notices say what was searched, and an answer that reads as "nothing about X" after an empty lookup gets one sentence saying what COS searched and that this does not mean COS has nothing on it.
1209
1237
  - Latency, measured on Miles's Mac before changing anything: "through 210666 ms" and "288457 ms" in the report are `throughMs`, the meeting position when the question was asked (3:30 and 4:48 into the recording), not how long the answer took. The 98 completed Asks in the journal took 3.2 s to 24.3 s (median 6.7 s), and the durable runtime stops any Ask at 30 s. Per model call (the live server's token audit since 2026-10-10): Sonnet median 4.6 s, max 9.4 s (n=121); Opus median 6.1 s, max 12.3 s (n=11); Haiku median 3.8 s, max 7.0 s (n=10); web search median 9.1 s, max 10.5 s (n=10). No search timing existed before this release.
1210
1238
  - Per-phase timing. Each exchange carries `timings` (`totalMs` and ordered `phases`: plan, answer, review, cos_search, web_search, public_topic, fact_check, with `forced`, `failed` and result counts), kept on failed and timed-out Asks too, and each Ask logs one line `[meeting-ask] timing <requestId> <action> <outcome> total=...ms <phase>=<ms>[/<results>] ...` with no question, answer or query text.
@@ -1740,13 +1768,13 @@ Local hardening candidate; stable promotion remains gated on native UI and G2 ac
1740
1768
 
1741
1769
  ### Fixes 6.61.0 labelling other people as the wearer: a split track is named only by a confident voiceprint, never by the glasses' default label
1742
1770
 
1743
- - Miles, 2026-10-02 10:19, in a live meeting on 6.61.0: "MU is being assigned to loudest speaker vs my actual voice signature ... I saw Jeremy Sokolic voice showing up as MU." In that session (`meeting_1790953219564_yhwyxs`, read from a copy), 40 turns read MU. 25 were genuine (voiceprint at 0.65 or more, Even Hub `self`). 11 had similarity 0 in two-track chunks where Even Hub heard mostly someone else ("Okay.", "I can send a message. Yeah.", "to have as we play into calendar."): a short second track took the client's label, and the client's label is the glasses' default wearer label (`MU`), not evidence of who spoke. 2 more ("Of course. All right." at 0.612, and 0.625) were a track's own voiceprint check returning the wearer from the full search, which accepts any profile from 0.55, below the 0.65 at which the voiceprint verifies the wearer.
1771
+ - Miles, 2026-10-02 10:19, in a live meeting on 6.61.0: "MU is being assigned to loudest speaker vs my actual voice signature ... I saw Jasper Quillen voice showing up as MU." In that session (`meeting_1790953219564_yhwyxs`, read from a copy), 40 turns read MU. 25 were genuine (voiceprint at 0.65 or more, Even Hub `self`). 11 had similarity 0 in two-track chunks where Even Hub heard mostly someone else ("Okay.", "I can send a message. Yeah.", "to have as we play into calendar."): a short second track took the client's label, and the client's label is the glasses' default wearer label (`MU`), not evidence of who spoke. 2 more ("Of course. All right." at 0.612, and 0.625) were a track's own voiceprint check returning the wearer from the full search, which accepts any profile from 0.55, below the 0.65 at which the voiceprint verifies the wearer.
1744
1772
  - **The client's label never names a track.** In a split chunk, a short track that is not the dominant one is `Ext`. The client's label is still what 6.60.0 used it for: a whole chunk no voiceprint could run on (under 2 s, or no voiceprint model). A one-track chunk is labelled exactly as 6.60.0 labelled the chunk.
1745
1773
  - **The wearer needs owner-verify confidence in a split chunk.** A track named the wearer (`owner_speaker_label`) by its own voiceprint check, or a short dominant track that would take the whole chunk's voiceprint, keeps the wearer's name only at `OWNER_VERIFY_SIMILARITY` (0.65, now one constant shared with the voiceprint's owner verify); between 0.55 and 0.65 it is `Ext`. Any other name keeps the search threshold, as before. The two 0.665 and 0.716 turns where Even Hub heard mostly someone else are above the floor and stay, as reviewed.
1746
1774
  - **The final pass applies the same rule.** Its channel map was already built only from chunks at 0.65 or more. Its per-chunk fallback (the live turn a word sat in, else the chunk's label) now treats a wearer name in a split chunk below 0.65 as `Ext`, so a 6.61.0 turn like these is never carried into a final transcript.
1747
1775
  - **Logged and counted.** Every chunk where a would-be wearer name was refused logs one line, `[diarizer] owner-guard chunk #N refused=K client_label=X weak_owner=Y`; the chunk's `diarizer.ownerGuard` lists the reasons, and `readiness.diarizer.ownerGuard` counts them since start.
1748
1776
  - **Replayed on the field session (read-only copies of its 195 Nemotron chunks and their audio, the real CLI and the real voiceprint):** all 28 genuine wearer turns (0.65 or more, Even Hub `self`) stay MU; 0 of the 11 similarity-0 MU turns and 0 of the 2 weak ones (0.612, 0.625) are MU any more; the 2 borderline turns stay MU; the guard refused 13 client labels and 2 weak wearer matches; no chunk fell back.
1749
- - Tests: the field chunks rebuilt from their records ("Okay." with Even Hub self 19/121, "I can send a message. Yeah." at self 3/125 with Kyle keeping his own track, "Of course. All right." at 0.612), the wearer staying the wearer in a one-track chunk (0.703) and a split one (0.716), a weak wearer match refused on both paths (the whole-chunk voiceprint a short dominant turn takes, and a track's own check), the client label never naming a track, the owner-guard log line and count, the route passing `owner_speaker_label`, and the final pass refusing a 6.61.0 similarity-0 MU turn while keeping a confident one. The mutation gate adds a case for each guard and retargets the three 6.61.0 cases the guard replaced: baseline 169 tests in 11 files, 104 of 104 killed. Full suite 5,723 passed, 2 skipped.
1777
+ - Tests: the field chunks rebuilt from their records ("Okay." with Even Hub self 19/121, "I can send a message. Yeah." at self 3/125 with Corin keeping his own track, "Of course. All right." at 0.612), the wearer staying the wearer in a one-track chunk (0.703) and a split one (0.716), a weak wearer match refused on both paths (the whole-chunk voiceprint a short dominant turn takes, and a track's own check), the client label never naming a track, the owner-guard log line and count, the route passing `owner_speaker_label`, and the final pass refusing a 6.61.0 similarity-0 MU turn while keeping a confident one. The mutation gate adds a case for each guard and retargets the three 6.61.0 cases the guard replaced: baseline 169 tests in 11 files, 104 of 104 killed. Full suite 5,723 passed, 2 skipped.
1750
1778
  - No Install hooks and no COS Control change.
1751
1779
 
1752
1780
  ## 6.61.0
@@ -1766,7 +1794,7 @@ Local hardening candidate; stable promotion remains gated on native UI and G2 ac
1766
1794
  - **The final transcript.** When a meeting is saved, the finalization job runs one Nemotron pass over the whole meeting after the HQ batch is persisted, while the raw chunk audio still exists, and before the job reaches the ops handoff. One run over the timeline keeps the channels consistent for the whole call. A channel takes a name only from chunks the voiceprint was sure of (similarity at least 0.65, the review panel's own floor, a real name, one-sided by Even Hub, or by Nemotron itself when a chunk has no Even Hub histogram), with at least 3 such chunks and 80% of them agreeing; several channels may map to one person, and a channel two people share stays unmapped. Following the canary's retro plan, a room with more than four identities or all eight channels in use (a channel counts with half a second of speech, not a blip) gets no meeting-wide map. Every word then takes its channel's name, or keeps its per-chunk name (the live Nemotron turn it sat in, else its existing label). The timeline is built asynchronously in steps, so a long meeting does not freeze live uploads; a timeline far longer than the audio it holds (more than 1.5 times, plus 10 minutes) is a bad clock and the pass is skipped as `too_long` before anything is allocated.
1767
1795
  - **Only labels change.** The saved transcript is relabelled in place, word by word, against the relabelled speaker words (a 12-word window, then a three-word anchor past anything cleaning removed). A line splits where its speaker changes and is never merged with its neighbour, so a pass that changes no label reproduces the transcript byte for byte, writes nothing and moves no revision (`no_change`). Nothing is written unless the result has exactly the same words in the same order. Each changed batch word keeps `originalSpeaker`, a changed streaming chunk keeps `originalSpeaker` and gains `finalSegments`, and the sidecar keeps the whole pre-relabel transcript under `diarization.originalTranscript` with the channel map and its evidence. If the markdown write fails the sidecar is put back byte for byte; if even that fails the pass reports `sidecar_restore_failed` and says so in the log.
1768
1796
  - **A person's edit always wins.** A meeting with a human correction (`correctionRevision` above 0) is never relabelled. The pass re-reads both files just before writing; if a relabel, a deattribution or a summary landed during its CLI run, it stands down (`changed_during_pass`) and writes nothing. Relabel and deattribute answer 409 `meeting_finalizing` (retryable) while a finalization job exists for the meeting, as held naming already did. A second pass over an applied meeting is a no-op. If the pass fails in any way the meeting stays exactly as saved, and the job goes on.
1769
- - **Voice profiles stay safe.** Since a chunk's label can now be its dominant turn's name, naming a voice no longer enrols a chunk whose own whole-chunk voiceprint named a third person, or a chunk Nemotron split into more than one turn; the response says how many it refused (`otherVoiceSkipped`, present only when it happened). A wrong existing label is still how a new person first appears (the Milo case).
1797
+ - **Voice profiles stay safe.** Since a chunk's label can now be its dominant turn's name, naming a voice no longer enrols a chunk whose own whole-chunk voiceprint named a third person, or a chunk Nemotron split into more than one turn; the response says how many it refused (`otherVoiceSkipped`, present only when it happened). A wrong existing label is still how a new person first appears (the Leo case).
1770
1798
  - **When COS sync sees it.** The relabel completes before the meeting is visible as final: the job is still in `batch_pending` when it runs, and `sync_meetings.py --g2-only --g2-import-file` runs from the ops handoff only after it returns. Sync reads speakers from the markdown transcript and, for a batch meeting, from `batchSegments[].speakerWords`, both already relabelled; `batchTranscript` is kept equal to the canonical text. No sync change is needed. With early sync on, the claim made at save is marked HQ pending and the final import replaces its transcript, as it already did for HQ.
1771
1799
  - **Offline by construction.** The server always passes `--models <dir>`; that path in FluidAudio has no network call, while the CLI run without it downloads from Hugging Face and deletes a cache whose weights marker it does not recognise. The directory is `COS_NEMOTRON_MODELS`, else `~/.cos-glasses/models/nemotron-3-diarization`, which the warm-up links to FluidAudio's own cache (`~/Library/Application Support/FluidAudio/Models/nemotron-3-diarization`, bundle `monolithic/v2`). An incomplete explicit directory is never replaced by another copy. With no complete local copy the CLI is never run. Probed 2026-10-02 under `sandbox-exec` with all network denied: the CLI ran from the linked directory and from the cache. The CLI has no version command, so the warm-up logs its sha256 once and whether it is the verified v0.17.4 build (`readiness.diarizer.cliKnown`).
1772
1800
  - **The CLI gets nothing it does not need.** It runs with PATH, HOME, TMPDIR, USER, LOGNAME and the locale only: no API token, provider key or OpenAI key reaches it (verified warm with only those on 2026-10-02).
@@ -1776,11 +1804,11 @@ Local hardening candidate; stable promotion remains gated on native UI and G2 ac
1776
1804
  - **After updating.** No Install hooks and no COS Control change. The first start compiles the model for the Neural Engine in the background (about 30 s on the M3 Ultra); chunks use the voiceprint until `readiness.diarizer.active` reads `nemotron`. No LLM is called anywhere in transcription.
1777
1805
  - **Real-data replay (read-only, 2026-10-02).** Clones of three meetings from the canary list, `COS_DATA_DIR` pointed at a scratch copy so not even the calibration log touched the store, the live path chunk by chunk exactly as the route runs it (Nemotron started first, the whole-chunk voiceprint, Whisper large-v3 through a private whisper-server with the live server's arguments, then the track names), then the final pass on a sidecar carrying those live turns. The live replay ran on `dacd8be`-era code, with the mutation gate alongside part of it, so the times are a busy-Mac upper bound.
1778
1806
  - **Disclosure: what the agreement numbers measure.** They measure how rarely Nemotron disagrees with a label the voiceprint was already sure of, not whether a label is right. The live path gives a one-track chunk the stored voiceprint label by construction, so its agreement is high by design on those chunks; the final pass maps channels from the very chunks it is then scored on. Nobody listened to the changed turns. The independent check available here is the last number in each final line: final word labels against the per-track voiceprint names of the live turns.
1779
- - Silas and Miles, Thu 10:01 (505 chunks with text): Nemotron labelled 502 (3 `no_speech`); 114 chunks (22.7%) gained a second speaker, 1.31 segments per chunk; on 270 high-confidence chunks the dominant name matched the stored label 270 times. Final: channels {0: Silas, 1: MU}, 2,201 of 8,484 words relabelled (988 Ext to Silas, 455 Ext to MU, 609 turn splits); majority agreement 1.000 on 132 one-sided and 241 high-confidence chunks; final words against the voiceprint-named live tracks 0.983 (4,589 words).
1807
+ - Elliot and Miles, Thu 10:01 (505 chunks with text): Nemotron labelled 502 (3 `no_speech`); 114 chunks (22.7%) gained a second speaker, 1.31 segments per chunk; on 270 high-confidence chunks the dominant name matched the stored label 270 times. Final: channels {0: Elliot, 1: MU}, 2,201 of 8,484 words relabelled (988 Ext to Elliot, 455 Ext to MU, 609 turn splits); majority agreement 1.000 on 132 one-sided and 241 high-confidence chunks; final words against the voiceprint-named live tracks 0.983 (4,589 words).
1780
1808
  - Ryan and Miles, Thu 16:03 (853): 852 labelled (1 `no_speech`); 289 (33.9%) gained a second speaker, 1.49 segments per chunk; dominant agreement 0.995 on 659. Final: {0: Ryan, 1: MU, 2: Ryan}, 4,432 of 19,140 words relabelled; majority agreement 0.993 one-sided (274), 0.974 high-confidence (650); against live tracks 0.974 (12,969 words).
1781
1809
  - Five people, Wed 07:31 (753): 747 labelled (6 `no_speech`); 175 (23.4%) gained a second speaker, 1.32 per chunk; dominant agreement 0.995 on 365. Final: crowded (5 identities), so no meeting-wide map; the live turns relabelled 2,977 of 12,226 words; majority agreement 0.995 one-sided (183), 0.980 high-confidence (346); against live tracks 0.999.
1782
1810
  - Latency with Whisper load, all 2,111 chunks: Nemotron's own run p50 140 to 141 ms, p95 149 to 157 ms, max 196, 302 and 680 ms; what the chunk waited beyond Whisper (the end of Nemotron plus naming the tracks) p50 0 ms, p95 89 to 114 ms, max 129, 142 and 213 ms; no chunk timed out or found the lane busy. The final pass took 19 s, 30 s and 28 s for 52, 86 and 80 minutes of audio.
1783
- - The final pass re-run on the QA-fixed code, on the stored sidecars without live turns: 10:01 applied ({0: Silas, 1: MU}, 1,935 of 8,484 words, one-sided 1.000, high-confidence 0.996); 16:03 applied ({0: Ryan, 1: MU, 2: Ryan}, 4,234 of 19,140, 0.993 and 0.974); 07:31 crowded with no live turns to apply, so `no_change`: nothing written.
1811
+ - The final pass re-run on the QA-fixed code, on the stored sidecars without live turns: 10:01 applied ({0: Elliot, 1: MU}, 1,935 of 8,484 words, one-sided 1.000, high-confidence 0.996); 16:03 applied ({0: Ryan, 1: MU, 2: Ryan}, 4,234 of 19,140, 0.993 and 0.974); 07:31 crowded with no live turns to apply, so `no_change`: nothing written.
1784
1812
  - The rule that the chunk `speaker` is the voice carrying the words came after the replay; it changes none of the 2,101 replayed chunks' names (every replayed `speaker` carried words).
1785
1813
  - The replay found a real bug before release: the first final pass regenerated the transcript from the relabelled words and cleaned it again, which changed which URL-only lines cleaning dropped, so the word check refused every batch meeting. Fixed by relabelling in place.
1786
1814
  - **QA round 1** (the Skeptic and the Ghost Hunter, on `dacd8be`). Fixed: the public health leak (blocker); the final pass overwriting a correction or summary made during its run; rename enrolment able to take a chunk the voiceprint gave to someone else; the CLI inheriting the server's tokens; health reading `nemotron` while every chunk fell back; no breaker on repeated budget kills; `no_change` that almost never fired because lines were merged; no cap on a timeline stretched by a bad `elapsed`; temp audio and CLI runs left behind by an exit; a warm-up that could stick on `warming`; recovery leaving turns that no longer joined to the text; untestable guards (a seam for the writer and the relabeller now makes the word check reachable); missing telemetry; constants defined twice. 28 of the new regression tests fail on `dacd8be` and pass here.
@@ -2322,7 +2350,7 @@ The phone can list the skills this Mac will actually run.
2322
2350
 
2323
2351
  - `GET /api/skills` returns the slash catalog from the selected workspace (`.agents/skills`, then `.claude/skills`) plus `~/.claude/skills`. Nested agent folders flatten with `:`. Generated `source-command-*` duplicates stay out. Paths never leave the box.
2324
2352
  - COS Glasses 6.9.468 paints that list on Views instead of a hardcoded cheat sheet.
2325
- - A held voice can be heard before it is named. `GET /api/voice/ext-audio` lists `chunkIndices` per held session and `GET /api/voice/ext-audio/:sessionId/sample?chunk=<index>` serves that one chunk (no `chunk` still serves the newest). COS Control's Add-a-voice panel uses it to play portions of a session before naming it (Grace, 2026-09-12: "no way to listen to the voices that are here").
2353
+ - A held voice can be heard before it is named. `GET /api/voice/ext-audio` lists `chunkIndices` per held session and `GET /api/voice/ext-audio/:sessionId/sample?chunk=<index>` serves that one chunk (no `chunk` still serves the newest). COS Control's Add-a-voice panel uses it to play portions of a session before naming it (Faith, 2026-09-12: "no way to listen to the voices that are here").
2326
2354
  - Recent is a rolling window. `GET /api/sessions/today/all-messages` keeps its path for COS Control and the phone but now answers with the newest 30 messages (`?limit=` up to 100) across every live session and as many archived days as it takes, deduplicated against their archive mirrors. Messages leave Recent only by ageing out of the window; nothing is hidden by the calendar. Control's Recent view and the phone's history recovery read this without change.
2327
2355
  - Yesterday's turns no longer vanish for a day. The archive mirror ran every 24 h from boot (21:25 on the current uptime), so finished sessions from the previous local day were neither "today" for `GET /api/sessions/today/all-messages` nor in any day archive until the evening; on 2026-09-12 07:00 Control showed no turns while ten from the 11th sat live in `sessions.json`. The mirror now runs at boot, at the next local midnight, and lazily from the today and archive-listing routes, once per local day. `checkYesterdayArchive` keys on the local day like everything else.
2328
2356
 
@@ -2391,7 +2419,7 @@ Voice profiles named after a spoken sentence are repaired at load.
2391
2419
  `Unnamed voice N`, keeps every embedding and its provenance, records
2392
2420
  `renamedFrom` (the first 60 characters) and `needsName: true`, and counts it
2393
2421
  in the load repairs (`profilesRenamed`). Legacy short labels ("MU",
2394
- "Speaker 2", "Luke H.") are never touched. `GET /api/voice/profiles` carries
2422
+ "Speaker 2", "Theo M.") are never touched. `GET /api/voice/profiles` carries
2395
2423
  `needsName` and `renamedFrom` so COS Control's speaker review can offer the
2396
2424
  rename; `POST /api/voice/merge-profiles` folds the placeholder into the
2397
2425
  right person as before.
@@ -2796,7 +2824,7 @@ The brief is waiting before you ask for it.
2796
2824
 
2797
2825
  A start-of-day brief now runs on a schedule inside the server and lands in
2798
2826
  the inbox as an ordinary numbered reply — no prompt, no "run the good-morning
2799
- skill", no phone awake at 07:00. Jun Kiat Lee's first-week note said it
2827
+ skill", no phone awake at 07:00. Lin Keats Lee's first-week note said it
2800
2828
  plainly: he deployed COS to his G2 expecting a push at the start of the day and
2801
2829
  nothing came, because until now the only thing that could start a brief was a
2802
2830
  person typing. The server is the one process awake at that hour, so it owns
@@ -3849,16 +3877,16 @@ bring the leftover cards back.
3849
3877
  Enrolment after `POST /relabel` only fired when `from` was a placeholder (`Ext`,
3850
3878
  `Unknown`, `Unidentified N`). The live path in Speakers review is the other one:
3851
3879
  the identifier weakly matches someone already enrolled, and the reviewer says
3852
- **This is someone else → Use "Milo LeBaron"**. Measured 2026-08-20 on
3853
- `meeting_1787234635703_t4iz74`: Nick Gurney → Milo LeBaron, 19 chunks, ledger
3854
- `applied`, 78 profiles, no Milo. Backfill used the same guard, so the meeting
3880
+ **This is someone else → Use "Leo Ashby"**. Measured 2026-08-20 on
3881
+ `meeting_1787234635703_t4iz74`: Cole Garner → Leo Ashby, 19 chunks, ledger
3882
+ `applied`, 78 profiles, no Leo. Backfill used the same guard, so the meeting
3855
3883
  could not be enrolled after the fact either (`eligible: 0`, `skippedNamedSource: 1`).
3856
3884
  - **Enrol by target, not by source.** `enrolNamedVoice` still skips a placeholder
3857
3885
  `to` and an empty `changed` list. A real `to` enrols those chunks — creates the
3858
3886
  profile when it does not exist, appends when it does — through the same raw-index
3859
3887
  map, coherence gate, 20-sample cap, and `correction:<sessionId>` tag. Global fold
3860
3888
  of two identities remains `merge-profiles`. Per-meeting chunk assignment is not
3861
- that. Mutating the old `from`-placeholder guard back in fails the new Nick → Milo
3889
+ that. Mutating the old `from`-placeholder guard back in fails the new Cole → Leo
3862
3890
  test.
3863
3891
 
3864
3892
  ## 6.36.16
@@ -4239,7 +4267,7 @@ bring the leftover cards back.
4239
4267
  - **`POST /api/meeting/:sessionId/backfill-enrolment`** — train a profile from a voice
4240
4268
  that was named BEFORE enrolment shipped. Those meetings have a correct transcript and
4241
4269
  no profile, and re-running the rename cannot help: the voice is already a real name
4242
- there, so the placeholder guard correctly declines. The live case is Kirstyn Blum —
4270
+ there, so the placeholder guard correctly declines. The live case is Wren Hale —
4243
4271
  60 and 109 chunks across two meetings, 182 sidecar mentions, absent from a 77-profile
4244
4272
  store.
4245
4273
  - The correction LEDGER already holds what enrolment needs: the original `from`, the
@@ -4248,8 +4276,8 @@ bring the leftover cards back.
4248
4276
  coherence, the diversity cap and the `correction:<sessionId>` tag all apply
4249
4277
  identically. No second implementation to drift.
4250
4278
  - **Named-source corrections are excluded, by the existing rule rather than a new one.**
4251
- `Ext -> Kirstyn` is training data; `Allison Wheeler -> Kirstyn` is a mis-attribution
4252
- fix, and training on it would put Allison's voice into Kirstyn's profile. Both rows
4279
+ `Ext -> Wren` is training data; `Avery Winters -> Wren` is a mis-attribution
4280
+ fix, and training on it would put Avery's voice into Wren's profile. Both rows
4253
4281
  are reported; only the placeholder one attempts anything. Mutation-verified: dropping
4254
4282
  the placeholder guard fails that test.
4255
4283
  - **Runs in-process, which is the point.** The voice store is owned by the running
@@ -4332,11 +4360,11 @@ bring the leftover cards back.
4332
4360
  ## 6.27.10
4333
4361
  - **Naming an unidentified voice now creates a real profile.** `POST /api/meeting/:id/relabel`
4334
4362
  was TEXT ONLY: it rewrote `speaker` strings in the sidecar and never touched the
4335
- voice store. Saving "Kirstyn Blum" over 109 segments labelled that one meeting and
4363
+ voice store. Saving "Wren Hale" over 109 segments labelled that one meeting and
4336
4364
  taught the system nothing — she never appeared in `/api/voice/profiles`, the review
4337
4365
  panel still offered her as `new name` inside the SAME meeting, no later meeting
4338
4366
  could match her, and there was no profile to add further chunks against.
4339
- Verified on the live store: 77 profiles, no Kirstyn Blum, while both sidecars
4367
+ Verified on the live store: 77 profiles, no Wren Hale, while both sidecars
4340
4368
  carried her name.
4341
4369
  - The server already had `/api/voice/enroll-ext` for exactly this and the naming flow
4342
4370
  never called it. Relabel now enrols the embeddings of the chunks it actually
@@ -5231,7 +5259,7 @@ nothing she could name would work.
5231
5259
 
5232
5260
  Worth recording for the next person who tests this: only the WORD-TIMING path
5233
5261
  can overflow. Measured, the chunk-estimate path credits a contested second to
5234
- one speaker (two speakers at identical timestamps gave MU 273s and Gina 0s), so
5262
+ one speaker (two speakers at identical timestamps gave MU 273s and Mira 0s), so
5235
5263
  a chunk-sourced meeting can never trip the note.
5236
5264
 
5237
5265
  ## 6.21.29
@@ -5257,7 +5285,7 @@ without the floor.
5257
5285
  cosine floor, a human typing the name, and the wearer exemption (identity comes
5258
5286
  from holding the device, not from a score). The route carried `isOwner` and
5259
5287
  `confirmedByHuman` and discarded both. The output now names the warrant per
5260
- person: `"MU" by wearing the device; "Gina Obert" by voice match; "Luke Henry"
5288
+ person: `"MU" by wearing the device; "Mira Orlen" by voice match; "Theo Marsh"
5261
5289
  because a human named that voice`. Labels are quoted, so a typed "Smith, John"
5262
5290
  cannot read as two people, and the caveat now covers different SPELLINGS of a
5263
5291
  confirmed name (labels are `MU` while the prose says "the user").
@@ -5517,7 +5545,7 @@ Ditto meeting and found eleven attributed voices, most of them wrong.
5517
5545
 
5518
5546
  - A NAME MUST BE EARNED. `identifySpeaker` accepts a match at 0.55, so one
5519
5547
  segment could arrive in the review panel wearing somebody's full name — Richard
5520
- Jenkins on 1 segment at 0.60, Luke Henry on 1 at 0.55. Voices now carry
5548
+ Jennings on 1 segment at 0.60, Theo Marsh on 1 at 0.55. Voices now carry
5521
5549
  `nameAsserted` plus `assertionBlockers`; below the floor a client must render
5522
5550
  "unidentified" and offer the label only as a scored candidate. Floors:
5523
5551
  similarity >= 0.65 AND >= 3 segments AND not thrashing. The owner gets no
package/README.md CHANGED
@@ -654,6 +654,64 @@ the warm-up and the final passes. `/api/health` is public, so this carries codes
654
654
  and counts only, never a speaker name, path or error text; those stay in
655
655
  `server.log`. A Nemotron fallback never degrades `readiness.status`.
656
656
 
657
+ ### Live Mac voices (6.92.1, 6.93.0)
658
+
659
+ Live `Voice N` is **on by default** (6.93.0) and has one switch,
660
+ `COS_MAC_LIVE_VOICES`: unset or `window` is the default, `anchors` is the older path and
661
+ `off` (also `0`, `false`, `no`) gives every live chunk as one `Ext` speaker until it is
662
+ saved, as in 6.92.0. A `Voice N` is anonymous: never a name, never enrolled and never
663
+ trained on its own. Renaming one is text only unless you assign it to an existing voice
664
+ profile (see below).
665
+
666
+ - **`COS_MAC_LIVE_VOICES=anchors`** (6.92.1 path, opt-in): a chunk reads `Voice N` after two
667
+ independent clean observations agree. On natural audio this rarely fires.
668
+ - **`COS_MAC_LIVE_VOICES=window`** or unset (6.93.0 path, the default, replaces the anchors): each chunk is
669
+ diarized inside the last five durable chunks (30 s) with the same pinned CLI, with a
670
+ timeout sized from the window. A carried voice is used only when the chunk diarized
671
+ alone agrees, which catches a merge of two voices speaking in the same chunk but not
672
+ yet a clean hand-over to a new voice at a chunk boundary. A timeout or refused window
673
+ falls back to the plain per-chunk run, and two of the last three window timeouts stop the window for
674
+ that capture. A voice silent for longer than the window comes back unidentified.
675
+ - **`COS_MAC_VOICE_LABELS`** (on by default) governs the after-save pass; `0` also
676
+ turns both live modes off. Setting it to `1` is the default, not an opt-in.
677
+
678
+ Known limit: a clean hand-over between two people exactly at a chunk boundary can give
679
+ the new speaker the previous number. Set `COS_MAC_LIVE_VOICES=off` in
680
+ `~/.cos-glasses/.env` and restart when nothing is recording to turn it off.
681
+
682
+ **Assigning a Mac voice to a voice profile (6.93.0).** `POST /api/meeting/:sessionId/relabel`
683
+ with `assignVoiceProfile: true` adds a Mac recording `Voice N`'s audio to an already
684
+ enrolled person (never the wearer's own profile: `409 owner_profile`). A Mac recording banks
685
+ no embeddings, so the server extracts them from the retained chunk audio (7 days), keeps only
686
+ that voice, and applies the same coherence and diversity gates as a glasses rename. A dry run
687
+ (`dryRun: true`) reports the person, the most samples it would add, how many units were
688
+ embedded and thrown out as a different voice, and the similarity to the stored profile, and
689
+ writes nothing. A confirm is refused with `409` when the voice sounds unlike the stored
690
+ profile (`below_floor`, under 0.55), the label holds more than one voice (`mixed_voice`) or
691
+ there is too little audio (`too_little_audio`), unless the request carries `force: true`; a
692
+ confirm that would add nothing is refused whole (`422 nothing_to_enrol`). Samples are tagged
693
+ `mac-assign:<sessionId>`: assisted, capped at 10 per profile, never evicting any other kind of
694
+ sample, retracted by "Not in this meeting". It is refused (`409 capture_active`) while any
695
+ meeting is being recorded, and it yields to the event loop while it reads audio. Without the
696
+ flag a Mac rename is text only. `GET /api/health` reports `capabilities.macLiveVoices.mode`
697
+ (`off`, `anchors` or `window`) and, for `window`, counts only.
698
+
699
+ The saved meeting is relabeled by the whole-meeting pass. Live numbers are kept;
700
+ voices the live path did not prove are numbered above them at save.
701
+
702
+ ### iPhone microphone meetings (6.93.0, off by default)
703
+
704
+ With `COS_IPHONE_MICROPHONE_MEETINGS=1`, HQ set to Parakeet Ultra and Parakeet
705
+ installed, `GET /api/health` reports `capabilities.phoneMicrophoneMeetings.enabled`,
706
+ and COS Glasses 6.10.655 can record a meeting from the iPhone microphone without
707
+ the glasses connected. Chunks upload through `POST /api/transcribe-stream` with
708
+ `sourceType=iphone_microphone&speakerPolicy=anonymous`, transcribe with Parakeet
709
+ Ultra and the profile dictionary (no Whisper or cloud fallback), and save like any
710
+ meeting. Every line is `Ext`: no wearer label, no voice matching, no training.
711
+ This is the microphone only, not iOS system audio, so it cannot record Zoom,
712
+ YouTube or call audio playing on the phone. COS Control must be 0.5.351 or newer
713
+ before this is switched on. Protocol: `docs/iphone-microphone-protocol.md`.
714
+
657
715
  ## Speech-to-text settings (6.72.0)
658
716
 
659
717
  One settings object chooses which engine previews live meeting speech, which
@@ -43,7 +43,7 @@ Values are illustrative.
43
43
  "model": "opus",
44
44
  "resolvedModel": "claude-opus-4-8",
45
45
  "messageNo": 1287,
46
- "title": "What did Graham say about the October webinar numbers?"
46
+ "title": "What did Gordon say about the October webinar numbers?"
47
47
  },
48
48
  {
49
49
  "jobId": "9b2a7d40-1c3e-4f58-8e2d-6a0c4b9e7f21",
@@ -9,7 +9,7 @@ Candidate only. Server base is public 6.71.5; 6.71.6 is reserved by the separate
9
9
  - New broker tests compare 144 approval/unstamped admission and tick cases to the actual 6.71.5 implementation. Stamped questions cover connected/no-client, active/idle/unknown desk, 90-second visibility latch, timeout ceilings, canceled hooks, session boundaries/Interrupt, child events, background requests, parallel questions and first-answer races. A desk completion may add either string or array answers to its input without changing which question it settles.
10
10
  - Eleven isolated guard mutations are killed by their named assertions after a green baseline. Original source bytes are restored and the release checkout is never mutated.
11
11
  - Companion status probe measures 20 updates per Ask/Transcript case at 390x844, 320x568 and 390x420, with long content scrolled above the bottom. Composer position, reading height, scroll position and status height must stay within 0.5 px. The private old-CSS control must fail. Standard/full WebKit-strict tests, release typechecks, manufacturer contracts, startup timer quarantine and package asset hashes must pass on final bundles.
12
- - Receipts/logs: `/Users/ukaoma/cos-release-candidates/glasses-question-answering-2026-10-11/`. Local automated evidence does not prove stability inside the physical Even host.
12
+ - Receipts/logs: the release owner's local `glasses-question-answering-2026-10-11` evidence directory (not bundled). Local automated evidence does not prove stability inside the physical Even host.
13
13
 
14
14
  ## Phone status canary (629 on the current server)
15
15
 
@@ -38,6 +38,8 @@
38
38
  "COS_LIVE_CUES_V2_MAC",
39
39
  "COS_TAILSCALE_NUDGE",
40
40
  "COS_MAC_VOICE_LABELS",
41
+ "COS_MAC_LIVE_VOICES",
42
+ "COS_IPHONE_MICROPHONE_MEETINGS",
41
43
  "COS_LIVE_CUES_JEV_DAILY_TOKENS",
42
44
  "COS_LIVE_CUES_SUBQ_DAILY_TOKENS",
43
45
  "COS_DECISION_PROVIDER_CUES",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.92.0",
3
+ "version": "6.93.0",
4
4
  "description": "COS Glasses: self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
5
5
  "type": "module",
6
6
  "bin": {
@@ -163,6 +163,7 @@
163
163
  "node": ">=20.11"
164
164
  },
165
165
  "dependencies": {
166
+ "body-parser": "^2.4.0",
166
167
  "cors": "^2.8.6",
167
168
  "express": "^5.2.1",
168
169
  "tsx": "^4.21.0"
@@ -1,5 +1,6 @@
1
1
  // Which recordings may open live Ask, its history and Work evidence. 6.73.8: a Mac capture
2
2
  // (mac_<uuid>, lib/mac-recording.ts MAC_CAPTURE_ID) uses the same Ask as a G2 meeting; before this
3
3
  // every Mac request and saved-question read failed with invalid_session_id.
4
+ // Future iPhone microphone captures share this store under their own phone_ IDs.
4
5
  export const MAC_ASK_SESSION = /^mac_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
5
- export const ASK_SESSION = /^(?:meeting_[A-Za-z0-9_-]{1,89}|mac_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/
6
+ export const ASK_SESSION = /^(?:meeting_[A-Za-z0-9_-]{1,89}|(?:mac_|phone_(?:[0-9]{13}_)?)[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/
@@ -8,8 +8,8 @@
8
8
  // MU: 20/20 — fireflies 10, g2-training 9, unknown 1 (zero human-verified)
9
9
  //
10
10
  // `fireflies` labels come from meeting ATTENDEE METADATA, never acoustically
11
- // verified. That is exactly the chain that manufactured the phantom "Erick
12
- // Hernandez" profile: 3 fireflies seeds compounding to 18 via g2-training. Under
11
+ // verified. That is exactly the chain that manufactured the phantom "Evan
12
+ // Hartwell" profile: 3 fireflies seeds compounding to 18 via g2-training. Under
13
13
  // plain FIFO, a human correction could evict the single `manual` sample while
14
14
  // ten unverified metadata seeds sat untouched. Age is the wrong axis. Who
15
15
  // supplied the label is the right one.
@@ -21,6 +21,26 @@
21
21
  // protected from eviction but capped at half a profile, keeping typical-voice
22
22
  // mass in the centroid.
23
23
 
24
+ // 6.93.0: `mac-assign:<sessionId>` is a sample a person ASSIGNED from a Mac recording's
25
+ // `Voice N` (mac-voice-assignment.ts). Nobody listened to each sample, the audio is
26
+ // call or video playback through a digital tap rather than the wearer's microphone,
27
+ // and the voice can be a merge of two people (R1). So it ranks as `assisted`, is NOT a
28
+ // correction (no quota, no protection), never vouches for identity, and may only ever
29
+ // replace another Mac-assigned sample, never a G2 correction, g2-training or fireflies
30
+ // one. Retraction by session still works (training-audio-provenance.ts).
31
+ export const MAC_ASSIGN_SOURCE = 'mac-assign'
32
+ /** Most Mac-assigned samples one profile may carry. Past this a new one replaces the oldest Mac-assigned one. */
33
+ export const MAC_ASSIGN_PROFILE_CAP = 10
34
+
35
+ /**
36
+ * Most Mac-assigned samples a profile holding these sources may carry: the flat cap, and never more than the
37
+ * profile's OTHER samples. Playback audio from a call must not outnumber what was heard on the wearer's own
38
+ * microphone, so a profile with 2 other samples carries at most 2 Mac ones, not 10.
39
+ */
40
+ export function macAssignCapFor(sampleCount: number, macCount: number): number {
41
+ return Math.min(MAC_ASSIGN_PROFILE_CAP, Math.max(0, sampleCount - macCount))
42
+ }
43
+
24
44
  /** Ordered weakest-first: eviction walks this list and takes from the first
25
45
  * non-empty tier. */
26
46
  export const EVICTION_ORDER = ['automatic', 'unknown', 'metadata', 'assisted', 'human'] as const
@@ -47,6 +67,8 @@ export function provenanceTier(source: string | undefined | null): ProvenanceTie
47
67
  return 'human'
48
68
  // The identifier chose the label; a human approved the batch run.
49
69
  case 'g2-training':
70
+ // 6.93.0: a person picked the name for a Mac `Voice N`; nobody heard each sample.
71
+ case MAC_ASSIGN_SOURCE:
50
72
  return 'assisted'
51
73
  // Label came from attendee metadata and was never acoustically verified.
52
74
  case 'fireflies':
@@ -76,6 +98,12 @@ export function vouchesForIdentity(source: string | undefined | null): boolean {
76
98
  || head === 'g2-training' || head === 'correction' || head === 'ext-group'
77
99
  }
78
100
 
101
+ /** True for a sample added by assigning a Mac recording voice to a profile. */
102
+ export function isMacAssign(source: string | undefined | null): boolean {
103
+ return String(source ?? '').trim().toLowerCase().startsWith(`${MAC_ASSIGN_SOURCE}:`)
104
+ || String(source ?? '').trim().toLowerCase() === MAC_ASSIGN_SOURCE
105
+ }
106
+
79
107
  /** True for a sample created by a human correcting a specific meeting. */
80
108
  export function isCorrection(source: string | undefined | null): boolean {
81
109
  return String(source ?? '').trim().toLowerCase().startsWith('correction')
@@ -98,6 +126,9 @@ export interface EvictionChoice {
98
126
  tier: ProvenanceTier
99
127
  /** Human-readable justification, logged so an eviction is never silent. */
100
128
  reason: string
129
+ /** 6.93.0: the incoming sample may displace nothing here (a Mac-assigned sample into a full profile with no Mac-assigned
130
+ * sample to replace). The caller must refuse it; `index` is -1 and must not be dropped. */
131
+ refuse?: boolean
101
132
  }
102
133
 
103
134
  /**
@@ -117,6 +148,20 @@ export function chooseEviction(
117
148
  // fire because the quota floor is 1. Verified by mutation — an explicit
118
149
  // guard here is unreachable in effect.
119
150
  const correctionIdx = sources.map((s, i) => ({ s, i })).filter(x => isCorrection(x.s)).map(x => x.i)
151
+ const macIdx = sources.map((s, i) => ({ s, i })).filter(x => isMacAssign(x.s)).map(x => x.i)
152
+
153
+ // 6.93.0: a Mac-assigned sample replaces the oldest Mac-assigned one or is refused. It never takes a slot
154
+ // from a correction, g2-training, fireflies or any other sample, whatever the profile's mix.
155
+ if (isMacAssign(incomingSource)) {
156
+ if (macIdx.length > 0) {
157
+ return {
158
+ index: macIdx[0],
159
+ tier: 'assisted',
160
+ reason: `Mac-assigned samples at ${macIdx.length}/${macAssignCapFor(sources.length, macIdx.length)} or the profile is full, replacing the oldest Mac-assigned sample`,
161
+ }
162
+ }
163
+ return { index: -1, tier: 'assisted', reason: 'profile is full and holds no Mac-assigned sample to replace, refusing the Mac-assigned sample', refuse: true }
164
+ }
120
165
 
121
166
  // A correction arriving at or above quota replaces the OLDEST CORRECTION, not
122
167
  // a typical-voice sample. Corrections stay protected as a class while the
@@ -129,6 +174,19 @@ export function chooseEviction(
129
174
  }
130
175
  }
131
176
 
177
+ // A Mac-assigned sample is the weakest evidence a profile holds, so any other incoming sample displaces the oldest one
178
+ // BEFORE it displaces an `auto:`, unknown or `fireflies` sample (6.93.0 polish; the first cut ranked it only inside the
179
+ // assisted tier, which let it outlive all three). Why it is weaker than an `auto:` sample: that one was heard on the
180
+ // wearer's own microphone, the same channel identification runs on, while a Mac sample is call or video playback that
181
+ // a person labelled in bulk and nobody listened to. Reaching here means the profile is full or the Mac cap bound.
182
+ if (macIdx.length > 0) {
183
+ return {
184
+ index: macIdx[0],
185
+ tier: 'assisted',
186
+ reason: `dropping the oldest Mac-assigned sample (${String(sources[macIdx[0]])}) before any other kind, it is the weakest evidence`,
187
+ }
188
+ }
189
+
132
190
  for (const tier of EVICTION_ORDER) {
133
191
  const idx = sources.findIndex(s => provenanceTier(s) === tier)
134
192
  if (idx === -1) continue
@@ -269,7 +269,7 @@ export function fuzzyReplacementFor(word: string, canonical: string[], targetLow
269
269
  * AND mixed-case names like GitHub/OpenAI)
270
270
  * - Word must NOT be in COMMON_ENGLISH_WORDS stop-list (primary defense)
271
271
  * - Target must start with uppercase (canonical proper noun)
272
- * - Target token length ≥ 4 chars (allows short names like Anna, Ryan, Luke as targets)
272
+ * - Target token length ≥ 4 chars (allows short names like Anna, Ryan, Theo as targets)
273
273
  * - Length difference ≤ 2 chars
274
274
  * - Distance within length-scaled threshold:
275
275
  * 5-8 chars → 1