@gotcos/glasses-server 6.92.0 → 6.94.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.
- package/.env.example +30 -1
- package/CHANGELOG.md +72 -29
- package/README.md +70 -0
- package/docs/active-query-jobs.md +1 -1
- package/docs/test-plan-6.71.7.md +1 -1
- package/managed-runtime-contract.json +2 -0
- package/package.json +2 -1
- package/server/lib/ask-session-id.ts +2 -1
- package/server/lib/embedding-eviction.ts +60 -2
- package/server/lib/fuzzy-correct.ts +1 -1
- package/server/lib/g2-enrichment-runner.ts +7 -0
- package/server/lib/g2-ops-handoff.ts +204 -2
- package/server/lib/hallucination-filter.ts +1 -1
- package/server/lib/imported-library-rows.ts +4 -2
- package/server/lib/live-cos-ask.ts +3 -2
- package/server/lib/mac-live-voices.ts +57 -0
- package/server/lib/mac-recording-asr.ts +16 -8
- package/server/lib/mac-speaker-projection.ts +211 -0
- package/server/lib/mac-stream-diarizer.ts +185 -0
- package/server/lib/mac-voice-assignment.ts +361 -0
- package/server/lib/mac-voice-labels.ts +70 -8
- package/server/lib/mac-window-replay.ts +271 -0
- package/server/lib/mac-window-voices.ts +390 -0
- package/server/lib/meeting-actions.ts +1 -1
- package/server/lib/meeting-ask-owner.ts +4 -4
- package/server/lib/meeting-ask-transcript-sources.ts +41 -3
- package/server/lib/meeting-audio-archive.ts +4 -0
- package/server/lib/meeting-batch-transcribe.ts +7 -1
- package/server/lib/meeting-context-keys.ts +6 -3
- package/server/lib/meeting-corrections.ts +4 -4
- package/server/lib/meeting-fact-check.ts +3 -3
- package/server/lib/meeting-parse.ts +1 -1
- package/server/lib/meeting-relabel-enrolment.ts +8 -8
- package/server/lib/meeting-relabel.ts +3 -3
- package/server/lib/meeting-scribe-content.ts +1 -1
- package/server/lib/meeting-speaker-review.ts +8 -8
- package/server/lib/meeting-store.ts +19 -9
- package/server/lib/nemotron-final.ts +114 -9
- package/server/lib/nemotron-live.ts +58 -6
- package/server/lib/phone-recording.ts +114 -0
- package/server/lib/prompt-tail-guard.ts +1 -1
- package/server/lib/speaker-embeddings.ts +154 -28
- package/server/lib/speaker-name.ts +1 -1
- package/server/lib/stranded-session-actions.ts +7 -2
- package/server/lib/training-audio-provenance.ts +3 -2
- package/server/lib/transcript-cleanup-context.ts +3 -3
- package/server/lib/transcript-cleanup-model.ts +1 -1
- package/server/lib/transcript-cleanup-spans.ts +10 -10
- package/server/lib/transcript-cleanup-text.ts +4 -4
- package/server/lib/transcript-cleanup.ts +1 -1
- package/server/lib/unsaved-audio-quarantine.ts +10 -1
- package/server/lib/voice-directory.ts +1 -1
- package/server/lib/voice-evidence-guard.ts +2 -1
- package/server/lib/voice-profile-store.ts +11 -5
- package/server/lib/whisper-local.ts +1 -1
- package/server/routes/__fixtures__/g2-filing-golden-6.93.0.json +62 -0
- package/server/routes/health.ts +8 -0
- package/server/routes/meeting.ts +231 -20
- package/server/routes/meetings.ts +9 -5
- package/server/routes/transcribe-stream.ts +140 -15
- package/server/routes/voice.ts +12 -4
- package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +225 -0
- 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,46 @@
|
|
|
1
|
+
## 6.94.0 - Saved Mac recordings are filed into the COS meeting library
|
|
2
|
+
|
|
3
|
+
2026-10-27. Pairs with COS operations `sync_meetings.py` on branch `claude/mac-library-cos` (the `--mac-import-file` import and `--capabilities`). Works with COS Control 0.5.351; Control needs no change.
|
|
4
|
+
|
|
5
|
+
- **Why:** a saved Mac system-audio recording never reached `operations/`. `routes/meeting.ts` skipped the hand-off for every Mac capture (the `MAC_CAPTURE_ID` checks added with Mac capture in 6.73.x), and `sync_meetings.py` only recognised files whose Source is G2 Glasses. Mac meetings were missing from vector search, LightRAG and task extraction, and kept the save-time domain, which is `personal` whenever the profile defines no domain keywords. On 2026-10-27 the glasses copy of the Quilt QBR was filed under quilt while the Mac copy of the same meeting stayed a personal standalone recording.
|
|
6
|
+
- **What:** after a Mac recording finishes (its standalone summary first, unchanged), the server runs `sync_meetings.py --mac-import-file <recording> --quiet` with the same retry ladder and result line as the G2 hand-off. The pipeline stages it under `personal/meetings/YYYY-MM/`, runs the same summary, title and domain step a G2 recording gets, and moves it to the domain the model picks, titled with a `(Mac)` suffix. The filed copy keeps `Source: Mac System Audio`, `Capture source: mac_system_audio` and its `.g2-chunks.json` sidecar with `sourceType`, so `GET /api/meetings` shows one row for it (the operations row, keyed by the same `mac_` session) and Control still treats it as a Mac recording. The same path runs after orphan recovery of a Mac capture.
|
|
7
|
+
- **Only when the pipeline says it can:** the server reads `sync_meetings.py --capabilities` and files Mac recordings only when it reports both `mac_import: 1` and `mac_twins: 2` or later, cached per script version. An older `sync_meetings.py` has no `--capabilities`, is never spawned for it, and the recording stays standalone exactly as in 6.93.0. A script with the import but an older twin rule (`mac_twins: 1` knew only glasses twins) is treated the same way.
|
|
8
|
+
- **One meeting, recorded twice:** when the glasses or Fireflies (or Granola) also recorded the meeting, the pipeline files the Mac copy as a twin (`<!-- twin-of: <file> -->` in its markdown): it is searchable, but never queued for LightRAG, never scanned for task completions and never turned into Work. Twin means both the existing G2 time gates (60 minute window, overlap at least 0.50, duration ratio at least 0.50; a cloud scribe uses its own Date and Duration) and text-anchor agreement with the merge engine's pairing evidence (`auto_merge` tier: at least 30 shared phrases at a consistent offset and three times the next different meeting, glasses or cloud). A cloud scribe needs its word-timed Fireflies sentences to be scored; Granola scribes carry none. A glasses copy and a cloud scribe of the same call are one meeting, and the marker names the cloud scribe. Copies of one recording (a session filed twice, a capture and the scribe it was blended into) count once. The marker also carries the recording's identity (`g2:<sessionId>` or `fireflies:<id>`), so a retitled or moved target is followed. A twin is released only when its recording is absent on two checks at least 24 hours apart; a file that exists but cannot be read counts as unknown, never as gone, and a marker with no identity is never released. The marker names a cloud scribe only when that scribe itself carries the shared phrases. Overlapping time with different words is not a twin and is processed fully. Either recording may arrive first: a Mac recording with no twin yet is held from those consumers for 12 hours after it ends (`COS_MAC_TWIN_HOLD_HOURS`), and filing the glasses copy or saving the cloud scribe marks it. `mac_twins.py release <file>` re-admits a twin, idempotently and safely across a crash. 13 of the 17 Mac recordings saved in October are twins.
|
|
9
|
+
- **Never a G2 file:** a Mac or iPhone recording is refused by every G2 path. `patchRecordingForG2Pipeline` refuses to relabel one as G2 Glasses, `stageRecordingIntoOperations` refuses a Mac or iPhone sidecar, and `handoffMeetingToOperations` refuses one before spawning anything. On the COS side the G2 import, the G2 sync, the retroactive blend, `g2_blend_backfill.py` and `meeting_dedup_cleanup.py` skip any sidecar whose source is Mac or iPhone, including one that carries speaker names. The merge engine already excluded Mac captures, and the merge trigger still never fires for one.
|
|
10
|
+
- **Failure:** filing starts only after the capture's own finalization (or orphan recovery) is complete and its job or lease released, so it holds no maintenance lease (a restart drain never waits on it) and a restart mid-filing never re-runs the standalone summary. A filing that fails is logged and never fails the capture. If the pipeline's model step fails, the staged copy stays held (out of LightRAG and Work, untitled titles never settle into Work) and `sync_meetings.py` exits 2 so the server retries; the hourly sync then retries it at most 3 times (2 per run, `COS_MAC_IMPORT_RETRY=0` turns that off), and `mac_library_backfill.py` files it on demand. A hand-off that never reached the pipeline (three attempts that all wait out the sync lock, or a restart before staging) is caught by the hourly sync, which files any finished Mac capture of the last two days the library does not hold, inside the same per-run budget.
|
|
11
|
+
- **Idempotent:** a re-save of a saved capture answers its receipt and files nothing; the pipeline finds a capture already in the library by session id, whatever it was renamed or moved to, and refuses to overwrite a file that belongs to another capture.
|
|
12
|
+
- **Existing recordings:** `operations/scripts/mac_library_backfill.py` lists every Mac recording in `~/.cos-glasses/data/recordings` with its staging destination and a keyword domain guess (dry run is the default), and `--apply` files each one through the same `--mac-import-file` command.
|
|
13
|
+
- **Unchanged:** a G2 save files byte for byte as in 6.93.0 (pinned by `server/routes/g2-filing-golden.test.ts` against a fixture recorded on 6.93.0, for both the locked import and an older sync script). The local Mac recording is written exactly as before. Two recordings of one meeting (glasses and Mac) are filed as two meetings; merging them is later work.
|
|
14
|
+
- **Publish order:** merge the COS `sync_meetings.py` change first, then publish this server. Either alone is safe: this server against the older script leaves Mac recordings standalone, and the new script does nothing until a server or the backfill asks.
|
|
15
|
+
|
|
16
|
+
## 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
|
|
17
|
+
|
|
18
|
+
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).
|
|
19
|
+
|
|
20
|
+
- **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.
|
|
21
|
+
- **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.
|
|
22
|
+
- **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`.
|
|
23
|
+
- **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).
|
|
24
|
+
- **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.
|
|
25
|
+
- **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`.
|
|
26
|
+
- **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`).
|
|
27
|
+
- **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.
|
|
28
|
+
- **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.
|
|
29
|
+
- **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.
|
|
30
|
+
- **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.
|
|
31
|
+
- **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).
|
|
32
|
+
- **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.
|
|
33
|
+
- **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.
|
|
34
|
+
|
|
35
|
+
## 6.92.1 - Conservative anonymous voice continuity for live Mac meetings
|
|
36
|
+
|
|
37
|
+
- 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.
|
|
38
|
+
- Private continuity anchors survive capture recovery, reject duplicate audio as independent support, and never enter the global profile store or public DTOs.
|
|
39
|
+
- Finalization preserves established live voice numbers and withholds channel mappings that contradict mixed live turns. Mac meeting-only renames still do not train profiles.
|
|
40
|
+
- 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.
|
|
41
|
+
- 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.
|
|
42
|
+
- 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`.
|
|
43
|
+
|
|
1
44
|
## 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
45
|
|
|
3
46
|
2026-10-25, plan r3 ("dictation parity", Wave A; pairs with COS Control 0.5.345). Zero `claude -p` calls: every
|
|
@@ -750,7 +793,7 @@ dictation is honored by the AI polish, and a mapping like that works from then o
|
|
|
750
793
|
never a line break or a spaced dash,
|
|
751
794
|
case-insensitive, with punctuation around it ("pricing,"). A key goes ahead of any earlier key whose words it
|
|
752
795
|
contains, so "forward slash build and price" is not cut short by "forward slash build"; chains a profile relies on
|
|
753
|
-
("Myles", then "Miles
|
|
796
|
+
("Myles", then "Miles Ukmya") keep their order. A written form is inserted literally ("$&" is not a pattern).
|
|
754
797
|
Same pass on every path that already ran it: dictation finalize, prompt drafts, G2 live lines, meeting batch
|
|
755
798
|
transcripts, previews. It runs before the AI polish.
|
|
756
799
|
- **Spoken paths, deterministic.** For messages and prompt drafts (never meeting lines), "forward slash" plus one word
|
|
@@ -1153,10 +1196,10 @@ Pairs with COS Glasses companion 6.10.645. Includes 6.77.0. No COS Control chang
|
|
|
1153
1196
|
|
|
1154
1197
|
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
1198
|
|
|
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 ("
|
|
1199
|
+
- 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
1200
|
- 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
|
|
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 (
|
|
1201
|
+
- 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.
|
|
1202
|
+
- 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
1203
|
- 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
1204
|
- 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
1205
|
- 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 +1215,7 @@ Includes 6.76.2. Off until Miles turns it on. No COS Control or companion change
|
|
|
1172
1215
|
- 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
1216
|
- 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
1217
|
- 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
|
|
1218
|
+
- 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
1219
|
|
|
1177
1220
|
## 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
1221
|
|
|
@@ -1187,7 +1230,7 @@ Pairs with COS Glasses companion 6.10.644 and COS Control 0.5.322. Includes 6.76
|
|
|
1187
1230
|
- 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
1231
|
- 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
1232
|
- 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-
|
|
1233
|
+
- 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
1234
|
|
|
1192
1235
|
## 6.76.1 - Stop a glasses meeting from COS Control
|
|
1193
1236
|
|
|
@@ -1201,10 +1244,10 @@ Pairs with COS Glasses companion 6.10.644 and COS Control 0.5.321. Includes 6.76
|
|
|
1201
1244
|
- 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
1245
|
## 6.76.0 - Live COS Ask knows who owns this COS, looks in COS for person questions, and times every phase
|
|
1203
1246
|
|
|
1204
|
-
- Field report (
|
|
1247
|
+
- 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
1248
|
- 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 (
|
|
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
|
|
1249
|
+
- 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.
|
|
1250
|
+
- 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
1251
|
- 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
1252
|
- 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
1253
|
- 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 +1783,13 @@ Local hardening candidate; stable promotion remains gated on native UI and G2 ac
|
|
|
1740
1783
|
|
|
1741
1784
|
### 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
1785
|
|
|
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
|
|
1786
|
+
- 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
1787
|
- **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
1788
|
- **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
1789
|
- **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
1790
|
- **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
1791
|
- **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
|
|
1792
|
+
- 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
1793
|
- No Install hooks and no COS Control change.
|
|
1751
1794
|
|
|
1752
1795
|
## 6.61.0
|
|
@@ -1766,7 +1809,7 @@ Local hardening candidate; stable promotion remains gated on native UI and G2 ac
|
|
|
1766
1809
|
- **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
1810
|
- **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
1811
|
- **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
|
|
1812
|
+
- **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
1813
|
- **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
1814
|
- **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
1815
|
- **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 +1819,11 @@ Local hardening candidate; stable promotion remains gated on native UI and G2 ac
|
|
|
1776
1819
|
- **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
1820
|
- **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
1821
|
- **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
|
-
-
|
|
1822
|
+
- 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
1823
|
- 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
1824
|
- 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
1825
|
- 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:
|
|
1826
|
+
- 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
1827
|
- 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
1828
|
- 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
1829
|
- **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 +2365,7 @@ The phone can list the skills this Mac will actually run.
|
|
|
2322
2365
|
|
|
2323
2366
|
- `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
2367
|
- 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 (
|
|
2368
|
+
- 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
2369
|
- 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
2370
|
- 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
2371
|
|
|
@@ -2391,7 +2434,7 @@ Voice profiles named after a spoken sentence are repaired at load.
|
|
|
2391
2434
|
`Unnamed voice N`, keeps every embedding and its provenance, records
|
|
2392
2435
|
`renamedFrom` (the first 60 characters) and `needsName: true`, and counts it
|
|
2393
2436
|
in the load repairs (`profilesRenamed`). Legacy short labels ("MU",
|
|
2394
|
-
"Speaker 2", "
|
|
2437
|
+
"Speaker 2", "Theo M.") are never touched. `GET /api/voice/profiles` carries
|
|
2395
2438
|
`needsName` and `renamedFrom` so COS Control's speaker review can offer the
|
|
2396
2439
|
rename; `POST /api/voice/merge-profiles` folds the placeholder into the
|
|
2397
2440
|
right person as before.
|
|
@@ -2796,7 +2839,7 @@ The brief is waiting before you ask for it.
|
|
|
2796
2839
|
|
|
2797
2840
|
A start-of-day brief now runs on a schedule inside the server and lands in
|
|
2798
2841
|
the inbox as an ordinary numbered reply — no prompt, no "run the good-morning
|
|
2799
|
-
skill", no phone awake at 07:00.
|
|
2842
|
+
skill", no phone awake at 07:00. Lin Keats Lee's first-week note said it
|
|
2800
2843
|
plainly: he deployed COS to his G2 expecting a push at the start of the day and
|
|
2801
2844
|
nothing came, because until now the only thing that could start a brief was a
|
|
2802
2845
|
person typing. The server is the one process awake at that hour, so it owns
|
|
@@ -3849,16 +3892,16 @@ bring the leftover cards back.
|
|
|
3849
3892
|
Enrolment after `POST /relabel` only fired when `from` was a placeholder (`Ext`,
|
|
3850
3893
|
`Unknown`, `Unidentified N`). The live path in Speakers review is the other one:
|
|
3851
3894
|
the identifier weakly matches someone already enrolled, and the reviewer says
|
|
3852
|
-
**This is someone else → Use "
|
|
3853
|
-
`meeting_1787234635703_t4iz74`:
|
|
3854
|
-
`applied`, 78 profiles, no
|
|
3895
|
+
**This is someone else → Use "Leo Ashby"**. Measured 2026-08-20 on
|
|
3896
|
+
`meeting_1787234635703_t4iz74`: Cole Garner → Leo Ashby, 19 chunks, ledger
|
|
3897
|
+
`applied`, 78 profiles, no Leo. Backfill used the same guard, so the meeting
|
|
3855
3898
|
could not be enrolled after the fact either (`eligible: 0`, `skippedNamedSource: 1`).
|
|
3856
3899
|
- **Enrol by target, not by source.** `enrolNamedVoice` still skips a placeholder
|
|
3857
3900
|
`to` and an empty `changed` list. A real `to` enrols those chunks — creates the
|
|
3858
3901
|
profile when it does not exist, appends when it does — through the same raw-index
|
|
3859
3902
|
map, coherence gate, 20-sample cap, and `correction:<sessionId>` tag. Global fold
|
|
3860
3903
|
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
|
|
3904
|
+
that. Mutating the old `from`-placeholder guard back in fails the new Cole → Leo
|
|
3862
3905
|
test.
|
|
3863
3906
|
|
|
3864
3907
|
## 6.36.16
|
|
@@ -4239,7 +4282,7 @@ bring the leftover cards back.
|
|
|
4239
4282
|
- **`POST /api/meeting/:sessionId/backfill-enrolment`** — train a profile from a voice
|
|
4240
4283
|
that was named BEFORE enrolment shipped. Those meetings have a correct transcript and
|
|
4241
4284
|
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
|
|
4285
|
+
there, so the placeholder guard correctly declines. The live case is Wren Hale —
|
|
4243
4286
|
60 and 109 chunks across two meetings, 182 sidecar mentions, absent from a 77-profile
|
|
4244
4287
|
store.
|
|
4245
4288
|
- The correction LEDGER already holds what enrolment needs: the original `from`, the
|
|
@@ -4248,8 +4291,8 @@ bring the leftover cards back.
|
|
|
4248
4291
|
coherence, the diversity cap and the `correction:<sessionId>` tag all apply
|
|
4249
4292
|
identically. No second implementation to drift.
|
|
4250
4293
|
- **Named-source corrections are excluded, by the existing rule rather than a new one.**
|
|
4251
|
-
`Ext ->
|
|
4252
|
-
fix, and training on it would put
|
|
4294
|
+
`Ext -> Wren` is training data; `Avery Winters -> Wren` is a mis-attribution
|
|
4295
|
+
fix, and training on it would put Avery's voice into Wren's profile. Both rows
|
|
4253
4296
|
are reported; only the placeholder one attempts anything. Mutation-verified: dropping
|
|
4254
4297
|
the placeholder guard fails that test.
|
|
4255
4298
|
- **Runs in-process, which is the point.** The voice store is owned by the running
|
|
@@ -4332,11 +4375,11 @@ bring the leftover cards back.
|
|
|
4332
4375
|
## 6.27.10
|
|
4333
4376
|
- **Naming an unidentified voice now creates a real profile.** `POST /api/meeting/:id/relabel`
|
|
4334
4377
|
was TEXT ONLY: it rewrote `speaker` strings in the sidecar and never touched the
|
|
4335
|
-
voice store. Saving "
|
|
4378
|
+
voice store. Saving "Wren Hale" over 109 segments labelled that one meeting and
|
|
4336
4379
|
taught the system nothing — she never appeared in `/api/voice/profiles`, the review
|
|
4337
4380
|
panel still offered her as `new name` inside the SAME meeting, no later meeting
|
|
4338
4381
|
could match her, and there was no profile to add further chunks against.
|
|
4339
|
-
Verified on the live store: 77 profiles, no
|
|
4382
|
+
Verified on the live store: 77 profiles, no Wren Hale, while both sidecars
|
|
4340
4383
|
carried her name.
|
|
4341
4384
|
- The server already had `/api/voice/enroll-ext` for exactly this and the naming flow
|
|
4342
4385
|
never called it. Relabel now enrols the embeddings of the chunks it actually
|
|
@@ -5231,7 +5274,7 @@ nothing she could name would work.
|
|
|
5231
5274
|
|
|
5232
5275
|
Worth recording for the next person who tests this: only the WORD-TIMING path
|
|
5233
5276
|
can overflow. Measured, the chunk-estimate path credits a contested second to
|
|
5234
|
-
one speaker (two speakers at identical timestamps gave MU 273s and
|
|
5277
|
+
one speaker (two speakers at identical timestamps gave MU 273s and Mira 0s), so
|
|
5235
5278
|
a chunk-sourced meeting can never trip the note.
|
|
5236
5279
|
|
|
5237
5280
|
## 6.21.29
|
|
@@ -5257,7 +5300,7 @@ without the floor.
|
|
|
5257
5300
|
cosine floor, a human typing the name, and the wearer exemption (identity comes
|
|
5258
5301
|
from holding the device, not from a score). The route carried `isOwner` and
|
|
5259
5302
|
`confirmedByHuman` and discarded both. The output now names the warrant per
|
|
5260
|
-
person: `"MU" by wearing the device; "
|
|
5303
|
+
person: `"MU" by wearing the device; "Mira Orlen" by voice match; "Theo Marsh"
|
|
5261
5304
|
because a human named that voice`. Labels are quoted, so a typed "Smith, John"
|
|
5262
5305
|
cannot read as two people, and the caveat now covers different SPELLINGS of a
|
|
5263
5306
|
confirmed name (labels are `MU` while the prose says "the user").
|
|
@@ -5517,7 +5560,7 @@ Ditto meeting and found eleven attributed voices, most of them wrong.
|
|
|
5517
5560
|
|
|
5518
5561
|
- A NAME MUST BE EARNED. `identifySpeaker` accepts a match at 0.55, so one
|
|
5519
5562
|
segment could arrive in the review panel wearing somebody's full name — Richard
|
|
5520
|
-
|
|
5563
|
+
Jennings on 1 segment at 0.60, Theo Marsh on 1 at 0.55. Voices now carry
|
|
5521
5564
|
`nameAsserted` plus `assertionBlockers`; below the floor a client must render
|
|
5522
5565
|
"unidentified" and offer the label only as a scored candidate. Floors:
|
|
5523
5566
|
similarity >= 0.65 AND >= 3 segments AND not thrashing. The owner gets no
|
package/README.md
CHANGED
|
@@ -654,6 +654,76 @@ 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
|
+
|
|
715
|
+
### Mac recordings in the COS meeting library (6.94.0)
|
|
716
|
+
|
|
717
|
+
With COS operations configured (`COS_SCRIPTS_DIR`), a saved Mac recording is filed
|
|
718
|
+
into `operations/{domain}/meetings/YYYY-MM/` after its standalone summary, through
|
|
719
|
+
`sync_meetings.py --mac-import-file`: the same summary, title and domain step as a
|
|
720
|
+
glasses meeting, titled `... (Mac)`, never matched or blended into a Fireflies or
|
|
721
|
+
Granola meeting. The server checks `sync_meetings.py --capabilities` first (it needs
|
|
722
|
+
`mac_import` and `mac_twins` 2); an older script leaves Mac recordings standalone. A Mac
|
|
723
|
+
recording of a meeting the glasses or Fireflies also recorded is filed as a twin:
|
|
724
|
+
searchable, but kept out of LightRAG, task scans and Work (`operations/scripts/mac_twins.py`). Recordings saved before 6.94.0 are filed with
|
|
725
|
+
`operations/scripts/mac_library_backfill.py` (dry run by default, `--apply` to file).
|
|
726
|
+
|
|
657
727
|
## Speech-to-text settings (6.72.0)
|
|
658
728
|
|
|
659
729
|
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
|
|
46
|
+
"title": "What did Gordon say about the October webinar numbers?"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"jobId": "9b2a7d40-1c3e-4f58-8e2d-6a0c4b9e7f21",
|
package/docs/test-plan-6.71.7.md
CHANGED
|
@@ -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:
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.94.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})$/
|