@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
@@ -0,0 +1,211 @@
1
+ // Anonymous, derived projection only; no identity/profile/training or recording imports.
2
+ import { createHash } from 'node:crypto'
3
+
4
+ const SHA = /^[a-f0-9]{64}$/
5
+ const TOKEN = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
6
+ const MAC = /^mac_[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
7
+ const matches = (value: unknown, pattern: RegExp): value is string => typeof value === 'string' && pattern.test(value)
8
+ const MAX_CHUNKS = 4096
9
+ const MAX_WORDS = 8192
10
+ const MAX_GENERATIONS = 100
11
+ const MAX_TOTAL_SEGMENTS = 8192
12
+ const MAX_STATE_BYTES = 4 * 1024 * 1024
13
+ const uint = (v: unknown): v is number => Number.isSafeInteger(v) && Number(v) >= 0
14
+ const fail = (reason: string): never => { throw new Error(`mac_projection_${reason}`) }
15
+ const keys = (value: object, names: string[]): boolean => Object.keys(value).length === names.length && names.every(k => Object.hasOwn(value, k))
16
+ export interface ConfirmationProof {
17
+ originalChunkIndex: number; audioSHA256: string; pcmSHA256: string
18
+ chunkStartSample: number; chunkSampleCount: number
19
+ startSample: number; endSample: number; exclusiveSamples: number; purity: number
20
+ }
21
+ interface Slot { slot: number; proofs: ConfirmationProof[]; label?: string }
22
+ export interface SpeakerSegment {
23
+ wordStart: number; wordEnd: number; startSample: number; endSample: number
24
+ speaker: string; similarity: 0
25
+ }
26
+ export interface ChunkProjection {
27
+ chunkIndex: number; generation: string; audioSHA256: string; canonicalBinding: string
28
+ wordCount: number; startSample: number; endSample: number; coveredThroughSample: number
29
+ changedAtRevision: number; segments: SpeakerSegment[]
30
+ }
31
+ export interface ProjectionState {
32
+ version: 1; captureId: string; generation: string | null; generations: string[]
33
+ speakerRevision: number; labelHighWater: number; slots: Slot[]; chunks: ChunkProjection[]
34
+ }
35
+ export interface ProjectionRead {
36
+ version: 1; captureId: string; generation: string | null; speakerRevision: number
37
+ reset: boolean; chunks: ChunkProjection[]
38
+ }
39
+ export function canonicalTextBinding(text: string, words: readonly { text: string; startSample: number; endSample: number }[]): string {
40
+ if (typeof text !== 'string' || Buffer.byteLength(text) > 128_000 || !Array.isArray(words) || words.length > MAX_WORDS) fail('canonical')
41
+ let wordBytes = 0
42
+ for (let i = 0; i < words.length; i++) {
43
+ const w = words[i]
44
+ if (!Object.hasOwn(words, i) || !w || typeof w.text !== 'string' || !uint(w.startSample) || !uint(w.endSample) || w.endSample < w.startSample) fail('canonical_word')
45
+ wordBytes += Buffer.byteLength(w.text)
46
+ if (wordBytes > 128_000) fail('canonical_word_bound')
47
+ }
48
+ return createHash('sha256').update(JSON.stringify({ text, words })).digest('hex')
49
+ }
50
+ export function newProjectionState(captureId: string): ProjectionState {
51
+ if (!matches(captureId, MAC)) fail('capture')
52
+ return { version: 1, captureId, generation: null, generations: [], speakerRevision: 0, labelHighWater: 0, slots: [], chunks: [] }
53
+ }
54
+ function dense<T>(value: T[], max: number): boolean {
55
+ if (!Array.isArray(value) || value.length > max || Object.keys(value).length !== value.length) return false
56
+ for (let i = 0; i < value.length; i++) if (!Object.hasOwn(value, i)) return false
57
+ return true
58
+ }
59
+ function proofValid(p: ConfirmationProof): boolean {
60
+ return !!p && keys(p, ['originalChunkIndex', 'audioSHA256', 'pcmSHA256', 'chunkStartSample', 'chunkSampleCount', 'startSample', 'endSample', 'exclusiveSamples', 'purity']) && uint(p.originalChunkIndex) && matches(p.audioSHA256, SHA) && matches(p.pcmSHA256, SHA)
61
+ && uint(p.chunkStartSample) && uint(p.chunkSampleCount) && p.chunkSampleCount > 0 && p.chunkSampleCount <= 480_000
62
+ && uint(p.startSample) && uint(p.endSample) && p.startSample >= p.chunkStartSample
63
+ && p.endSample > p.startSample && p.endSample <= p.chunkStartSample + p.chunkSampleCount
64
+ && uint(p.exclusiveSamples) && p.exclusiveSamples >= 32_000 && p.exclusiveSamples <= p.endSample - p.startSample
65
+ && Number.isFinite(p.purity) && p.purity >= 0.9 && p.purity <= 1
66
+ }
67
+ function chunkValid(c: ChunkProjection, state: ProjectionState): boolean {
68
+ if (!c || !keys(c, ['chunkIndex', 'generation', 'audioSHA256', 'canonicalBinding', 'wordCount', 'startSample', 'endSample', 'coveredThroughSample', 'changedAtRevision', 'segments']) || !uint(c.chunkIndex) || !state.generations.includes(c.generation) || !matches(c.audioSHA256, SHA) || !matches(c.canonicalBinding, SHA)
69
+ || !uint(c.wordCount) || c.wordCount > MAX_WORDS || !uint(c.startSample) || !uint(c.endSample) || c.endSample < c.startSample
70
+ || !uint(c.coveredThroughSample) || c.coveredThroughSample < c.startSample || c.coveredThroughSample > c.endSample
71
+ || !uint(c.changedAtRevision) || c.changedAtRevision < 1 || c.changedAtRevision > state.speakerRevision || !dense(c.segments, MAX_WORDS)) return false
72
+ let word = 0; let sample = c.startSample
73
+ for (const segment of c.segments) {
74
+ if (!segment || !keys(segment, ['wordStart', 'wordEnd', 'startSample', 'endSample', 'speaker', 'similarity']) || segment.wordStart !== word || !uint(segment.wordEnd) || segment.wordEnd <= word || segment.wordEnd > c.wordCount
75
+ || !uint(segment.startSample) || !uint(segment.endSample) || segment.startSample < sample || segment.endSample < segment.startSample
76
+ || segment.endSample > c.endSample || (segment.speaker !== 'Ext' && segment.endSample > c.coveredThroughSample) || segment.similarity !== 0
77
+ || (segment.speaker !== 'Ext' && !matches(segment.speaker, /^Voice [1-9]\d?$/))
78
+ || (segment.speaker !== 'Ext' && Number(segment.speaker.slice(6)) > state.labelHighWater)) return false
79
+ word = segment.wordEnd; sample = segment.endSample
80
+ }
81
+ // Empty segments is an explicit clear: clients fall back to immutable canonical labels.
82
+ return c.segments.length === 0 || word === c.wordCount
83
+ }
84
+ /** Strict restore: missing/corrupt high-water or partial arrays never reset numbering. */
85
+ export function validateProjectionState(state: ProjectionState): void {
86
+ if (!state || !keys(state, ['version', 'captureId', 'generation', 'generations', 'speakerRevision', 'labelHighWater', 'slots', 'chunks']) || state.version !== 1 || !matches(state.captureId, MAC) || !uint(state.speakerRevision) || !uint(state.labelHighWater) || state.labelHighWater > 99
87
+ || !dense(state.generations, MAX_GENERATIONS) || new Set(state.generations).size !== state.generations.length
88
+ || state.generations.some(g => typeof g !== 'string' || !matches(g, TOKEN))
89
+ || (state.generation !== null && !state.generations.includes(state.generation)) || !dense(state.slots, 8) || !dense(state.chunks, MAX_CHUNKS)
90
+ || new Set(state.slots.map(s => s?.slot)).size !== state.slots.length || new Set(state.chunks.map(c => c?.chunkIndex)).size !== state.chunks.length) fail('corrupt_state')
91
+ const labels = new Set<string>()
92
+ for (const slot of state.slots) {
93
+ if (!slot || !keys(slot, slot.label === undefined ? ['slot', 'proofs'] : ['slot', 'proofs', 'label']) || !uint(slot.slot) || slot.slot > 7 || !dense(slot.proofs, 3) || slot.proofs.some(p => !proofValid(p))
94
+ || new Set(slot.proofs.map(p => p.originalChunkIndex)).size !== slot.proofs.length
95
+ || new Set(slot.proofs.map(p => p.pcmSHA256)).size !== slot.proofs.length
96
+ || (slot.label !== undefined && (slot.proofs.length !== 3 || !matches(slot.label, /^Voice [1-9]\d?$/)
97
+ || Number(slot.label.slice(6)) > state.labelHighWater || labels.has(slot.label)))) fail('corrupt_slot')
98
+ if (slot.label) labels.add(slot.label)
99
+ }
100
+ const proofs = state.slots.flatMap(slot => slot.proofs)
101
+ for (let i = 0; i < proofs.length; i++) for (let j = i + 1; j < proofs.length; j++) {
102
+ const a = proofs[i], b = proofs[j]
103
+ if (a.originalChunkIndex === b.originalChunkIndex && (a.audioSHA256 !== b.audioSHA256 || a.pcmSHA256 !== b.pcmSHA256
104
+ || a.chunkStartSample !== b.chunkStartSample || a.chunkSampleCount !== b.chunkSampleCount)) fail('corrupt_provenance')
105
+ if (a.originalChunkIndex !== b.originalChunkIndex && Math.max(a.chunkStartSample, b.chunkStartSample)
106
+ < Math.min(a.chunkStartSample + a.chunkSampleCount, b.chunkStartSample + b.chunkSampleCount)) fail('corrupt_overlap')
107
+ const samePCM = a.pcmSHA256 === b.pcmSHA256
108
+ const startA = samePCM ? a.startSample - a.chunkStartSample : a.startSample
109
+ const startB = samePCM ? b.startSample - b.chunkStartSample : b.startSample
110
+ if ((samePCM || a.originalChunkIndex === b.originalChunkIndex) && Math.max(startA, startB)
111
+ < Math.min(startA + a.endSample - a.startSample, startB + b.endSample - b.startSample)) fail('corrupt_repeated_support')
112
+ }
113
+ let totalSegments = 0
114
+ for (const c of state.chunks) {
115
+ if (!c || !Array.isArray(c.segments)) fail('corrupt_chunk')
116
+ totalSegments += c.segments.length
117
+ if (totalSegments > MAX_TOTAL_SEGMENTS || !chunkValid(c, state)) fail('corrupt_chunk_or_bound')
118
+ if (c.generation !== state.generation && c.segments.some(segment => labels.has(segment.speaker))) fail('corrupt_reused_label')
119
+ }
120
+ if (Buffer.byteLength(JSON.stringify(state)) > MAX_STATE_BYTES) fail('state_bound')
121
+ }
122
+ /** All mutation is staged and synchronously atomically persisted before publication. */
123
+ export class MacSpeakerProjection {
124
+ private state: ProjectionState
125
+ private admittedGeneration: string | null = null
126
+ private pending?: ProjectionState
127
+ private committing = false
128
+ constructor(state: ProjectionState, private readonly persist: (state: Readonly<ProjectionState>) => void) {
129
+ validateProjectionState(state); this.state = structuredClone(state)
130
+ }
131
+ snapshot(): ProjectionState { return structuredClone(this.state) }
132
+ private commit(edit: (state: ProjectionState) => void): void {
133
+ if (this.committing) fail('reentrant_persist')
134
+ const next = structuredClone(this.state); edit(next)
135
+ if (next.speakerRevision >= Number.MAX_SAFE_INTEGER) fail('revision_exhausted')
136
+ next.speakerRevision++
137
+ validateProjectionState(next)
138
+ if (this.pending && JSON.stringify(this.pending) !== JSON.stringify(next)) fail('uncommitted')
139
+ this.pending = next
140
+ this.committing = true
141
+ try {
142
+ const accepted: unknown = this.persist(Object.freeze(structuredClone(next)))
143
+ if (accepted && typeof (accepted as { then?: unknown }).then === 'function') fail('async_persist')
144
+ this.state = next
145
+ this.pending = undefined
146
+ } finally { this.committing = false }
147
+ }
148
+ beginGeneration(generation: string): void {
149
+ if (!matches(generation, TOKEN) || this.state.generations.includes(generation) || this.state.generations.length >= MAX_GENERATIONS) fail('generation')
150
+ this.commit(s => { s.generation = generation; s.generations.push(generation); s.slots = [] })
151
+ this.admittedGeneration = generation
152
+ }
153
+ endGeneration(generation: string): void {
154
+ this.assertGeneration(generation)
155
+ this.commit(s => { s.generation = null; s.slots = [] })
156
+ this.admittedGeneration = null
157
+ }
158
+ private assertGeneration(generation: string): void { if (this.state.generation !== generation || this.admittedGeneration !== generation) fail('stale_generation') }
159
+ observe(generation: string, slotIndex: number, proof: ConfirmationProof): string | null {
160
+ this.assertGeneration(generation)
161
+ if (!uint(slotIndex) || slotIndex > 7 || !proofValid(proof)) fail('proof')
162
+ for (const slot of this.state.slots) for (const p of slot.proofs) {
163
+ if (p.originalChunkIndex === proof.originalChunkIndex && (p.audioSHA256 !== proof.audioSHA256 || p.pcmSHA256 !== proof.pcmSHA256
164
+ || p.chunkStartSample !== proof.chunkStartSample || p.chunkSampleCount !== proof.chunkSampleCount)) fail('proof_conflict')
165
+ if (p.originalChunkIndex !== proof.originalChunkIndex && Math.max(p.chunkStartSample, proof.chunkStartSample)
166
+ < Math.min(p.chunkStartSample + p.chunkSampleCount, proof.chunkStartSample + proof.chunkSampleCount)) fail('overlapping_originals')
167
+ }
168
+ const existing = this.state.slots.find(s => s.slot === slotIndex)
169
+ if (existing?.proofs.some(p => p.originalChunkIndex === proof.originalChunkIndex || p.pcmSHA256 === proof.pcmSHA256)) return existing.label ?? null
170
+ // Repeated canonical PCM/overlapping support cannot confirm another slot either.
171
+ for (const slot of this.state.slots) for (const p of slot.proofs) {
172
+ const samePCM = p.pcmSHA256 === proof.pcmSHA256
173
+ const a = samePCM ? p.startSample - p.chunkStartSample : p.startSample
174
+ const b = samePCM ? proof.startSample - proof.chunkStartSample : proof.startSample
175
+ if ((samePCM || p.originalChunkIndex === proof.originalChunkIndex) && Math.max(a, b) < Math.min(a + p.endSample - p.startSample, b + proof.endSample - proof.startSample)) return existing?.label ?? null
176
+ }
177
+ if (existing?.proofs.length === 3) return existing.label ?? null
178
+ this.commit(s => {
179
+ let slot = s.slots.find(item => item.slot === slotIndex)
180
+ if (!slot) { slot = { slot: slotIndex, proofs: [] }; s.slots.push(slot) }
181
+ slot.proofs.push(structuredClone(proof))
182
+ if (slot.proofs.length === 3 && s.labelHighWater < 99) slot.label = `Voice ${++s.labelHighWater}`
183
+ })
184
+ return this.state.slots.find(s => s.slot === slotIndex)?.label ?? null
185
+ }
186
+ /** Whole-chunk replacement; [] explicitly clears prior segments. Text/word anchors never change. */
187
+ replaceChunk(generation: string, chunk: Omit<ChunkProjection, 'generation' | 'changedAtRevision'>): void {
188
+ this.assertGeneration(generation)
189
+ const old = this.state.chunks.find(c => c.chunkIndex === chunk.chunkIndex)
190
+ if (old && chunk.coveredThroughSample < old.coveredThroughSample) fail('coverage_regression')
191
+ if (old && (old.generation !== generation || old.audioSHA256 !== chunk.audioSHA256 || old.canonicalBinding !== chunk.canonicalBinding
192
+ || old.wordCount !== chunk.wordCount || old.startSample !== chunk.startSample || old.endSample !== chunk.endSample)) fail('immutable_binding')
193
+ if (old) {
194
+ const { generation: _generation, changedAtRevision: _revision, ...previous } = old
195
+ if (Object.keys(previous).every(k => JSON.stringify(previous[k as keyof typeof previous]) === JSON.stringify(chunk[k as keyof typeof chunk]))) return
196
+ }
197
+ if (!old && this.state.chunks.length >= MAX_CHUNKS) fail('chunk_bound')
198
+ const validLabels = new Set(this.state.slots.map(s => s.label).filter(Boolean))
199
+ if (!dense(chunk.segments, MAX_WORDS) || chunk.segments.some(s => s.speaker !== 'Ext' && !validLabels.has(s.speaker))) fail('unconfirmed_label')
200
+ this.commit(s => {
201
+ const replacement: ChunkProjection = { ...structuredClone(chunk), generation, changedAtRevision: s.speakerRevision + 1 }
202
+ const i = s.chunks.findIndex(c => c.chunkIndex === chunk.chunkIndex)
203
+ if (i < 0) s.chunks.push(replacement); else s.chunks[i] = replacement
204
+ })
205
+ }
206
+ read(cursor?: { generation: string | null; speakerRevision: number }): ProjectionRead {
207
+ const reset = !cursor || cursor.generation !== this.state.generation || !uint(cursor.speakerRevision) || cursor.speakerRevision > this.state.speakerRevision
208
+ return { version: 1, captureId: this.state.captureId, generation: this.state.generation, speakerRevision: this.state.speakerRevision,
209
+ reset, chunks: structuredClone(this.state.chunks.filter(c => reset || c.changedAtRevision > cursor!.speakerRevision)) }
210
+ }
211
+ }
@@ -0,0 +1,185 @@
1
+ // Isolated protocol/state machine only. No discovery, subprocess, models or live imports.
2
+ import { createHash } from 'node:crypto'
3
+
4
+ export const STREAM_MAX_SAMPLES = 96_000
5
+ export const STREAM_MAX_PCM_BYTES = STREAM_MAX_SAMPLES * 2
6
+ export type StreamOp = 'open' | 'append' | 'finish' | 'abort'
7
+ export interface StreamRequest {
8
+ version: 1; op: StreamOp; captureId: string; generation: string; requestId: string
9
+ startSample?: number; firstChunkIndex?: number; originalChunkIndex?: number; sampleCount?: number
10
+ audioSHA256?: string; pcmSHA256?: string; pcmS16LEBase64?: string
11
+ }
12
+ export interface StreamReceipt {
13
+ version: 1; op: StreamOp; captureId: string; generation: string; requestId: string
14
+ state: 'open' | 'finished' | 'aborted'; consumedThroughSample: number; consumedThroughChunkIndex: number
15
+ frameStart: number; frameCount: number; numSpeakers: 8; probabilities: number[]
16
+ }
17
+ export interface StreamDependencies {
18
+ exchange(request: Readonly<StreamRequest>, signal: AbortSignal): Promise<unknown>
19
+ // Synchronous, atomic durable acceptance. A throw leaves one staged receipt for exact retry.
20
+ // Actual filesystem persistence and cancellation-aware CAS remain future integration work.
21
+ accept(request: Readonly<StreamRequest>, receipt: Readonly<StreamReceipt>): void
22
+ deadlineMs?: number
23
+ }
24
+ const SHA = /^[a-f0-9]{64}$/
25
+ const CAPTURE = /^mac_[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
26
+ const TOKEN = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/
27
+ const matches = (value: unknown, pattern: RegExp): value is string => typeof value === 'string' && pattern.test(value)
28
+ const ENVELOPE = ['version', 'op', 'captureId', 'generation', 'requestId']
29
+ const fields = (op: StreamOp): string[] => [...ENVELOPE, ...(op === 'open' ? ['startSample', 'firstChunkIndex'] : op === 'append' ? ['originalChunkIndex', 'startSample', 'sampleCount', 'audioSHA256', 'pcmSHA256', 'pcmS16LEBase64'] : [])]
30
+ const requestKey = (r: StreamRequest): string => JSON.stringify(Object.fromEntries(fields(r.op).map(k => [k, r[k as keyof StreamRequest]])))
31
+ const integer = (v: unknown): v is number => Number.isSafeInteger(v) && Number(v) >= 0
32
+ const fail = (reason: string): never => { throw new Error(`mac_stream_${reason}`) }
33
+ export function validateStreamRequest(request: StreamRequest): void {
34
+ if (!request || typeof request !== 'object' || Array.isArray(request) || request.version !== 1 || !['open', 'append', 'finish', 'abort'].includes(request.op)
35
+ || !matches(request.captureId, CAPTURE) || !matches(request.generation, TOKEN) || !matches(request.requestId, TOKEN)) fail('request')
36
+ if (Object.keys(request).length !== fields(request.op).length || fields(request.op).some(k => !Object.hasOwn(request, k))) fail('fields')
37
+ if (request.op === 'open') {
38
+ if (!integer(request.startSample) || request.startSample % 160 !== 0 || request.startSample > 1_382_400_000 || !integer(request.firstChunkIndex) || request.firstChunkIndex >= 10_000_000) fail('start_sample')
39
+ } else if (request.op === 'append') {
40
+ if (!integer(request.originalChunkIndex) || !integer(request.startSample) || !integer(request.sampleCount)
41
+ || request.sampleCount < 1 || request.sampleCount > STREAM_MAX_SAMPLES
42
+ || !Number.isSafeInteger(request.startSample + request.sampleCount)
43
+ || !matches(request.audioSHA256, SHA) || !matches(request.pcmSHA256, SHA)) fail('append_metadata')
44
+ const encoded = request.pcmS16LEBase64
45
+ if (typeof encoded !== 'string' || encoded.length > Math.ceil(STREAM_MAX_PCM_BYTES / 3) * 4
46
+ || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) fail('pcm')
47
+ const pcm = Buffer.from(encoded!, 'base64')
48
+ if (pcm.length !== request.sampleCount! * 2 || pcm.toString('base64') !== encoded
49
+ || createHash('sha256').update(pcm).digest('hex') !== request.pcmSHA256) fail('pcm_hash')
50
+ }
51
+ }
52
+ interface Pending {
53
+ request: StreamRequest; resolve(value: StreamReceipt): void; reject(error: unknown): void
54
+ }
55
+ /** One capture generation, one exchange plus one queued request. Never resumable after failure. */
56
+ export class MacStreamDiarizer {
57
+ private state: 'new' | 'open' | 'finished' | 'aborted' | 'failed' = 'new'
58
+ private scope?: { captureId: string; generation: string }
59
+ private watermark = 0
60
+ private initialSample = 0
61
+ private nextIndex = 0
62
+ private nextFrame = 0
63
+ private last?: { key: string; receipt: StreamReceipt }
64
+ private terminal?: { key: string; receipt: StreamReceipt }
65
+ private staged?: { key: string; request: StreamRequest; receipt: StreamReceipt }
66
+ private active = false
67
+ private queue?: Pending
68
+ private controller?: AbortController
69
+ private fence = 0
70
+ private usedRequestIds = new Set<string>()
71
+ private usedPCMHashes = new Set<string>()
72
+ constructor(private readonly deps: StreamDependencies) {
73
+ if (!Number.isSafeInteger(deps.deadlineMs ?? 1000) || (deps.deadlineMs ?? 1000) < 1 || (deps.deadlineMs ?? 1000) > 30_000) fail('deadline')
74
+ }
75
+ get status(): string { return this.state }
76
+ private invalidate(): void {
77
+ this.fence++; this.state = 'failed'; this.controller?.abort()
78
+ this.staged = undefined
79
+ this.queue?.reject(new Error('mac_stream_cancelled')); this.queue = undefined
80
+ }
81
+ /** Owner cancellation is immediate and fences an already-running/late transport callback. */
82
+ cancel(): void { this.invalidate() }
83
+ request(request: StreamRequest): Promise<StreamReceipt> {
84
+ try { validateStreamRequest(request) } catch (error) { return Promise.reject(error) }
85
+ const copy = { ...request }
86
+ if (this.scope && (copy.captureId !== this.scope.captureId || copy.generation !== this.scope.generation)) return Promise.reject(new Error('mac_stream_scope'))
87
+ if (this.active) {
88
+ if (copy.op === 'abort') { this.invalidate(); return Promise.reject(new Error('mac_stream_cancelled')) }
89
+ if (this.queue) { this.invalidate(); return Promise.reject(new Error('mac_stream_queue_overflow')) }
90
+ return new Promise((resolve, reject) => { this.queue = { request: copy, resolve, reject } })
91
+ }
92
+ return this.dispatch(copy)
93
+ }
94
+ private async dispatch(request: StreamRequest): Promise<StreamReceipt> {
95
+ this.active = true
96
+ try { return await this.run(request) } finally {
97
+ this.active = false
98
+ const queued = this.queue; this.queue = undefined
99
+ if (queued) void this.dispatch(queued.request).then(queued.resolve, queued.reject)
100
+ }
101
+ }
102
+ private async run(request: StreamRequest): Promise<StreamReceipt> {
103
+ const key = requestKey(request)
104
+ if (this.state === 'failed') fail('generation_ended')
105
+ if (this.scope && (request.captureId !== this.scope.captureId || request.generation !== this.scope.generation)) fail('scope')
106
+ if (this.terminal) {
107
+ if (this.terminal.key === key) return structuredClone(this.terminal.receipt)
108
+ fail('terminal')
109
+ }
110
+ if (this.last?.key === key) return structuredClone(this.last.receipt)
111
+ if (this.staged && this.staged.key !== key) fail('uncommitted')
112
+ if (!this.staged) {
113
+ if (this.usedRequestIds.has(request.requestId)) fail('request_conflict_or_evicted')
114
+ if (request.op === 'append' && (this.usedPCMHashes.has(request.pcmSHA256!) || this.usedPCMHashes.size >= 4096)) fail('repeated_pcm_or_bound')
115
+ if (request.op === 'open') {
116
+ if (this.state !== 'new') fail('already_open')
117
+ this.scope = { captureId: request.captureId, generation: request.generation }
118
+ } else {
119
+ if (this.state !== 'open') fail('not_open')
120
+ if (request.op === 'append' && (request.startSample !== this.watermark
121
+ || request.originalChunkIndex !== this.nextIndex)) fail('sequence')
122
+ }
123
+ const fence = this.fence
124
+ const controller = this.controller = new AbortController()
125
+ let timer: ReturnType<typeof setTimeout> | undefined
126
+ try {
127
+ const raw = await Promise.race([
128
+ this.deps.exchange(Object.freeze({ ...request }), controller.signal),
129
+ new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error('mac_stream_timeout')), this.deps.deadlineMs ?? 1000) }),
130
+ new Promise<never>((_, reject) => controller.signal.addEventListener('abort', () => reject(new Error('mac_stream_cancelled')), { once: true })),
131
+ ])
132
+ if (fence !== this.fence || controller.signal.aborted) fail('cancelled')
133
+ const receipt = this.validateReceipt(request, raw)
134
+ this.staged = { key, request, receipt }
135
+ } catch (error) { this.invalidate(); throw error } finally { if (timer) clearTimeout(timer); this.controller = undefined }
136
+ }
137
+ const staged = this.staged!
138
+ // No await between the cancellation fence, atomic durable acceptance and local publication.
139
+ const fence = this.fence
140
+ const accepted: unknown = this.deps.accept(Object.freeze({ ...staged.request }), Object.freeze(structuredClone(staged.receipt)))
141
+ if (accepted && typeof (accepted as { then?: unknown }).then === 'function') { this.invalidate(); fail('async_accept') }
142
+ if (fence !== this.fence) fail('cancelled')
143
+ const receipt = staged.receipt
144
+ this.scope = { captureId: request.captureId, generation: request.generation }
145
+ this.watermark = receipt.consumedThroughSample
146
+ this.nextFrame = receipt.frameStart + receipt.frameCount
147
+ this.usedRequestIds.add(request.requestId)
148
+ if (request.op === 'append') this.usedPCMHashes.add(request.pcmSHA256!)
149
+ if (request.op === 'open') { this.nextIndex = request.firstChunkIndex!; this.initialSample = request.startSample! }
150
+ if (request.op === 'append') this.nextIndex = request.originalChunkIndex! + 1
151
+ this.state = receipt.state
152
+ this.last = { key, receipt: structuredClone(receipt) }
153
+ if (request.op === 'finish' || request.op === 'abort') this.terminal = this.last
154
+ this.staged = undefined
155
+ return structuredClone(receipt)
156
+ }
157
+ private validateReceipt(request: StreamRequest, raw: unknown): StreamReceipt {
158
+ if (!raw || typeof raw !== 'object') fail('receipt')
159
+ const r = raw as StreamReceipt
160
+ const expectedEnd = request.op === 'open' ? request.startSample! : request.op === 'append' ? request.startSample! + request.sampleCount! : this.watermark
161
+ const expectedFrame = request.op === 'open' ? request.startSample! / 160 : this.nextFrame
162
+ const expectedIndex = request.op === 'open' ? request.firstChunkIndex! - 1 : request.op === 'append' ? request.originalChunkIndex! : this.nextIndex - 1
163
+ const expectedState = request.op === 'finish' ? 'finished' : request.op === 'abort' ? 'aborted' : 'open'
164
+ if (r.version !== 1 || r.op !== request.op || r.captureId !== request.captureId || r.generation !== request.generation
165
+ || r.requestId !== request.requestId || r.state !== expectedState || r.consumedThroughChunkIndex !== expectedIndex || r.consumedThroughSample !== expectedEnd
166
+ || r.frameStart !== expectedFrame || !integer(r.frameCount) || r.frameCount > 900 || r.numSpeakers !== 8
167
+ || !Array.isArray(r.probabilities) || r.probabilities.length !== r.frameCount * 8
168
+ || (request.op === 'open' || request.op === 'abort') && r.frameCount !== 0
169
+ || (r.frameStart + r.frameCount) * 160 > expectedEnd + (request.op === 'finish' ? 160 : 0)) fail('receipt_shape')
170
+ if (request.op === 'finish') {
171
+ const initial = this.initialSample
172
+ const count = expectedEnd - initial
173
+ const exactFrames = count > 0 ? 1 + Math.floor((count + 512 - 400) / 160) : 0
174
+ if (r.frameStart + r.frameCount !== initial / 160 + exactFrames) fail('finish_coverage')
175
+ }
176
+ for (let i = 0; i < r.probabilities.length; i++) {
177
+ const p = r.probabilities[i]
178
+ if (!Object.hasOwn(r.probabilities, i) || !Number.isFinite(p) || p < 0 || p > 1) fail('probability')
179
+ }
180
+ // Copy only protocol fields: ignored native instrumentation/extra payload cannot grow retained state.
181
+ return { version: 1, op: r.op, captureId: r.captureId, generation: r.generation, requestId: r.requestId, state: r.state,
182
+ consumedThroughSample: r.consumedThroughSample, consumedThroughChunkIndex: r.consumedThroughChunkIndex,
183
+ frameStart: r.frameStart, frameCount: r.frameCount, numSpeakers: 8, probabilities: r.probabilities.slice() }
184
+ }
185
+ }