@gotcos/glasses-server 6.89.0 → 6.91.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.
@@ -9,6 +9,22 @@
9
9
  // Clients find it through capabilities.dictationCleanup.customInstructions on /api/health and
10
10
  // /api/models; an older server omits it (and answers 404 here), so the clients hide the box.
11
11
  // The text is never logged and never returned anywhere but GET/PUT here.
12
+ //
13
+ // 6.91.0 (plan r2, contract frozen 2026-10-24). Token-protected too:
14
+ //
15
+ // GET /api/dictation/learned-style {enabled, rules, text, hash, revision, updatedAt, registers, kinds, allowlist, ...}
16
+ // PUT /api/dictation/learned-style {enabled?, rules?: [{id, kind, register, param?, count, lastSeen}], baseRevision?}
17
+ // Strict keys. text, hash, revision and updatedAt are the server's
18
+ // (400); a stale baseRevision (it covers the rules AND the switch)
19
+ // is 409; over 20 rules or 1,500 characters is 413.
20
+ // DELETE /api/dictation/learned-style Reset: the rules go, the switch stays as it was
21
+ // GET /api/dictation/polish/options {promptVersion, effectiveVersion, source, envOverride, updatedAt}
22
+ // PUT /api/dictation/polish/options {promptVersion: "default" | "v3"}: Control's "Previous polish"
23
+ //
24
+ // Clients find them through capabilities.dictationCleanup.learnedStyle {protocol, maxChars,
25
+ // maxRules, enabled} and capabilities.dictationCleanup.polishOptions {protocol} on /api/health and
26
+ // /api/models. learnedStyle.enabled is false with COS_DICTATION_LEARNED_STYLE=0: the routes still
27
+ // answer, and no prompt carries the rules.
12
28
 
13
29
  import { Router } from 'express'
14
30
  import {
@@ -19,6 +35,17 @@ import {
19
35
  CUSTOM_INSTRUCTIONS_MAX_CHARS, CUSTOM_INSTRUCTIONS_PROTOCOL, customInstructionsHash, readCustomInstructions,
20
36
  sanitizeCustomInstructions, writeCustomInstructions, type StoredInstructions,
21
37
  } from '../lib/dictation-polish-instructions.js'
38
+ import {
39
+ CONTRACTION_PARAMS, LEARNED_STYLE_ALLOWLIST, LEARNED_STYLE_KINDS, LEARNED_STYLE_MAX_CHARS, LEARNED_STYLE_MAX_RULES,
40
+ LEARNED_STYLE_PROTOCOL, LEARNED_STYLE_REGISTERS, learnedStyleEnabled, learnedStyleLinesFor, readLearnedStyle,
41
+ validateLearnedStyleRules, writeLearnedStyle, type StoredLearnedStyle,
42
+ } from '../lib/dictation-learned-style.js'
43
+ import {
44
+ dictationPolishPromptChoice, POLISH_MODEL_DEFAULTS, POLISH_OPTIONS_PROTOCOL, POLISH_PROMPT_OPTIONS, readPolishOptions,
45
+ styleAndRegisterApply, writePolishOptions, type PolishPromptOption,
46
+ } from '../lib/dictation-polish-options.js'
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
+ import { publishedRegisters, registersEnabled } from '../lib/dictation-register.js'
22
49
 
23
50
  /** Where the instructions apply (prompt-drafts.ts polishInstructionsFor): never meeting lines. */
24
51
  export const CUSTOM_INSTRUCTIONS_APPLY_TO = ['message', 'prompt_draft'] as const
@@ -28,6 +55,21 @@ export function customInstructionsCapability() {
28
55
  return { protocol: CUSTOM_INSTRUCTIONS_PROTOCOL, maxChars: CUSTOM_INSTRUCTIONS_MAX_CHARS }
29
56
  }
30
57
 
58
+ /**
59
+ * 6.91.0: capabilities.dictationCleanup.learnedStyle on both health surfaces. Present on every
60
+ * 6.91.0 server; `enabled` is the server's switch (COS_DICTATION_LEARNED_STYLE). Codes and numbers only.
61
+ */
62
+ export function learnedStyleCapability() {
63
+ return { protocol: LEARNED_STYLE_PROTOCOL, maxChars: LEARNED_STYLE_MAX_CHARS, maxRules: LEARNED_STYLE_MAX_RULES, enabled: learnedStyleEnabled() }
64
+ }
65
+ /** 6.91.0: capabilities.dictationCleanup.polishOptions ("Previous polish"). */
66
+ export function polishOptionsCapability() {
67
+ return { protocol: POLISH_OPTIONS_PROTOCOL }
68
+ }
69
+
70
+ /** Where the learned style applies (lib/dictation-polish-pipeline.ts learnedStyleFor): never meeting lines. */
71
+ export const LEARNED_STYLE_APPLY_TO = ['message', 'prompt_draft'] as const
72
+
31
73
  function view(stored: StoredInstructions) {
32
74
  return {
33
75
  customInstructions: stored.customInstructions,
@@ -76,16 +118,164 @@ dictationPolishSettingsRouter.delete('/dictation/polish/instructions', (_req, re
76
118
  }
77
119
  })
78
120
 
79
- dictationPolishSettingsRouter.get('/dictation/polish/rules', (_req, res) => {
80
- const version = dictationPolishPromptVersion()
121
+ dictationPolishSettingsRouter.get('/dictation/polish/rules', (req, res) => {
122
+ // 6.91.0 option C: each model has its own default (v4 on Sonnet, v3 otherwise). `?model=` picks
123
+ // whose view `version` and `rules` describe; without it they describe the newest default.
124
+ const wanted = typeof req.query?.model === 'string' && (POLISH_MODEL_DEFAULTS as readonly string[]).includes(req.query.model) ? req.query.model : undefined
125
+ const choice = dictationPolishPromptChoice(process.env, wanted)
126
+ const version = choice.version
127
+ const promptByModel = Object.fromEntries(POLISH_MODEL_DEFAULTS.map(model => [model, dictationPolishPromptVersion(process.env, model)]))
128
+ // Learned style and the literal register apply only to the pairs they were canaried on.
129
+ const stylePairs = POLISH_MODEL_DEFAULTS.filter(model => styleAndRegisterApply(model, promptByModel[model])).map(model => ({ model, prompt: promptByModel[model] }))
130
+ const styled = wanted === undefined ? stylePairs.length > 0 : styleAndRegisterApply(wanted, version)
81
131
  res.json({
82
132
  version,
83
133
  defaultVersion: DEFAULT_DICTATION_POLISH_PROMPT,
84
- rolledBack: version !== DEFAULT_DICTATION_POLISH_PROMPT,
134
+ rolledBack: choice.source !== 'default',
85
135
  systemPrompt: DICTATION_POLISH_SYSTEM_PROMPTS[version],
86
136
  rules: dictationPolishRules(version).join('\n'),
87
137
  // The block exactly as the model sees it, with a placeholder for the user's words.
88
138
  userBlock: [...USER_PREFERENCES_INTRO, '<user-style-preferences>', INSTRUCTIONS_PLACEHOLDER, '</user-style-preferences>', USER_PREFERENCES_PRECEDENCE].join('\n'),
89
139
  maxChars: CUSTOM_INSTRUCTIONS_MAX_CHARS,
140
+ // 6.91.0: which prompt each model gets, the rules of each prompt in use, and where the learned
141
+ // style and the literal register apply (Sonnet with v4 only: the pair canaries C2 and C3 ran on).
142
+ ...(wanted ? { model: wanted } : {}),
143
+ promptByModel,
144
+ rulesByPrompt: Object.fromEntries([...new Set(Object.values(promptByModel))].map(v => [v, dictationPolishRules(v).join('\n')])),
145
+ stylePairs,
146
+ registersEnabled: registersEnabled(),
147
+ // The other blocks exactly as the model sees them. The learned style is the saved rules rendered
148
+ // from the server's templates ('' when none apply); the literal block goes to terminals only
149
+ // (none with COS_DICTATION_REGISTERS=0); the closing line is v4's.
150
+ learnedStyleBlock: styled && learnedStyleEnabled() ? learnedStyleBlock(learnedStyleLinesFor(readLearnedStyle(), 'default')).join('\n').trim() : '',
151
+ learnedStyleFrame: [LEARNED_STYLE_INTRO, '<learned-style>', LEARNED_STYLE_PLACEHOLDER, '</learned-style>', LEARNED_STYLE_LIMITS].join('\n'),
152
+ registerBlocks: styled && registersEnabled() && version === 'v4' ? { literal: LITERAL_REGISTER_RULES.join('\n') } : {},
153
+ closingRules: version === 'v4' ? { default: V4_CLOSING_RULE, literal: V4_LITERAL_CLOSING_RULE } : {},
90
154
  })
91
155
  })
156
+
157
+ /** The placeholder the rules view shows where the learned-style lines go. */
158
+ export const LEARNED_STYLE_PLACEHOLDER = '(your learned style rules)'
159
+
160
+ // ---------------------------------------------------------------------------------------------
161
+ // 6.91.0 learned style and polish options.
162
+
163
+ function styleView(stored: StoredLearnedStyle) {
164
+ return {
165
+ enabled: stored.enabled,
166
+ rules: stored.rules,
167
+ text: stored.text,
168
+ hash: stored.hash,
169
+ revision: stored.revision,
170
+ updatedAt: stored.updatedAt,
171
+ // Control reads the app-to-register map here, so it never keeps a second table. The literal
172
+ // register is opt-in (COS_DICTATION_REGISTERS=1); `registersEnabled` says whether it applies.
173
+ registers: publishedRegisters(),
174
+ registersEnabled: registersEnabled(),
175
+ kinds: LEARNED_STYLE_KINDS,
176
+ registerValues: LEARNED_STYLE_REGISTERS,
177
+ allowlist: LEARNED_STYLE_ALLOWLIST,
178
+ contractionParams: CONTRACTION_PARAMS,
179
+ maxChars: LEARNED_STYLE_MAX_CHARS,
180
+ maxRules: LEARNED_STYLE_MAX_RULES,
181
+ serverEnabled: learnedStyleEnabled(),
182
+ appliesTo: LEARNED_STYLE_APPLY_TO,
183
+ }
184
+ }
185
+
186
+ const STYLE_PUT_KEYS = new Set(['enabled', 'rules', 'baseRevision'])
187
+ const SERVER_OWNED = new Set(['text', 'hash', 'revision', 'updatedAt'])
188
+
189
+ /** A Node error code ("EACCES") for the log, never a message (a message can carry the path). */
190
+ function errorCode(err: unknown): string {
191
+ const code = (err as { code?: unknown } | null)?.code
192
+ return typeof code === 'string' && /^[A-Z][A-Z0-9_]{1,23}$/.test(code) ? code : 'error'
193
+ }
194
+
195
+ dictationPolishSettingsRouter.get('/dictation/learned-style', (_req, res) => {
196
+ res.json(styleView(readLearnedStyle()))
197
+ })
198
+
199
+ dictationPolishSettingsRouter.put('/dictation/learned-style', (req, res) => {
200
+ const body = req.body
201
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return res.status(400).json({ error: 'invalid_body' })
202
+ const keys = Object.keys(body)
203
+ const owned = keys.find(key => SERVER_OWNED.has(key))
204
+ if (owned) return res.status(400).json({ error: 'server_owned_field', field: owned })
205
+ if (!keys.every(key => STYLE_PUT_KEYS.has(key)) || !('rules' in body || 'enabled' in body)) {
206
+ return res.status(400).json({ error: 'invalid_body', expected: { enabled: 'boolean?', rules: '[{id, kind, register, param, count, lastSeen}]?', baseRevision: 'string?' } })
207
+ }
208
+ if ('enabled' in body && typeof body.enabled !== 'boolean') return res.status(400).json({ error: 'invalid_enabled' })
209
+ if ('baseRevision' in body && (typeof body.baseRevision !== 'string' || !/^[0-9a-f]{8}$/.test(body.baseRevision))) {
210
+ return res.status(400).json({ error: 'invalid_base_revision' })
211
+ }
212
+ const current = readLearnedStyle()
213
+ if (typeof body.baseRevision === 'string' && body.baseRevision !== current.revision) {
214
+ // Someone else saved first (rules or the switch): re-GET and merge.
215
+ return res.status(409).json({ error: 'base_revision_mismatch', revision: current.revision })
216
+ }
217
+ let rules = current.rules
218
+ if ('rules' in body) {
219
+ const clean = validateLearnedStyleRules(body.rules)
220
+ if (!clean.ok) {
221
+ return res.status(clean.status).json({
222
+ error: clean.error,
223
+ ...(clean.index !== undefined ? { index: clean.index } : {}),
224
+ ...(clean.max !== undefined ? { max: clean.max } : {}),
225
+ })
226
+ }
227
+ rules = clean.rules
228
+ }
229
+ try {
230
+ res.json(styleView(writeLearnedStyle(typeof body.enabled === 'boolean' ? body.enabled : current.enabled, rules)))
231
+ } catch (err) {
232
+ // The reason stays generic: an fs error can carry the path, never the rules, but say less.
233
+ console.warn(`[dictation-polish] learned_style_not_saved put (${errorCode(err)})`)
234
+ res.status(500).json({ error: 'learned_style_not_saved' })
235
+ }
236
+ })
237
+
238
+ dictationPolishSettingsRouter.delete('/dictation/learned-style', (_req, res) => {
239
+ try {
240
+ // Reset clears the rules and leaves the person's switch as it was: an Off stays off.
241
+ res.json(styleView(writeLearnedStyle(readLearnedStyle().enabled, [])))
242
+ } catch (err) {
243
+ console.warn(`[dictation-polish] learned_style_not_saved delete (${errorCode(err)})`)
244
+ res.status(500).json({ error: 'learned_style_not_saved' })
245
+ }
246
+ })
247
+
248
+ function optionsView() {
249
+ const stored = readPolishOptions()
250
+ const choice = dictationPolishPromptChoice()
251
+ return {
252
+ promptVersion: stored.promptVersion,
253
+ effectiveVersion: choice.version,
254
+ // 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])),
256
+ source: choice.source,
257
+ // The environment named a version, so this option has no effect until it is removed.
258
+ envOverride: choice.source === 'env',
259
+ options: POLISH_PROMPT_OPTIONS,
260
+ updatedAt: stored.updatedAt,
261
+ }
262
+ }
263
+
264
+ dictationPolishSettingsRouter.get('/dictation/polish/options', (_req, res) => {
265
+ res.json(optionsView())
266
+ })
267
+
268
+ dictationPolishSettingsRouter.put('/dictation/polish/options', (req, res) => {
269
+ const body = req.body
270
+ if (!body || typeof body !== 'object' || Array.isArray(body) || !Object.keys(body).every(key => key === 'promptVersion')
271
+ || !(POLISH_PROMPT_OPTIONS as readonly string[]).includes(body.promptVersion)) {
272
+ return res.status(400).json({ error: 'invalid_body', expected: { promptVersion: POLISH_PROMPT_OPTIONS } })
273
+ }
274
+ try {
275
+ writePolishOptions(body.promptVersion as PolishPromptOption)
276
+ res.json(optionsView())
277
+ } catch (err) {
278
+ console.warn(`[dictation-polish] options_not_saved (${errorCode(err)})`)
279
+ res.status(500).json({ error: 'options_not_saved' })
280
+ }
281
+ })
@@ -1,5 +1,5 @@
1
1
  import { DICTATION_CLEAN_MODELS } from '../lib/dictation-clean-models.js'
2
- import { customInstructionsCapability } from './dictation-polish-settings.js'
2
+ import { customInstructionsCapability, learnedStyleCapability, polishOptionsCapability } from './dictation-polish-settings.js'
3
3
  import { PAIRING_PROTOCOL_VERSION } from '../lib/glasses-pairing.js'
4
4
  import { taskBridgeAvailable } from '../lib/task-bridge.js'
5
5
  import { Router } from 'express'
@@ -491,7 +491,11 @@ healthRouter.get('/health', async (_req, res) => {
491
491
  ...(voiceProvenance ? { voice_provenance: voiceProvenance } : {}),
492
492
  capabilities: {
493
493
  meetingAsk: { ...meetingAskCapabilities(), webRetrieval: meetingWebCapability().available },
494
- dictationCleanup: { models: DICTATION_CLEAN_MODELS, customInstructions: customInstructionsCapability() },
494
+ // 6.91.0: learnedStyle ("Your style", enabled = the server switch) and polishOptions ("Previous polish").
495
+ dictationCleanup: {
496
+ models: DICTATION_CLEAN_MODELS, customInstructions: customInstructionsCapability(),
497
+ learnedStyle: learnedStyleCapability(), polishOptions: polishOptionsCapability(),
498
+ },
495
499
  // 6.67.0: glasses pairing (docs/pairing-contract.md). Public on purpose: the phone
496
500
  // reads it before it holds a token, and an older server omits it, which is how
497
501
  // the phone knows to say "Update COS on your Mac (server 6.67 or newer)".
@@ -679,7 +683,11 @@ healthRouter.get('/models', async (req, res) => {
679
683
  ...(apiToken ? { displayStreamTicket: mintDisplayTicket(apiToken) } : {}),
680
684
  capabilities: {
681
685
  meetingAsk: { ...meetingAskCapabilities(), webRetrieval: meetingWebCapability().available },
682
- dictationCleanup: { models: DICTATION_CLEAN_MODELS, customInstructions: customInstructionsCapability() },
686
+ // 6.91.0: learnedStyle ("Your style", enabled = the server switch) and polishOptions ("Previous polish").
687
+ dictationCleanup: {
688
+ models: DICTATION_CLEAN_MODELS, customInstructions: customInstructionsCapability(),
689
+ learnedStyle: learnedStyleCapability(), polishOptions: polishOptionsCapability(),
690
+ },
683
691
  durableQueryJobs: {
684
692
  enabled: durableJobs.enabled,
685
693
  protocolVersion: durableJobs.protocolVersion,
@@ -33,28 +33,16 @@ import {
33
33
  isCaptionCreditHallucination,
34
34
  isBrandUrlOnly,
35
35
  clearSessionHallucinationState,
36
- applyNegativeRules,
37
36
  } from '../lib/hallucination-filter.js'
38
- import { applyCorrections } from '../lib/whisper-local.js'
39
- import { applyFuzzyCorrections } from '../lib/fuzzy-correct.js'
40
- import { getAllSpeakerNames } from '../lib/speaker-embeddings.js'
41
37
  import { transcribeWhisperPreview } from '../lib/whisper-preview.js'
42
38
  import { currentDictationPlan, runParakeetDictation } from '../lib/dictation-stt.js'
43
39
  import { dictationSlotForMode, legacyDictationKey, type DictationPlan } from '../lib/stt-settings.js'
44
- import { autoCleanDictation, AUTOCLEAN_MAX_CHARS } from '../lib/dictation-clean.js'
45
- import { fillerPassEnabled, stripDisfluencies } from '../lib/dictation-fillers.js'
46
- import { polishBudgetExhausted, recordPolishCall, type PolishBucket } from '../lib/dictation-polish-budget.js'
47
- import { polishNeed, skipCleanEnabled } from '../lib/dictation-polish-need.js'
48
- import {
49
- discardPolishPrewarm, isPrewarmModel, prewarmEnabled, PrewarmUnavailableError, runWarmPolish,
50
- startPolishPrewarm, takePolishPrewarm,
51
- } from '../lib/dictation-prewarm.js'
52
- import { getVocabulary, getWhisperCorrections } from '../lib/profile.js'
53
- import { correctionOnlyForms } from '../lib/glossary-learn.js'
54
- import { polishLinkProblem, renderSpokenPaths, spokenPathsEnabled } from '../lib/dictation-format.js'
55
- import { customInstructionsHash, readCustomInstructions } from '../lib/dictation-polish-instructions.js'
56
- import { createBreaker } from '../lib/claude-circuit.js'
57
- import { logTokenAudit } from '../lib/token-audit.js'
40
+ import { AUTOCLEAN_MAX_CHARS } from '../lib/dictation-clean.js'
41
+ import { polishBudgetExhausted } from '../lib/dictation-polish-budget.js'
42
+ import { discardPolishPrewarm, isPrewarmModel, prewarmEnabled, startPolishPrewarm } from '../lib/dictation-prewarm.js'
43
+ import { applyCorrections } from '../lib/whisper-local.js'
44
+ import { registerFromContext } from '../lib/dictation-register.js'
45
+ import { autoCleanBreakers, cleanOutboundDictation, type AutoCleanRequest, type PolishResult } from '../lib/dictation-polish-pipeline.js'
58
46
  import { emitDisplay } from '../lib/display-bus.js'
59
47
  import {
60
48
  acquireMaintenanceWork,
@@ -85,19 +73,8 @@ function speculativeHqWarmEnabled(): boolean {
85
73
  return !['0', 'false', 'off'].includes((process.env.COS_HQ_SPECULATIVE_WARM ?? '1').toLowerCase())
86
74
  }
87
75
 
88
- const autoCleanBreakers = {
89
- sonnet: createBreaker({ label: 'dictation-autoclean-sonnet' }),
90
- 'luna-5.6-fast': createBreaker({ label: 'dictation-autoclean-luna' }),
91
- haiku: createBreaker({ label: 'dictation-autoclean-haiku' }),
92
- }
93
- const autoCleanDefaultEnabled = () => ['1', 'true', 'on'].includes((process.env.COS_DICTATION_AUTOCLEAN ?? '').toLowerCase())
94
-
95
- /** 6.79.1, additive on finalize answers: why the polish model did not run (or did not finish). */
96
- export type PolishSkipped = 'disabled' | 'too_long' | 'breaker_open' | 'daily_cap' | 'failed' | 'clean' | 'link_guard'
97
- /** 6.79.2, additive when finalize was given a prewarmToken: what became of that warm process. */
98
- export type PrewarmOutcome = 'used' | 'missed' | 'discarded'
99
-
100
- interface AutoCleanRequest { enabled?: boolean; model?: DictationCleanModel }
76
+ // 6.91.0: the outbound polish pipeline (breakers, gates, model, guards) lives in
77
+ // lib/dictation-polish-pipeline.ts, so the offline evals call it rather than a copy.
101
78
  function routeAutoClean(req: { body?: any; query?: any }): AutoCleanRequest {
102
79
  const rawEnabled = req.body?.autoclean ?? req.query?.autoclean
103
80
  const enabled = rawEnabled === undefined ? undefined : ['1', 'true', 'on'].includes(String(rawEnabled).toLowerCase())
@@ -107,113 +84,6 @@ function routeAutoClean(req: { body?: any; query?: any }): AutoCleanRequest {
107
84
  return { enabled, model: isDictationCleanModel(rawModel) ? rawModel : undefined }
108
85
  }
109
86
 
110
- async function cleanOutboundDictation(
111
- text: string,
112
- opts: AutoCleanRequest & { bucket: PolishBucket; signal?: AbortSignal; prewarmToken?: string },
113
- ): Promise<PolishResult> {
114
- const result = await polishOutboundDictation(text, opts)
115
- // A warm process the model call did not take must not sit until its TTL.
116
- if (opts.prewarmToken && !result.prewarm) {
117
- return { ...result, prewarm: discardPolishPrewarm(opts.prewarmToken) ? 'discarded' : 'missed' }
118
- }
119
- return result
120
- }
121
-
122
- /** 6.85.0, additive and present only when the model was given the user's polish instructions. */
123
- interface PolishInstructionsUse { customInstructions?: true; instructionsHash?: string }
124
- type PolishResult = { text: string; polishSkipped?: PolishSkipped; prewarm?: PrewarmOutcome } & PolishInstructionsUse
125
-
126
- /**
127
- * 6.85.0: the user's polish instructions apply to dictation (Mac dictation and G2 messages, which
128
- * are surface "message", and prompt drafts). Meeting lines are a faithful record, so they stay on
129
- * the built-in rules alone, the same way they skip the filler pass and spoken paths.
130
- */
131
- export function polishInstructionsFor(bucket: PolishBucket): string {
132
- return bucket === 'meeting' ? '' : readCustomInstructions().customInstructions
133
- }
134
-
135
- async function polishOutboundDictation(
136
- text: string,
137
- opts: AutoCleanRequest & { bucket: PolishBucket; signal?: AbortSignal; prewarmToken?: string },
138
- ): Promise<PolishResult> {
139
- let cleaned = applyNegativeRules(applyCorrections(text)).replace(/\s+/g, ' ').trim() || text
140
- // applyCorrections above is an EXACT string map, so it only fixes misspellings someone
141
- // already hand-authored. A novel miss ("Rileyy" for Riley, "Exampel" for Example) sails
142
- // through it. The Levenshtein pass is what catches those, and until now it had exactly
143
- // ONE call site — transcribe-audio.ts:252, the server-transcription route — so phone
144
- // Moonshine dictation, which arrives here as text, never got it. Same construction and
145
- // same non-fatal posture as that site, deliberately: targets are speaker names plus
146
- // vocabulary, and a throw must never cost the user their transcript.
147
- //
148
- // Runs BEFORE the autoclean LLM so the model sees corrected proper nouns rather than
149
- // being asked to guess at them, and it still helps on every path where autoclean is
150
- // off, over the char cap, or breaker-open.
151
- try {
152
- const fuzzyTargets = [...getAllSpeakerNames(), ...getVocabulary()]
153
- const { text: corrected, replacements } = applyFuzzyCorrections(cleaned, fuzzyTargets)
154
- if (replacements > 0) {
155
- console.log(`[prompt-draft] Fuzzy corrected ${replacements} word(s)`)
156
- cleaned = corrected
157
- }
158
- } catch (fuzzyErr: any) {
159
- console.warn(`[prompt-draft] Fuzzy correction failed (non-fatal): ${fuzzyErr?.message ?? fuzzyErr}`)
160
- }
161
- // 6.79.1: um/uh never survive a skipped polish. Meeting lines stay faithful.
162
- if (opts.bucket !== 'meeting' && fillerPassEnabled()) cleaned = stripDisfluencies(cleaned)
163
- // 6.83.1: "forward slash pricing," is "/pricing," even when the model is skipped. One word
164
- // at a phrase end only; slugs of several words are the model's (dictation-format.ts).
165
- if (opts.bucket !== 'meeting' && spokenPathsEnabled()) cleaned = renderSpokenPaths(cleaned)
166
- if (!(opts.enabled ?? autoCleanDefaultEnabled())) return { text: cleaned, polishSkipped: 'disabled' }
167
- const model = opts.model ?? 'haiku'
168
- const autoCleanBreaker = autoCleanBreakers[model]
169
- if (cleaned.length > AUTOCLEAN_MAX_CHARS) return { text: cleaned, polishSkipped: 'too_long' }
170
- // 6.79.2, off by default: nothing for the model to fix (see dictation-polish-need.ts).
171
- if (skipCleanEnabled() && polishNeed(cleaned) === null) return { text: cleaned, polishSkipped: 'clean' }
172
- if (autoCleanBreaker.isOpen()) return { text: cleaned, polishSkipped: 'breaker_open' }
173
- if (polishBudgetExhausted(opts.bucket)) return { text: cleaned, polishSkipped: 'daily_cap' }
174
- const startedAt = Date.now()
175
- recordPolishCall(opts.bucket)
176
- // Read once per polish: the warm and cold paths see the same instructions.
177
- const customInstructions = polishInstructionsFor(opts.bucket)
178
- const instructionsHash = customInstructionsHash(customInstructions)
179
- const used: PolishInstructionsUse = customInstructions ? { customInstructions: true, instructionsHash } : {}
180
- // 6.79.2: a warm process started at key press answers without the CLI's start-up cost.
181
- const warmEntry = opts.prewarmToken ? takePolishPrewarm(opts.prewarmToken, model) : null
182
- let prewarm: PrewarmOutcome | undefined = opts.prewarmToken ? 'missed' : undefined
183
- try {
184
- let polished: string | null = null
185
- if (warmEntry) {
186
- try {
187
- polished = (await runWarmPolish(warmEntry, cleaned, getVocabulary(), { signal: opts.signal, customInstructions })).trim()
188
- prewarm = 'used'
189
- } catch (err) {
190
- // Nothing was sent: the cold path below is the same call, not a second one.
191
- if (!(err instanceof PrewarmUnavailableError)) throw err
192
- }
193
- }
194
- if (polished === null) polished = (await autoCleanDictation(cleaned, getVocabulary(), { model, signal: opts.signal, customInstructions })).trim()
195
- autoCleanBreaker.recordSuccess()
196
- logTokenAudit({
197
- source: 'g2-dictation-autoclean', model, inputChars: cleaned.length, outputChars: polished.length,
198
- durationMs: Date.now() - startedAt, caller: `dictation_autoclean_${opts.bucket}`,
199
- polishInstructions: { on: Boolean(customInstructions), hash: instructionsHash },
200
- })
201
- // 6.83.1: the model never invents a link and never drops one the dictionary wrote. Its
202
- // answer is refused (the pre-polish text goes out, polishSkipped "link_guard") rather than
203
- // pasting a URL nobody said. The call still counted: it ran.
204
- const linkProblem = polished ? polishLinkProblem(cleaned, polished, correctionOnlyForms(getWhisperCorrections()), getVocabulary()) : null
205
- if (linkProblem) {
206
- console.warn(`[prompt-draft] auto-clean answer refused: ${linkProblem}`)
207
- return { text: cleaned, ...(prewarm ? { prewarm } : {}), polishSkipped: 'link_guard', ...used }
208
- }
209
- return { text: polished || cleaned, ...(prewarm ? { prewarm } : {}), ...used }
210
- } catch (err: any) {
211
- autoCleanBreaker.recordFailure()
212
- console.warn(`[prompt-draft] auto-clean failed (glossary-only): ${err?.message ?? err}`)
213
- return { text: cleaned, polishSkipped: 'failed', ...(prewarm ? { prewarm } : {}), ...used }
214
- }
215
- }
216
-
217
87
  /** Finalize already-transcribed phone text through the same glossary + optional
218
88
  * AI cleanup used by server-owned prompt drafts. This route accepts text only:
219
89
  * Moonshine audio and rolling preview audio never leave the phone. */
@@ -238,13 +108,17 @@ promptDraftsRouter.post('/dictation/finalize', async (req, res) => {
238
108
 
239
109
  const abort = new AbortController()
240
110
  res.on('close', () => { if (!res.writableEnded) abort.abort() })
241
- const { text: finalText, polishSkipped, prewarm, customInstructions, instructionsHash } = await cleanOutboundDictation(text, {
111
+ const result = await cleanOutboundDictation(text, {
242
112
  enabled: true,
243
113
  model,
244
114
  bucket: surface,
245
115
  signal: abort.signal,
246
116
  prewarmToken: rawToken,
117
+ // 6.91.0: the target app (Control has sent it since 0.5.325) picks the register. Read here,
118
+ // never logged or echoed; anything that is not a short one-line name is the default register.
119
+ register: registerFromContext(req.body?.context),
247
120
  })
121
+ const { text: finalText, polishSkipped, prewarm, customInstructions, instructionsHash } = result
248
122
  // 6.79.1: polishSkipped only when the model did not polish, so a polished answer is unchanged.
249
123
  // 6.79.2: prewarm only when the request carried a prewarmToken.
250
124
  res.json({
@@ -252,9 +126,27 @@ promptDraftsRouter.post('/dictation/finalize', async (req, res) => {
252
126
  ...(polishSkipped ? { polishSkipped } : {}), ...(prewarm ? { prewarm } : {}),
253
127
  // 6.85.0: only when the model was given the user's polish instructions (a hash, never the text).
254
128
  ...(customInstructions ? { customInstructions, instructionsHash } : {}),
129
+ ...polishFields(result),
255
130
  })
256
131
  })
257
132
 
133
+ /**
134
+ * 6.91.0, additive on both finalize answers, every field optional (old clients ignore them):
135
+ * polishOutcome (only when the model answered: polished, unchanged or guarded, about the model
136
+ * step), guard (the guard that refused it), and whenever the model was called: polishPrompt (v3
137
+ * or v4), learnedStyle (true with its hash, or false), and polishRegister on the one pair it was
138
+ * canaried on (Sonnet with v4). Never the model's own text.
139
+ */
140
+ function polishFields(result: PolishResult) {
141
+ return {
142
+ ...(result.polishOutcome ? { polishOutcome: result.polishOutcome } : {}),
143
+ ...(result.guard ? { guard: result.guard } : {}),
144
+ ...(result.polishRegister ? { polishRegister: result.polishRegister } : {}),
145
+ ...(result.polishPrompt ? { polishPrompt: result.polishPrompt } : {}),
146
+ ...(result.learnedStyle ? { learnedStyle: true, learnedStyleHash: result.learnedStyleHash } : result.learnedStyleOff ? { learnedStyle: false } : {}),
147
+ }
148
+ }
149
+
258
150
  const PREWARM_TOKEN = /^[0-9a-f-]{36}$/
259
151
 
260
152
  /** 6.79.2: start the polish process at key press. Fire and forget: every refusal is a normal
@@ -539,7 +431,8 @@ async function finalizeDraft(draftId: string, mode: 'hq' | 'fast', autoClean: Au
539
431
  await markPromptDraftError(draftId, 'No speech detected')
540
432
  throw new NoSpeechDetectedError()
541
433
  }
542
- const { text: finalText, polishSkipped, customInstructions, instructionsHash } = await cleanOutboundDictation(text, { ...autoClean, bucket: 'prompt_draft', signal })
434
+ const polish = await cleanOutboundDictation(text, { ...autoClean, bucket: 'prompt_draft', signal })
435
+ const { text: finalText, polishSkipped, customInstructions, instructionsHash } = polish
543
436
  const finalized = await markPromptDraftFinalized(draftId, finalText)
544
437
  const qualities = transcriptRecords.map(record => record.actualQuality)
545
438
  const actualQuality: 'hq' | 'fast' | 'cloud' = qualities.includes('cloud')
@@ -571,6 +464,8 @@ async function finalizeDraft(draftId: string, mode: 'hq' | 'fast', autoClean: Au
571
464
  ...(polishSkipped ? { polishSkipped } : {}),
572
465
  // 6.85.0, additive.
573
466
  ...(customInstructions ? { customInstructions, instructionsHash } : {}),
467
+ // 6.91.0, additive.
468
+ ...polishFields(polish),
574
469
  }
575
470
  }
576
471
 
@@ -2215,6 +2215,10 @@ async function transcribeWithServerWhisper(audioBuffer: Buffer, whisperAudio: Bu
2215
2215
  }
2216
2216
  }
2217
2217
 
2218
+ /** 6.90.0 calibration tags for Mac audio: the whole-chunk shadow score, and one Nemotron track. */
2219
+ export const MAC_SHADOW_EVENT = 'mac-shadow'
2220
+ export const MAC_TRACK_EVENT = 'mac-nemotron-track'
2221
+
2218
2222
  /** The wearer's label for the 6.61.1 owner guard; null when the profile cannot say. */
2219
2223
  function ownerLabelOrNull(): string | null {
2220
2224
  try { return getOwnerSpeakerLabel() } catch { return null }
@@ -2227,7 +2231,7 @@ function identifyChunkSpeaker(audioBuffer: Buffer, sessionId: string, chunkIndex
2227
2231
  if (!isEmbeddingAvailable() || audioDurationSec < VOICEPRINT_MIN_AUDIO_SEC) return policyLabel({ speaker: clientSpeaker, similarity: 0 }, policy)
2228
2232
 
2229
2233
  const tEmb = performance.now()
2230
- const embeddingResult = identifySpeaker(audioBuffer, expectedSpeakers, { minSimilarity: policy === 'high-confidence' ? AUTO_ENROLL_THRESHOLD : undefined, ...(MAC_CAPTURE_ID.test(sessionId) ? { prioritizeOwner: false, event: 'mac-system-audio' } : {}) })
2234
+ const embeddingResult = identifySpeaker(audioBuffer, expectedSpeakers, { minSimilarity: policy === 'high-confidence' ? AUTO_ENROLL_THRESHOLD : undefined, ...(MAC_CAPTURE_ID.test(sessionId) ? { prioritizeOwner: false, event: 'mac-system-audio', shadowEvent: MAC_SHADOW_EVENT } : {}) })
2231
2235
  console.log(`[perf] identifySpeaker: ${(performance.now() - tEmb).toFixed(1)}ms`)
2232
2236
  if (!embeddingResult) return policyLabel({ speaker: clientSpeaker, similarity: 0 }, policy)
2233
2237
 
@@ -2537,7 +2541,14 @@ async function processStreamChunk(opts: {
2537
2541
  identify: wav => {
2538
2542
  const policy = capturePolicy
2539
2543
  if (policy === 'anonymous') return { speaker: 'Ext', similarity: 0 }
2540
- const named = identifySpeaker(wav, undefined, { event: 'nemotron-track', minSimilarity: policy === 'high-confidence' ? AUTO_ENROLL_THRESHOLD : undefined })
2544
+ // 6.90.0: a Mac track is never checked against the wearer first (the wearer
2545
+ // is not in Mac playback) and is tagged so its calibration rows are separable.
2546
+ const mac = MAC_CAPTURE_ID.test(sessionId)
2547
+ const named = identifySpeaker(wav, undefined, {
2548
+ event: mac ? MAC_TRACK_EVENT : 'nemotron-track',
2549
+ minSimilarity: policy === 'high-confidence' ? AUTO_ENROLL_THRESHOLD : undefined,
2550
+ ...(mac ? { prioritizeOwner: false } : {}),
2551
+ })
2541
2552
  return named ? policyLabel({ speaker: named.speaker, similarity: named.similarity }, policy) : null
2542
2553
  },
2543
2554
  })