@gotcos/glasses-server 6.91.1 → 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 (73) hide show
  1. package/.env.example +64 -2
  2. package/CHANGELOG.md +154 -29
  3. package/README.md +90 -1
  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 +10 -1
  7. package/package.json +2 -1
  8. package/server/lib/ask-session-id.ts +2 -1
  9. package/server/lib/claude-circuit.ts +8 -4
  10. package/server/lib/claude-text-runner.ts +113 -0
  11. package/server/lib/dictation-fillers-fixture-cases.ts +214 -0
  12. package/server/lib/dictation-fillers.ts +30 -0
  13. package/server/lib/dictation-meaning-guard.ts +47 -0
  14. package/server/lib/dictation-polish-pipeline.ts +90 -12
  15. package/server/lib/embedding-eviction.ts +60 -2
  16. package/server/lib/fuzzy-correct.ts +1 -1
  17. package/server/lib/hallucination-filter.ts +1 -1
  18. package/server/lib/imported-library-rows.ts +4 -2
  19. package/server/lib/live-cos-ask.ts +3 -2
  20. package/server/lib/mac-live-voices.ts +57 -0
  21. package/server/lib/mac-recording-asr.ts +16 -8
  22. package/server/lib/mac-speaker-projection.ts +211 -0
  23. package/server/lib/mac-stream-diarizer.ts +185 -0
  24. package/server/lib/mac-voice-assignment.ts +361 -0
  25. package/server/lib/mac-voice-labels.ts +70 -8
  26. package/server/lib/mac-window-replay.ts +271 -0
  27. package/server/lib/mac-window-voices.ts +390 -0
  28. package/server/lib/meeting-actions.ts +1 -1
  29. package/server/lib/meeting-ask-owner.ts +4 -4
  30. package/server/lib/meeting-ask-transcript-sources.ts +41 -3
  31. package/server/lib/meeting-audio-archive.ts +4 -0
  32. package/server/lib/meeting-batch-transcribe.ts +7 -1
  33. package/server/lib/meeting-context-keys.ts +6 -3
  34. package/server/lib/meeting-corrections.ts +4 -4
  35. package/server/lib/meeting-fact-check.ts +3 -3
  36. package/server/lib/meeting-parse.ts +1 -1
  37. package/server/lib/meeting-relabel-enrolment.ts +8 -8
  38. package/server/lib/meeting-relabel.ts +3 -3
  39. package/server/lib/meeting-scribe-content.ts +1 -1
  40. package/server/lib/meeting-speaker-review.ts +8 -8
  41. package/server/lib/meeting-store.ts +19 -9
  42. package/server/lib/nemotron-final.ts +114 -9
  43. package/server/lib/nemotron-live.ts +58 -6
  44. package/server/lib/phone-recording.ts +114 -0
  45. package/server/lib/prompt-edit.ts +84 -3
  46. package/server/lib/prompt-tail-guard.ts +1 -1
  47. package/server/lib/speaker-embeddings.ts +154 -28
  48. package/server/lib/speaker-name.ts +1 -1
  49. package/server/lib/stranded-session-actions.ts +7 -2
  50. package/server/lib/token-audit.ts +6 -1
  51. package/server/lib/training-audio-provenance.ts +3 -2
  52. package/server/lib/transcript-cleanup-context.ts +3 -3
  53. package/server/lib/transcript-cleanup-model.ts +1 -1
  54. package/server/lib/transcript-cleanup-spans.ts +10 -10
  55. package/server/lib/transcript-cleanup-text.ts +4 -4
  56. package/server/lib/transcript-cleanup.ts +1 -1
  57. package/server/lib/unsaved-audio-quarantine.ts +10 -1
  58. package/server/lib/voice-directory.ts +1 -1
  59. package/server/lib/voice-evidence-guard.ts +2 -1
  60. package/server/lib/voice-profile-store.ts +11 -5
  61. package/server/lib/whisper-local.ts +1 -1
  62. package/server/routes/dictation-polish-settings.ts +8 -1
  63. package/server/routes/health.ts +8 -0
  64. package/server/routes/meeting.ts +189 -19
  65. package/server/routes/meetings.ts +9 -5
  66. package/server/routes/prompt-drafts.ts +3 -2
  67. package/server/routes/prompt-edit.ts +47 -5
  68. package/server/routes/transcribe-stream.ts +140 -15
  69. package/server/routes/voice.ts +12 -4
  70. package/server/scripts/dictation-parity-6920-mutation-gate.mjs +229 -0
  71. package/server/scripts/generate-dictation-fillers-fixture.ts +18 -0
  72. package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +225 -0
  73. package/server/scripts/mac-window-voices-6930-mutation-gate.mjs +193 -0
@@ -42,9 +42,12 @@ export interface TokenAuditEntry extends TokenAuditKeyFields {
42
42
  polishRegister?: string
43
43
  /** 6.91.0, dictation polish only: the prompt that ran (v3 or v4). */
44
44
  polishPrompt?: string
45
- /** 6.91.0, dictation polish only: what became of the model's answer (polished, unchanged, guarded) and the guard kind. */
45
+ /** 6.91.0, dictation polish only: what became of the model's answer (polished, unchanged, guarded) and the guard kind.
46
+ * 6.92.0: `aborted` (audit only, never in a finalize answer) when the client went away mid-call. */
46
47
  polishOutcome?: string
47
48
  polishGuard?: string
49
+ /** 6.92.0, prompt edit only: ok, failed or aborted. */
50
+ promptEditOutcome?: 'ok' | 'failed' | 'aborted'
48
51
  }
49
52
 
50
53
  /**
@@ -86,6 +89,8 @@ export function buildTokenAuditRecord(entry: TokenAuditEntry, now: Date = new Da
86
89
  ...(entry.polishPrompt ? { polish_prompt: entry.polishPrompt } : {}),
87
90
  ...(entry.polishOutcome ? { polish_outcome: entry.polishOutcome } : {}),
88
91
  ...(entry.polishGuard ? { polish_guard: entry.polishGuard } : {}),
92
+ // 6.92.0: set only by the prompt-edit caller.
93
+ ...(entry.promptEditOutcome ? { prompt_edit_outcome: entry.promptEditOutcome } : {}),
89
94
  }
90
95
  }
91
96
 
@@ -5,7 +5,7 @@
5
5
  // those segments into that person's profile — so the wrong voice is now part of
6
6
  // what the system thinks they sound like, and it will keep matching. Removing the
7
7
  // label without removing the samples fixes the transcript and leaves the profile
8
- // poisoned. That is the mechanism behind the phantom "Erick Hernandez": three
8
+ // poisoned. That is the mechanism behind the phantom "Evan Hartwell": three
9
9
  // mislabelled seeds compounded to eighteen samples through self-training.
10
10
  //
11
11
  // WHAT CAN AND CANNOT BE TRACED. Provenance strings differ in whether they name
@@ -13,6 +13,7 @@
13
13
  //
14
14
  // auto:<sessionId> traceable — autoEnroll stamps it
15
15
  // correction:<sessionId> traceable — the relabel ledger stamps it
16
+ // mac-assign:<sessionId> traceable, a Mac recording voice assigned to a profile (6.93.0)
16
17
  // g2-training:<sessionId> traceable ONLY from 2026-08-06 onward (see below)
17
18
  // g2-training NOT traceable — every sample written before that
18
19
  // fireflies / manual / … NOT traceable — no session concept
@@ -49,7 +50,7 @@ export function trainingSourceFor(filename: string): string {
49
50
  export function isSampleFromSession(source: string | undefined | null, sessionId: string): boolean {
50
51
  const s = String(source ?? '')
51
52
  if (!sessionId) return false
52
- for (const prefix of ['auto:', 'correction:', 'g2-training:', 'ext-group:']) {
53
+ for (const prefix of ['auto:', 'correction:', 'g2-training:', 'ext-group:', 'mac-assign:']) {
53
54
  if (s.startsWith(prefix) && s.slice(prefix.length) === sessionId) return true
54
55
  }
55
56
  return false
@@ -10,8 +10,8 @@
10
10
  // SCREEN AWARENESS. On-screen text lines COS Control recorded during the meeting
11
11
  // (transcript-cleanup-screen.ts), shown beside each passage by time.
12
12
  //
13
- // TERMS. Every source contributes terms: capitalised words and runs of them ("Grace
14
- // Ukaoma", "Jewel360"). Terms are the per-meeting vocabulary: deterministic correction
13
+ // TERMS. Every source contributes terms: capitalised words and runs of them ("Faith
14
+ // Ukamya", "Jewel360"). Terms are the per-meeting vocabulary: deterministic correction
15
15
  // may write a term from this meeting's screen or files, the model may write any term,
16
16
  // and a term the model writes that came from the meeting (not the dictionary) becomes a
17
17
  // dictionary SUGGESTION, never an automatic addition.
@@ -86,7 +86,7 @@ export function extractTerms(text: string, max: number = CONTEXT_LIMITS.termsPer
86
86
  const w = words[i].w
87
87
  const capital = /^\p{Lu}/u.test(w)
88
88
  if (looksLikeName(w)) out.add(w)
89
- // "Grace Ukaoma": a capitalised word directly before a name-like word, with nothing but a space between.
89
+ // "Faith Ukamya": a capitalised word directly before a name-like word, with nothing but a space between.
90
90
  if (capital && i + 1 < words.length && text.slice(words[i].end, words[i + 1].at) === ' ' && looksLikeName(words[i + 1].w)) {
91
91
  out.add(`${w} ${words[i + 1].w}`)
92
92
  }
@@ -5,7 +5,7 @@
5
5
  // servers, no hooks, an empty scratch workspace, a pinned model id, low effort, a
6
6
  // bounded output budget. Never a direct SDK. The doubtful spans of a meeting go in ONE
7
7
  // batched prompt; the answer is strict JSON, one entry per span id:
8
- // {"edits":[{"id":"S1","action":"keep"},{"id":"S2","action":"replace","text":"Grace Ukaoma"}]}
8
+ // {"edits":[{"id":"S1","action":"keep"},{"id":"S2","action":"replace","text":"Faith Ukamya"}]}
9
9
  //
10
10
  // THE GUARDS. Every replacement is checked before anything is applied. It is rejected if
11
11
  // unknown_span its id was not sent
@@ -16,10 +16,10 @@
16
16
  // near_name a word the engine was unsure of (below 0.9) that is a capitalised word
17
17
  // one edit past fuzzy-correct's limit from a known name, or that sounds
18
18
  // like one (same consonant skeleton and onset); two or three short
19
- // adjacent pieces joined ("U coma"), none a function word; or the
20
- // leading-y rule at any confidence ("Yukoma" for Ukaoma)
19
+ // adjacent pieces joined ("U cmya"), none a function word; or the
20
+ // leading-y rule at any confidence ("Yukmya" for Ukamya)
21
21
  // oov_capitalized a capitalised word mid-sentence that is no known name and no common
22
- // word, within two words of a known name ("Grace Eucoma")
22
+ // word, within two words of a known name ("Faith Eucmya")
23
23
  // repeat a phrase of 2 to 4 words (or one long word) said three or more
24
24
  // times in a row: the classic decoder loop
25
25
  // Flagged words one word apart merge; a span holds at most 6 words (12 for a repeat)
@@ -104,7 +104,7 @@ const POSSESSIVE = /(['’]s)$/i
104
104
  * attendees as targets Slack still became Black twice; they reach the model as context
105
105
  * and drive near-name spans instead), and the curated dictionary and the owner
106
106
  * ("strong" targets) rewrite only a word the engine was unsure of. With those rules the
107
- * 68 meetings got 27 fixes, all real (Manuj to Manoj 9 times, MarketPOS to MarktPOS).
107
+ * 68 meetings got 27 fixes, all real (Mohun to Mohan 9 times, MarketPOS to MarktPOS).
108
108
  * - a term from THIS meeting's screen or files (the per-meeting vocabulary) may rewrite a
109
109
  * capitalised word at any confidence: it is what was on screen while it was said.
110
110
  * Never into an inflection of the word, and a word that only opens a sentence only when the
@@ -159,11 +159,11 @@ interface NearInput { key: string; capitalized: boolean; probability?: number; j
159
159
 
160
160
  /**
161
161
  * The known name `key` is plausibly a mishearing of, or null.
162
- * - the leading-y rule (meeting-ask-owner.ts): "Yukoma" is one edit from "Ukaoma" once its y goes
162
+ * - the leading-y rule (meeting-ask-owner.ts): "Yukmya" is one edit from "Ukamya" once its y goes
163
163
  * - otherwise only for a word the engine was unsure of (below NEAR_NAME_UNSURE, or unscored):
164
164
  * a capitalised single word one edit past fuzzy-correct's own limit, or
165
165
  * any word (or short joined pieces) that sounds like the name (soundsAlike)
166
- * - joined pieces may also be within fuzzy-correct's own limit ("U coma" read as one word)
166
+ * - joined pieces may also be within fuzzy-correct's own limit ("U cmya" read as one word)
167
167
  */
168
168
  function nearName(input: NearInput, targets: readonly NameTarget[], known: ReadonlySet<string>): string | null {
169
169
  const { key } = input
@@ -174,7 +174,7 @@ function nearName(input: NearInput, targets: readonly NameTarget[], known: Reado
174
174
  for (const t of targets) {
175
175
  if (Math.abs(t.key.length - key.length) > 2) continue
176
176
  const d = levenshtein(key, t.key)
177
- // A single word spelled exactly like a name is the name. Pieces that join into one exactly ("Uka oma") are the
177
+ // A single word spelled exactly like a name is the name. Pieces that join into one exactly ("Uka mya") are the
178
178
  // strongest sign of a split name there is.
179
179
  if (d === 0 && !input.join) return null
180
180
  // "setting" next to a "Settings" label is the same word, not a misheard name.
@@ -260,12 +260,12 @@ export function selectSpans(
260
260
  if (near) flag(t.i, 'near_name', near)
261
261
  }
262
262
  }
263
- // Two or three adjacent words joined: "U coma", "Yu ko ma". One of them must be short (a split fragment).
263
+ // Two or three adjacent words joined: "U cmya", "Yu ko ma". One of them must be short (a split fragment).
264
264
  for (let i = 0; i + 1 < tokens.length; i++) {
265
265
  for (const width of [2, 3]) {
266
266
  const group = tokens.slice(i, i + width)
267
267
  if (group.length < width || group.some(g => g.line !== tokens[i].line || !g.key || fixed.has(g.i) || known.has(g.key))) continue
268
- // A split name is short pieces ("U coma", "Yu ko ma"); a long word plus a short one is just two words,
268
+ // A split name is short pieces ("U cmya", "Yu ko ma"); a long word plus a short one is just two words,
269
269
  // and a function word is never a piece of a name.
270
270
  if (!group.some(g => g.key.length <= 3) || group.some(g => g.key.length > 5 || FUNCTION_WORDS.has(g.key))) continue
271
271
  const joined = group.map(g => g.key).join('')
@@ -303,7 +303,7 @@ export function selectSpans(
303
303
  while (k < flagged.length) {
304
304
  const next = flagged[k]
305
305
  if (tokens[next].line !== tokens[from].line || next - to > 1 || next + 1 - from > SPAN_LIMITS.maxSpanWords) break
306
- // Never across a clause: "Yukoma and eucoma, then U coma" is two passages, not one.
306
+ // Never across a clause: "Yukmya and eucmya, then U cmya" is two passages, not one.
307
307
  if (tokens.slice(to - 1, next).some(t => /[,.;:!?]["')\]]*$/.test(t.raw))) break
308
308
  if ([...Array(next - to).keys()].some(g => repeatTokens.has(to + g))) break
309
309
  for (const r of flags.get(next)!.reasons) reasons.add(r)
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // Tokens are whitespace runs inside a line's text. Each carries its `core`: the token
8
8
  // without leading and trailing punctuation, which is what is compared, flagged and
9
- // replaced, so "Ukoma," becomes "Ukaoma," and the comma stays exactly where it was.
9
+ // replaced, so "Ukmya," becomes "Ukamya," and the comma stays exactly where it was.
10
10
  //
11
11
  // No IO and no model here.
12
12
 
@@ -156,8 +156,8 @@ export function alignWordInfo(tokens: Token[], words: readonly WordInfo[]): numb
156
156
  }
157
157
 
158
158
  /**
159
- * A consonant skeleton: how a word sounds with its vowels unsure. Ukaoma, Yukoma,
160
- * eucoma and "U coma" all read "km". Only ever used together with an onset check and
159
+ * A consonant skeleton: how a word sounds with its vowels unsure. Ukamya, Yukmya,
160
+ * eucmya and "U cmya" all read "km". Only ever used together with an onset check and
161
161
  * a length bound, because a two-letter skeleton alone matches far too much.
162
162
  */
163
163
  export function skeleton(word: string): string {
@@ -168,7 +168,7 @@ export function skeleton(word: string): string {
168
168
  return s.replace(/(.)\1+/g, '$1')
169
169
  }
170
170
 
171
- /** Vowel onset, counting a leading y before a vowel ("Yukoma") as a vowel. */
171
+ /** Vowel onset, counting a leading y before a vowel ("Yukmya") as a vowel. */
172
172
  export function vowelOnset(word: string): boolean {
173
173
  const f = fold(word)
174
174
  return /^[aeiou]/.test(f) || /^y[aeiou]/.test(f)
@@ -405,7 +405,7 @@ export function recordSuggestions(applied: readonly AppliedEdit[], pack: Context
405
405
  for (const edit of applied) {
406
406
  const heardKeys = new Set([...edit.original.matchAll(WORDS)].map(m => fold(m[0])))
407
407
  const ws = [...edit.replacement.matchAll(WORDS)].map(m => m[0])
408
- // Longest first: "Grace Ukaoma" is one suggestion, not that plus "Ukaoma".
408
+ // Longest first: "Faith Ukamya" is one suggestion, not that plus "Ukamya".
409
409
  const candidates = [...new Set<string>([edit.replacement, ...ws.slice(0, -1).map((w, i) => `${w} ${ws[i + 1]}`), ...ws])]
410
410
  .sort((a, b) => b.split(' ').length - a.split(' ').length)
411
411
  const covered = new Set<string>()
@@ -1,3 +1,4 @@
1
+ import { readPhoneRecording, PHONE_SOURCE_TYPE } from './phone-recording.js'
1
2
  // Quarantine for unsaved meeting audio.
2
3
  //
3
4
  // Until 6.19.0 the session-audio purge (boot sweep + 60s interval) DELETED any
@@ -54,6 +55,8 @@ export const DISCARDED_RETENTION_MS = 24 * 60 * 60 * 1000
54
55
  export interface QuarantineReceipt { outcome: 'no_speech'; discarded: true; chunkFiles?: number; note: string }
55
56
 
56
57
  export interface UnsavedCapture {
58
+ sourceType?: typeof PHONE_SOURCE_TYPE
59
+ provenanceState?: 'admitted' | 'unverified'
57
60
  sessionId: string
58
61
  dirName: string
59
62
  quarantinedAt: string | null
@@ -250,8 +253,14 @@ export function listUnsavedCaptures(
250
253
  const manifest = readManifest(dirPath)
251
254
  const atMs = quarantinedAtMs(dirPath, manifest)
252
255
  const ownRetentionMs = effectiveRetentionMs(manifest, retentionMs)
256
+ const sessionId = manifest?.sessionId ?? dir.replace(/\.\d+$/, '')
257
+ let phoneSource: { sourceType?: typeof PHONE_SOURCE_TYPE; provenanceState?: 'admitted' | 'unverified' } = {}
258
+ if (sessionId.startsWith('phone_')) {
259
+ try { phoneSource = readPhoneRecording(sessionId) ? { sourceType: PHONE_SOURCE_TYPE, provenanceState: 'admitted' } : { provenanceState: 'unverified' } } catch { phoneSource = { provenanceState: 'unverified' } }
260
+ }
253
261
  captures.push({
254
- sessionId: manifest?.sessionId ?? dir.replace(/\.\d+$/, ''),
262
+ sessionId,
263
+ ...phoneSource,
255
264
  dirName: dir,
256
265
  quarantinedAt: atMs != null ? new Date(atMs).toISOString() : null,
257
266
  ageHours: atMs != null ? Math.round(((Date.now() - atMs) / 3_600_000) * 10) / 10 : null,
@@ -117,7 +117,7 @@ let building: Promise<VoiceDirectorySnapshot> | null = null
117
117
  function sourceCounts(profile: VoiceProfile): Record<string, number> {
118
118
  const counts: Record<string, number> = {}
119
119
  for (const source of profile.sources ?? []) {
120
- const key = source.startsWith('auto:') ? 'auto' : source
120
+ const key = source.startsWith('auto:') ? 'auto' : source.startsWith('mac-assign:') ? 'mac-assign' : source
121
121
  counts[key] = (counts[key] ?? 0) + 1
122
122
  }
123
123
  return counts
@@ -29,7 +29,7 @@ export const IMPORTED_EVIDENCE_ID_PATTERN = /^(imported|blended):/
29
29
  /** The two sidecars of the imported library. Never chunk audio, never `.g2-chunks.json`. */
30
30
  export const IMPORTED_EVIDENCE_PATH_PATTERN = /\.(?:import|derived)\.json$/i
31
31
 
32
- export type VoiceEvidenceReason = 'imported_read_only' | 'blended_derived_record'
32
+ export type VoiceEvidenceReason = 'imported_read_only' | 'blended_derived_record' | 'phone_anonymous_capture'
33
33
 
34
34
  export interface VoiceEvidenceRefusal {
35
35
  status: 409
@@ -66,6 +66,7 @@ export function isImportedVoiceEvidence(value: unknown): boolean {
66
66
  */
67
67
  export function assertVoiceEvidenceSource(values: ReadonlyArray<unknown>): VoiceEvidenceRefusal | null {
68
68
  for (const value of values) {
69
+ if (typeof value === 'string' && /(?:^|[:/])phone_/.test(value)) return { status: 409, body: { error: 'iPhone microphone recordings are anonymous and cannot name or train voices', reason: 'phone_anonymous_capture', source: value } }
69
70
  const kind = importedEvidenceKind(value)
70
71
  if (!kind) continue
71
72
  return {
@@ -15,7 +15,7 @@ import { checkSpeakerName, type SpeakerNameRejection } from './speaker-name.js'
15
15
  import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs'
16
16
  import { basename, dirname, join, resolve } from 'node:path'
17
17
  import { durableAtomicWriteFileSync, loadJsonOrQuarantine } from './atomic-fs.js'
18
- import { EVICTION_ORDER, provenanceTier } from './embedding-eviction.js'
18
+ import { EVICTION_ORDER, isMacAssign, macAssignCapFor, provenanceTier } from './embedding-eviction.js'
19
19
 
20
20
  export interface VoiceProfile {
21
21
  name: string
@@ -194,7 +194,7 @@ export function junkNameReason(name: string): SpeakerNameRejection | null {
194
194
  const check = checkSpeakerName(name, { ownerLabel: name })
195
195
  if (check.ok) return null
196
196
  if (check.reason === 'too_long' || check.reason === 'too_many_words') return check.reason
197
- // Sentence punctuation alone is not enough: "Luke H." is a legitimate legacy
197
+ // Sentence punctuation alone is not enough: "Theo M." is a legitimate legacy
198
198
  // alias. Three or more words with punctuation is speech, not a name.
199
199
  if (check.reason === 'sentence_like' && name.trim().split(/\s+/).length >= 3) return check.reason
200
200
  return null
@@ -555,9 +555,15 @@ export function mergeProfilesInStore(
555
555
 
556
556
  // Preserve stronger label provenance. Within one
557
557
  // source tier retain newer evidence, matching weakest-first/oldest eviction.
558
- const keep = embeddings.map((_, i) => i).sort((a, b) =>
559
- EVICTION_ORDER.indexOf(provenanceTier(sources[b])) - EVICTION_ORDER.indexOf(provenanceTier(sources[a])) || b - a,
560
- ).slice(0, cap).sort((a, b) => a - b)
558
+ // A Mac-assigned sample ranks BELOW every other tier, the same rule chooseEviction applies (embedding-eviction.ts).
559
+ const rank = (i: number): number => isMacAssign(sources[i]) ? -1 : EVICTION_ORDER.indexOf(provenanceTier(sources[i]))
560
+ const kept = embeddings.map((_, i) => i).sort((a, b) => rank(b) - rank(a) || b - a).slice(0, cap)
561
+ // The merged profile obeys the Mac cap too: no more than 10, and never more than its other samples. Surplus Mac
562
+ // samples go oldest first, which is what chooseEviction does, so a merge cannot hold what an enrolment would refuse.
563
+ const macKept = kept.filter(i => isMacAssign(sources[i])).sort((a, b) => a - b)
564
+ const macAllowed = macAssignCapFor(kept.length, macKept.length)
565
+ const trimmed = new Set(macKept.slice(0, Math.max(0, macKept.length - macAllowed)))
566
+ const keep = kept.filter(i => !trimmed.has(i)).sort((a, b) => a - b)
561
567
  outcome.droppedToCap = embeddings.length - keep.length
562
568
  target.embeddings = keep.map(i => embeddings[i])
563
569
  target.sources = keep.map(i => sources[i])
@@ -1398,7 +1398,7 @@ function buildCorrections(): Array<[RegExp, string]> {
1398
1398
  * The profile's order, except that a key goes ahead of any earlier key whose words it
1399
1399
  * contains (6.83.1): "forward slash build and price" runs before "forward slash build", or
1400
1400
  * the shorter key would cut the longer phrase short. Chains the person relies on
1401
- * ("Myles" then "Miles Ukoma") keep their order, since neither key contains the other.
1401
+ * ("Myles" then "Miles Ukmya") keep their order, since neither key contains the other.
1402
1402
  */
1403
1403
  export function correctionOrder(corrections: Record<string, string>): Array<[string, string]> {
1404
1404
  const norm = (key: string) => ` ${key.trim().toLowerCase().split(/[\s-]+/).join(' ')} `
@@ -46,6 +46,7 @@ import {
46
46
  } from '../lib/dictation-polish-options.js'
47
47
  import { LEARNED_STYLE_INTRO, LEARNED_STYLE_LIMITS, learnedStyleBlock, LITERAL_REGISTER_RULES, V4_CLOSING_RULE, V4_LITERAL_CLOSING_RULE } from '../lib/dictation-clean.js'
48
48
  import { publishedRegisters, registersEnabled } from '../lib/dictation-register.js'
49
+ import { meaningGuardEnabled } from '../lib/dictation-meaning-guard.js'
49
50
 
50
51
  /** Where the instructions apply (prompt-drafts.ts polishInstructionsFor): never meeting lines. */
51
52
  export const CUSTOM_INSTRUCTIONS_APPLY_TO = ['message', 'prompt_draft'] as const
@@ -248,11 +249,17 @@ dictationPolishSettingsRouter.delete('/dictation/learned-style', (_req, res) =>
248
249
  function optionsView() {
249
250
  const stored = readPolishOptions()
250
251
  const choice = dictationPolishPromptChoice()
252
+ const effectiveByModel = Object.fromEntries(POLISH_MODEL_DEFAULTS.map(model => [model, dictationPolishPromptChoice(process.env, model).version]))
251
253
  return {
252
254
  promptVersion: stored.promptVersion,
253
255
  effectiveVersion: choice.version,
254
256
  // 6.91.0 option C: the prompt each model actually gets.
255
- effectiveByModel: Object.fromEntries(POLISH_MODEL_DEFAULTS.map(model => [model, dictationPolishPromptChoice(process.env, model).version])),
257
+ effectiveByModel,
258
+ // 6.92.0 (plan r3, A-W2): what Control's picker line needs, additive. The pairs the meaning guard,
259
+ // Your style and the register apply to (the same list GET /rules computes), and whether the
260
+ // meaning guard is on (COS_DICTATION_MEANING_GUARD). Older servers omit both.
261
+ stylePairs: POLISH_MODEL_DEFAULTS.filter(model => styleAndRegisterApply(model, effectiveByModel[model])).map(model => ({ model, prompt: effectiveByModel[model] })),
262
+ meaningGuard: meaningGuardEnabled(),
256
263
  source: choice.source,
257
264
  // The environment named a version, so this option has no effect until it is removed.
258
265
  envOverride: choice.source === 'env',
@@ -1,3 +1,5 @@
1
+ import { phoneMicrophoneMeetingsCapability } from '../lib/phone-recording.js'
2
+ import { macLiveVoicesSnapshot } from '../lib/mac-window-replay.js'
1
3
  import { DICTATION_CLEAN_MODELS } from '../lib/dictation-clean-models.js'
2
4
  import { customInstructionsCapability, learnedStyleCapability, polishOptionsCapability } from './dictation-polish-settings.js'
3
5
  import { PAIRING_PROTOCOL_VERSION } from '../lib/glasses-pairing.js'
@@ -550,6 +552,9 @@ healthRouter.get('/health', async (_req, res) => {
550
552
  finalization: getMeetingFinalizationSnapshot(),
551
553
  },
552
554
  ...(localFirstMeetings ? { localFirstMeetings } : {}),
555
+ phoneMicrophoneMeetings: phoneMicrophoneMeetingsCapability(getServerInstanceId()),
556
+ // 6.93.0 W8: which live Mac Voice N path is on (window by default since 6.93.0; anchors; off) and the window path's counts.
557
+ macLiveVoices: macLiveVoicesSnapshot(),
553
558
  ...(morningBrief ? { morningBrief } : {}),
554
559
  tasks: {
555
560
  gate: taskBridgeAvailable() ? 'ready' : 'disabled',
@@ -741,6 +746,9 @@ healthRouter.get('/models', async (req, res) => {
741
746
  },
742
747
  },
743
748
  ...(localFirstMeetings ? { localFirstMeetings } : {}),
749
+ phoneMicrophoneMeetings: phoneMicrophoneMeetingsCapability(getServerInstanceId()),
750
+ // 6.93.0 W8: which live Mac Voice N path is on (window by default since 6.93.0; anchors; off) and the window path's counts.
751
+ macLiveVoices: macLiveVoicesSnapshot(),
744
752
  tasks: {
745
753
  gate: taskBridgeAvailable() ? 'ready' : 'disabled',
746
754
  },