@gotcos/glasses-server 6.95.0 → 6.98.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 (123) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +4 -0
  3. package/managed-runtime-contract.json +13 -0
  4. package/package.json +3 -1
  5. package/server/index.ts +12 -0
  6. package/server/lib/attached-provider-adapter.ts +35 -1
  7. package/server/lib/claude-bridge.ts +47 -0
  8. package/server/lib/codex-bridge.ts +11 -1
  9. package/server/lib/codex-rollout-watermark.ts +42 -0
  10. package/server/lib/cursor-bridge.ts +2 -1
  11. package/server/lib/decision-provider.ts +4 -2
  12. package/server/lib/dictation-clean-models.ts +2 -0
  13. package/server/lib/dictation-clean.ts +2 -2
  14. package/server/lib/dictation-polish-pipeline.ts +22 -2
  15. package/server/lib/dictation-prewarm.ts +16 -0
  16. package/server/lib/direct-bridge.ts +14 -2
  17. package/server/lib/direct-sse.ts +6 -1
  18. package/server/lib/display-bus.ts +1 -1
  19. package/server/lib/fork-thread.ts +32 -1
  20. package/server/lib/held-naming-batches.ts +44 -7
  21. package/server/lib/held-voice-groups.ts +5 -4
  22. package/server/lib/job-audit-caller.ts +20 -0
  23. package/server/lib/keyed-sessions.ts +21 -1
  24. package/server/lib/lens-gist-engines.ts +17 -5
  25. package/server/lib/live-cos-ask.ts +29 -6
  26. package/server/lib/live-cue-verdicts.ts +43 -8
  27. package/server/lib/live-cues-engine.ts +4 -1
  28. package/server/lib/live-cues-filter.ts +50 -0
  29. package/server/lib/live-cues-general-prompts.ts +137 -0
  30. package/server/lib/live-cues-general-topic.ts +245 -0
  31. package/server/lib/live-cues-general.ts +715 -0
  32. package/server/lib/live-cues-v2-data.ts +453 -0
  33. package/server/lib/live-cues-v2-grounded.ts +262 -0
  34. package/server/lib/live-cues-v2-judge-b.ts +59 -0
  35. package/server/lib/live-cues-v2-names.ts +49 -0
  36. package/server/lib/live-cues-v2-notes.ts +8 -4
  37. package/server/lib/live-cues-v2-open-items.ts +132 -0
  38. package/server/lib/live-cues-v2-pacing.ts +45 -0
  39. package/server/lib/live-cues-v2-prompts-kinds.ts +9 -4
  40. package/server/lib/live-cues-v2-settings.ts +89 -10
  41. package/server/lib/live-cues-v2.ts +570 -47
  42. package/server/lib/mac-recording-asr.ts +6 -3
  43. package/server/lib/mac-speaker-call-human.ts +371 -0
  44. package/server/lib/mac-speaker-call-interval-human.ts +499 -0
  45. package/server/lib/mac-speaker-call-interval-projection.ts +233 -0
  46. package/server/lib/mac-speaker-call-original-view.ts +328 -0
  47. package/server/lib/mac-speaker-call-producer-worker.ts +615 -0
  48. package/server/lib/mac-speaker-call-producer.ts +539 -0
  49. package/server/lib/mac-speaker-call-store.ts +264 -0
  50. package/server/lib/mac-speaker-call-view.ts +373 -0
  51. package/server/lib/mac-speaker-live-compute.ts +72 -0
  52. package/server/lib/mac-speaker-live-memory.ts +414 -0
  53. package/server/lib/mac-speaker-live-projection.ts +126 -0
  54. package/server/lib/mac-speaker-live-save.ts +90 -0
  55. package/server/lib/mac-speaker-live-store.ts +71 -0
  56. package/server/lib/mac-speaker-live-worker.ts +58 -0
  57. package/server/lib/mac-speaker-live.ts +175 -0
  58. package/server/lib/mac-speaker-profile-extractor.ts +15 -10
  59. package/server/lib/mac-speaker-shadow-repair.ts +307 -0
  60. package/server/lib/mac-voice-assignment.ts +46 -27
  61. package/server/lib/mac-voice-display.ts +195 -0
  62. package/server/lib/mac-voice-naming-batch.ts +1067 -0
  63. package/server/lib/mac-voice-suggestions.ts +548 -0
  64. package/server/lib/maintenance-lifecycle.ts +2 -0
  65. package/server/lib/media-store.ts +2 -1
  66. package/server/lib/meeting-ask-pasted.ts +135 -0
  67. package/server/lib/meeting-ask-review.ts +8 -1
  68. package/server/lib/meeting-ask-runner.ts +4 -1
  69. package/server/lib/meeting-ask-runtime.ts +189 -17
  70. package/server/lib/meeting-ask-store.ts +2 -0
  71. package/server/lib/meeting-ask-web.ts +284 -13
  72. package/server/lib/meeting-batch-progress.ts +51 -5
  73. package/server/lib/meeting-batch-transcribe.ts +35 -2
  74. package/server/lib/meeting-corrections.ts +2 -1
  75. package/server/lib/meeting-fact-check.ts +3 -0
  76. package/server/lib/meeting-finalization-jobs.ts +69 -2
  77. package/server/lib/meeting-public-topic.ts +6 -1
  78. package/server/lib/meeting-relabel-enrolment.ts +2 -1
  79. package/server/lib/meeting-relabel.ts +17 -10
  80. package/server/lib/meeting-speaker-review.ts +187 -21
  81. package/server/lib/meeting-store.ts +6 -1
  82. package/server/lib/parakeet.ts +51 -5
  83. package/server/lib/pending-batch-retention.ts +58 -0
  84. package/server/lib/private-reference.ts +38 -0
  85. package/server/lib/provider-usage.ts +561 -0
  86. package/server/lib/public-https-url.ts +32 -0
  87. package/server/lib/query-job-runtime.ts +3 -0
  88. package/server/lib/rich-media-safety.ts +23 -2
  89. package/server/lib/session-find-ask.ts +288 -0
  90. package/server/lib/session-find-collect.ts +715 -0
  91. package/server/lib/session-find-redact.ts +254 -0
  92. package/server/lib/session-find.ts +345 -0
  93. package/server/lib/speaker-calibration-log.ts +3 -2
  94. package/server/lib/speaker-embeddings.ts +46 -35
  95. package/server/lib/speaker-name-nfc.ts +37 -0
  96. package/server/lib/speaker-rename-fanout.ts +7 -6
  97. package/server/lib/speaker-shadow-decide.ts +59 -22
  98. package/server/lib/speaker-shadow-pair-source.ts +91 -0
  99. package/server/lib/speaker-shadow-paired.ts +268 -0
  100. package/server/lib/speaker-shadow-report.ts +2 -2
  101. package/server/lib/speaker-shadow-worker.ts +50 -15
  102. package/server/lib/speaker-shadow.ts +80 -19
  103. package/server/lib/speaker-trainer.ts +2 -1
  104. package/server/lib/token-audit.ts +36 -6
  105. package/server/lib/transcript-cleanup.ts +3 -2
  106. package/server/lib/voice-directory.ts +10 -5
  107. package/server/lib/voice-profile-store.ts +24 -14
  108. package/server/lib/web-lookup-ledger.ts +151 -0
  109. package/server/lib/work-review-runtime.ts +2 -0
  110. package/server/lib/work-search.ts +3 -1
  111. package/server/lib/xlsx-text.ts +277 -0
  112. package/server/routes/decision-provider.ts +4 -1
  113. package/server/routes/jev.ts +5 -1
  114. package/server/routes/live-cues.ts +36 -14
  115. package/server/routes/meeting.ts +515 -61
  116. package/server/routes/session-find.ts +73 -0
  117. package/server/routes/transcribe-stream.ts +114 -33
  118. package/server/routes/voice.ts +28 -18
  119. package/server/scripts/live-cues-v3-mutation-gate.mjs +311 -0
  120. package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +5 -5
  121. package/shared/live-cues-v2.ts +38 -5
  122. package/shared/media-attachment.ts +2 -0
  123. package/shared/meeting-ask.ts +11 -0
@@ -0,0 +1,90 @@
1
+ /** Pure bridge from qualified live evidence to the existing closed-file repair
2
+ * contract. No IO, inference, enrollment, or implied authorization to write.
3
+ * Caller must verify current audio/model/profile/ledger bytes and journal all
4
+ * files before applying the returned receipt. */
5
+ import { refreshMacSpeakerLiveState, replayMacSpeakerLiveEvidence, validateMacSpeakerLiveState, hashMacSpeakerLive, type MacSpeakerLiveState } from './mac-speaker-live-memory.js'
6
+ import { projectMacSpeakerLiveChunks, type MacSpeakerLiveSourceChunk } from './mac-speaker-live-projection.js'
7
+ import { normalizedSpeakerCosine, type MacSpeakerProfile } from './mac-speaker-reconcile.js'
8
+ import { hashMacSpeakerDocument, projectMacSpeakerRepair, type MacProfileMatchEvidence, type MacSpeakerRepairAssignment, type MacSpeakerRepairPlan, type MacSpeakerWordBinding } from './mac-speaker-repair-projection.js'
9
+ type Doc = Record<string, any>
10
+ const refuse = (why: string): never => { throw Error(`Mac live save: ${why}`) }
11
+ const MAX_RAW = 8192
12
+ /** Verified PCM counts include silent originals, because saved batch word
13
+ * clocks concatenate audio files rather than using capture elapsed time. */
14
+ export function planMacSpeakerLiveSave(input: {
15
+ doc: Doc; state: MacSpeakerLiveState; profiles: readonly MacSpeakerProfile[]
16
+ rawAudioSampleCounts: Record<string, number>; correctionHistoryClear: boolean
17
+ }): MacSpeakerRepairPlan {
18
+ const { doc, state, profiles, rawAudioSampleCounts: counts } = input
19
+ validateMacSpeakerLiveState(state)
20
+ if (!input.correctionHistoryClear || state.withheldReason || doc.sessionId !== state.captureId
21
+ || doc.finalizationState !== 'complete' || doc.sourceType !== 'mac_system_audio'
22
+ || doc.speakerRepair || (doc.correctionRevision ?? 0) !== 0 || doc.batchApplied !== true
23
+ || !Array.isArray(doc.chunkEntries) || !Array.isArray(doc.batchSegments)
24
+ || doc.chunkEntries.length > MAX_RAW || !doc.batchSegments.length) refuse('ineligible source or human history')
25
+ // An unkeyed integrity hash cannot authenticate caller-supplied membership.
26
+ // Rebuild membership from each source receipt; namespace reservation only
27
+ // affects presentation labels, not acoustic memory or receipt assignment.
28
+ const rebuilt = replayMacSpeakerLiveEvidence({ captureId: state.captureId, generation: state.generation,
29
+ modelSHA256: state.modelSHA256, profileSHA256: state.profileSHA256, evidence: state.evidence, profiles })
30
+ if (hashMacSpeakerLive(rebuilt.memory) !== hashMacSpeakerLive(state.memory)
31
+ || hashMacSpeakerLive(rebuilt.evidence.map(e => e.cohortId ?? null)) !== hashMacSpeakerLive(state.evidence.map(e => e.cohortId ?? null))) refuse('unverified receipt membership')
32
+ // Recompute the complete overlay against the frozen full profile cohort.
33
+ const replay = refreshMacSpeakerLiveState(state, profiles)
34
+ if (hashMacSpeakerLive(replay.projections) !== hashMacSpeakerLive(state.projections)) refuse('stale identity or profile claims')
35
+ const indexed = doc.chunkEntries.filter((e: Doc) => e.chunk?.text).map((e: Doc) => ({ i: e.chunkIndex, c: e.chunk as MacSpeakerLiveSourceChunk }))
36
+ const projection = projectMacSpeakerLiveChunks({ captureId: state.captureId, generation: state.generation, chunksIndexed: indexed, projections: state.projections })
37
+ if (projection.withheld.length || projection.appliedRawChunkIndices.length !== state.projections.length) refuse('saved originals changed or became protected')
38
+ const current = new Map<number, Doc>(doc.chunkEntries.map((e: Doc) => [e.chunkIndex, e.chunk]))
39
+ const centroids = profiles.map(p => ({ name: p.name, embedding: Array.from({ length: 192 }, (_, i) => p.embeddings.reduce((sum, v) => sum + v[i] / p.embeddings.length, 0)) }))
40
+ const proofs = new Map<string, MacProfileMatchEvidence>()
41
+ for (const span of state.projections) if (span.profileName && !proofs.has(span.identityId)) {
42
+ const identity = state.identities.find(i => i.identityId === span.identityId)!
43
+ const group = state.evidence.filter(e => e.cohortId === identity.cohortId)
44
+ const target = centroids.find(p => p.name === span.profileName) ?? refuse('profile missing')
45
+ proofs.set(span.identityId, {
46
+ kind: 'profile-match', profileName: span.profileName, profileSha256: state.profileSHA256,
47
+ modelSha256: state.modelSHA256, algorithmVersion: 'mac-native-live-v1',
48
+ crossWindowCosines: group.map(a => group.map(b => normalizedSpeakerCosine(a.receipt.embedding, b.receipt.embedding)!)),
49
+ windows: group.map(e => ({ id: e.window.id, captureStartSample: e.window.startSample, captureEndSample: e.window.endSample,
50
+ pcmSha256: e.receipt.pcmSHA256, similarity: normalizedSpeakerCosine(e.receipt.embedding, target.embedding)!,
51
+ runnerUpSimilarity: Math.max(-1, ...centroids.filter(p => p !== target).map(p => normalizedSpeakerCosine(e.receipt.embedding, p.embedding)!)),
52
+ rawChunks: e.window.sources.map(s => ({ rawChunkIndex: s.chunkIndex, audioSha256: s.audioSHA256, sampleCount: s.sampleCount })) })),
53
+ })
54
+ }
55
+ const assignments: MacSpeakerRepairAssignment[] = state.projections.flatMap(span => {
56
+ const original = current.get(span.rawChunkIndex) ?? refuse('missing original')
57
+ if (original.speaker === span.displayLabel) return []
58
+ return [{ rawChunkIndex: span.rawChunkIndex, from: original.speaker, to: span.displayLabel,
59
+ evidenceId: span.identityId, batchWords: [], ...(span.profileName ? { targetEvidence: proofs.get(span.identityId)! } : {}) }]
60
+ })
61
+ if (!assignments.length) refuse('no qualified label changes')
62
+ const selected = new Map(assignments.map(a => [a.rawChunkIndex, a]))
63
+ const withheldBatchWords: Array<MacSpeakerWordBinding & { reason: 'outside-qualified-span' }> = []
64
+ doc.batchSegments.forEach((segment: Doc, segmentIndex: number) => {
65
+ if (!Number.isSafeInteger(segment.startChunkIdx) || !Number.isSafeInteger(segment.endChunkIdx)
66
+ || segment.startChunkIdx < 0 || segment.endChunkIdx < segment.startChunkIdx
67
+ || segment.endChunkIdx - segment.startChunkIdx >= MAX_RAW || !Array.isArray(segment.speakerWords)) refuse('invalid batch geometry')
68
+ const ranges: Array<{ raw: number; start: number; end: number }> = []; let at = 0
69
+ for (let raw = segment.startChunkIdx; raw <= segment.endChunkIdx; raw++) {
70
+ const n = counts[String(raw)]
71
+ if (!Number.isSafeInteger(n) || n <= 0 || n > 480000) refuse('unverified batch PCM clock')
72
+ ranges.push({ raw, start: at, end: at + n / 16000 }); at += n / 16000
73
+ }
74
+ segment.speakerWords.forEach((word: Doc, wordIndex: number) => {
75
+ if (!Number.isFinite(word.start) || !Number.isFinite(word.end) || word.start < 0 || word.end < word.start || word.end > at) refuse('invalid batch word clock')
76
+ const overlapping = ranges.filter(r => word.start === word.end ? word.start >= r.start && word.start < r.end : word.start < r.end && word.end > r.start)
77
+ if (!overlapping.some(r => selected.has(r.raw))) return
78
+ const binding = { segmentIndex, wordIndex, from: word.speaker, word: word.word, start: word.start, end: word.end }
79
+ if (overlapping.some(r => !selected.has(r.raw))) withheldBatchWords.push({ ...binding, reason: 'outside-qualified-span' })
80
+ else if (new Set(overlapping.map(r => selected.get(r.raw)!.to)).size !== 1) refuse('word crosses two identities')
81
+ else selected.get(overlapping[0].raw)!.batchWords.push(binding)
82
+ })
83
+ })
84
+ const plan: MacSpeakerRepairPlan = { id: `live_${state.stateSHA256.slice(0, 24)}`, evidenceHash: state.stateSHA256,
85
+ expectedDocumentHash: hashMacSpeakerDocument(doc), protectedRawChunkIndices: [], rawAudioSampleCounts: counts, assignments, withheldBatchWords }
86
+ // Exercise all existing named-evidence and exact word/time protection gates
87
+ // here; callers cannot accidentally treat plan construction as qualification.
88
+ projectMacSpeakerRepair(doc, plan)
89
+ return plan
90
+ }
@@ -0,0 +1,71 @@
1
+ /** Pure durable publication preparation. The caller writes this replacement and
2
+ * originals atomically before committing in-memory state or emitting an event. */
3
+ import { createMacSpeakerLiveState, MAC_SPEAKER_LIVE_TERMINAL_REVISION, hashMacSpeakerLive, validateMacSpeakerLiveState, withholdMacSpeakerLiveState, type MacSpeakerLiveState } from './mac-speaker-live-memory.js'
4
+ import { projectMacSpeakerLiveChunks, type MacSpeakerLiveSourceChunk } from './mac-speaker-live-projection.js'
5
+ const ZERO='0'.repeat(64)
6
+ export function macSpeakerLiveVoiceHighWater(chunks:readonly unknown[],minimum=0):number{
7
+ let maximum=minimum
8
+ const visit=(v:unknown):void=>{if(!v||typeof v!=='object')return
9
+ const row=v as Record<string,unknown>
10
+ for(const key of ['speaker','originalSpeaker'])if(typeof row[key]==='string'){
11
+ const match=/^Voice\s*([1-9]\d?)$/i.exec(row[key] as string);if(match)maximum=Math.max(maximum,Number(match[1]))
12
+ }
13
+ for(const key of ['segments','finalSegments','words'])if(Array.isArray(row[key]))for(const child of row[key] as unknown[])visit(child)
14
+ }
15
+ for(const c of chunks)visit(c)
16
+ return maximum
17
+ }
18
+ function reseal(state:MacSpeakerLiveState):MacSpeakerLiveState {const{stateSHA256,...body}=state;return{...body,stateSHA256:hashMacSpeakerLive(body)}}
19
+ export function prepareMacSpeakerLivePublication(input:{captureId:string;generation:string;chunksIndexed:readonly {i:number;c:MacSpeakerLiveSourceChunk & Record<string,unknown>}[];state?:MacSpeakerLiveState;validated:boolean;revision:number;previousDigest?:string}){
20
+ let state=input.state,invalid=false
21
+ const unsafeCounter=!Number.isSafeInteger(input.revision)||input.revision<0||input.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION-1
22
+ const revision=unsafeCounter?MAC_SPEAKER_LIVE_TERMINAL_REVISION-1:input.revision
23
+ if(state){try{validateMacSpeakerLiveState(state);if(state.captureId!==input.captureId||state.generation!==input.generation)throw Error('capture_generation_drift')}
24
+ catch{invalid=true;state=withholdMacSpeakerLiveState(createMacSpeakerLiveState(input.captureId,input.generation,ZERO,ZERO,99),'recovered_state_invalid')}}
25
+ if(unsafeCounter||state&&state.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION-1){
26
+ state=withholdMacSpeakerLiveState(createMacSpeakerLiveState(input.captureId,input.generation,ZERO,ZERO,99),'revision_counter_exhausted')
27
+ state.revision=MAC_SPEAKER_LIVE_TERMINAL_REVISION;state=reseal(state)
28
+ }
29
+ if(state){state=structuredClone(state);if(!input.validated||invalid)state.projections=[];state=reseal(state)}
30
+ let overlay:ReturnType<typeof projectMacSpeakerLiveChunks>
31
+ try{overlay=projectMacSpeakerLiveChunks({captureId:input.captureId,generation:input.generation,chunksIndexed:input.chunksIndexed,projections:state?.projections??[]})}
32
+ catch(error){
33
+ // Projection conflicts (including a newly arrived legacy Voice collision)
34
+ // revoke the complete overlay. Never leave the prior durable claim visible.
35
+ if(!state?.projections.length)throw error
36
+ state.projections=[];state=reseal(state)
37
+ overlay=projectMacSpeakerLiveChunks({captureId:input.captureId,generation:input.generation,chunksIndexed:input.chunksIndexed,projections:[]})
38
+ }
39
+ // A valid state must never advertise proof for rows the source projector refused.
40
+ if(state&&overlay.withheld.length&&state.projections.length){state.projections=[];state=reseal(state);overlay=projectMacSpeakerLiveChunks({captureId:input.captureId,generation:input.generation,chunksIndexed:input.chunksIndexed,projections:[]})}
41
+ const changed=input.previousDigest!==overlay.effectiveOverlaySHA256
42
+ // A prior claim already at the terminal cursor has no representable newer
43
+ // withdrawal. Refuse adoption; recovery preserves it outside active claims.
44
+ if(input.revision===MAC_SPEAKER_LIVE_TERMINAL_REVISION&&changed&&input.previousDigest!==undefined)throw Error('live_revision_exhausted')
45
+ if(!state&&input.previousDigest!==undefined&&changed&&revision>0){
46
+ invalid=true;state=withholdMacSpeakerLiveState(createMacSpeakerLiveState(input.captureId,input.generation,ZERO,ZERO,99),'recovered_state_missing')
47
+ }
48
+ const nextRevision=Math.max(state?.revision??0,revision+(changed||invalid?1:0))
49
+ if(nextRevision>MAC_SPEAKER_LIVE_TERMINAL_REVISION)throw Error('live_revision_exhausted')
50
+ if(state){state.revision=nextRevision;state=reseal(state);validateMacSpeakerLiveState(state)}
51
+ return{speakerIdentityPolicy:'mac-native-v1' as const,sourceType:'mac_system_audio' as const,speakerGeneration:input.generation,speakerRevision:nextRevision,
52
+ speakerProjectionSHA256:overlay.effectiveOverlaySHA256,speakerProjectedChunksIndexed:overlay.speakerProjectedChunksIndexed,
53
+ ...(state?{macSpeakerLive:state}:{}),...(invalid?{macSpeakerLiveQuarantineSHA256:hashMacSpeakerLive(input.state??null)}:{}),invalid,
54
+ sourceBindingSHA256:overlay.sourceBindingSHA256}
55
+ }
56
+
57
+ /** Main-thread withdrawal path. Never validates, clones or hashes retained
58
+ * acoustic state. That state is private/untrusted until the worker certifies it. */
59
+ export function macSpeakerLiveOriginalOnly(input:{captureId:string;generation:string;chunksIndexed:readonly {i:number;c:Record<string,unknown>}[];state?:MacSpeakerLiveState;revision:number;previousDigest?:string}){
60
+ const digest=hashMacSpeakerLive({captureId:input.captureId,generation:input.generation,changes:[]}),changed=input.previousDigest!==digest
61
+ if(input.revision===MAC_SPEAKER_LIVE_TERMINAL_REVISION&&changed&&input.previousDigest!==undefined)throw Error('live_revision_exhausted')
62
+ const unsafe=!Number.isSafeInteger(input.revision)||input.revision<0||input.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION-1
63
+ const missing=!input.state&&input.previousDigest!==undefined&&changed&&input.revision>0
64
+ let terminal:MacSpeakerLiveState|undefined
65
+ if(unsafe||missing){terminal=withholdMacSpeakerLiveState(createMacSpeakerLiveState(input.captureId,input.generation,ZERO,ZERO,99),unsafe?'revision_counter_exhausted':'recovered_state_missing')
66
+ terminal.revision=unsafe?MAC_SPEAKER_LIVE_TERMINAL_REVISION:input.revision+1;terminal=reseal(terminal)}
67
+ const revision=terminal?.revision??input.revision+(changed?1:0)
68
+ return{speakerIdentityPolicy:'mac-native-v1' as const,sourceType:'mac_system_audio' as const,speakerGeneration:input.generation,speakerRevision:revision,speakerProjectionSHA256:digest,
69
+ speakerProjectedChunksIndexed:input.chunksIndexed,...(terminal?{macSpeakerLive:terminal}:{}),...(!terminal&&input.state?{macSpeakerLiveRetainedState:input.state}:{}),invalid:false,
70
+ sourceBindingSHA256:hashMacSpeakerLive({captureId:input.captureId,generation:input.generation,originals:input.chunksIndexed})}
71
+ }
@@ -0,0 +1,58 @@
1
+ /** Pure computation entry. No filesystem, native models, enrollment or writes. */
2
+ import { createHash } from 'node:crypto'
3
+ import { hasMacSpeakerProtectedRows } from './mac-speaker-protection.js'
4
+ import { isMainThread, parentPort } from 'node:worker_threads'
5
+ import { planMacSpeakerWindows, type VerifiedMacRawChunk, type PreparedMacSpeakerWindow, type MacWindowEmbeddingReceipt } from './mac-speaker-windows.js'
6
+ import { createMacSpeakerLiveState, appendMacSpeakerLiveEvidence, refreshMacSpeakerLiveState, validateMacSpeakerLiveState, validateMacSpeakerLiveProfiles, withholdMacSpeakerLiveState, hashMacSpeakerLive, type MacSpeakerLiveState } from './mac-speaker-live-memory.js'
7
+ import { prepareMacSpeakerLivePublication } from './mac-speaker-live-store.js'
8
+ import type { MacSpeakerProfile } from './mac-speaker-reconcile.js'
9
+ import type { SidecarEntry } from './nemotron-final.js'
10
+ import type { MacSpeakerLiveSourceChunk } from './mac-speaker-live-projection.js'
11
+ export interface MacSpeakerLiveComputeInput {
12
+ captureId:string;generation:string;sourceBindingSHA256:string;fileBindingsSHA256:string;revision:number;previousDigest?:string;voiceHighWater:number
13
+ state?:MacSpeakerLiveState;modelSHA256:string;profileSHA256:string;profileJSONBytes:Uint8Array
14
+ chunksIndexed:readonly {i:number;c:Record<string,unknown>}[];rawChunks:VerifiedMacRawChunk[];humanHistory:boolean
15
+ }
16
+ export interface MacSpeakerLiveComputePrepared {window?:PreparedMacSpeakerWindow;plannedWindows:number;pendingWindows:number;rejectedChunks:number;reason?:string}
17
+ export type MacSpeakerLivePreparedPublication=ReturnType<typeof prepareMacSpeakerLivePublication>
18
+ export interface MacSpeakerLiveComputeResult {
19
+ state:MacSpeakerLiveState;publication:MacSpeakerLivePreparedPublication;reason:string;plannedWindows:number;pendingWindows:number;rejectedChunks:number;completeEligibleSnapshot:boolean
20
+ }
21
+ export function prepareMacSpeakerLiveCompute(input:MacSpeakerLiveComputeInput){
22
+ if(input.profileJSONBytes.byteLength>32*1024*1024||input.chunksIndexed.length>8192||input.rawChunks.length>8192||hashMacSpeakerLive(input.chunksIndexed)!==input.sourceBindingSHA256)throw Error('compute_input_binding')
23
+ if(createHash('sha256').update(input.profileJSONBytes).digest('hex')!==input.profileSHA256)throw Error('compute_profile_bytes_changed')
24
+ const parsed=JSON.parse(Buffer.from(input.profileJSONBytes).toString()) as {profiles?:MacSpeakerProfile[]}
25
+ if(!Array.isArray(parsed.profiles))throw Error('profile_cohort_invalid')
26
+ const profiles=parsed.profiles.map(p=>({...p,profileSHA256:input.profileSHA256}));validateMacSpeakerLiveProfiles(profiles,input.profileSHA256)
27
+ let state=input.state
28
+ if(state){try{validateMacSpeakerLiveState(state);if(state.captureId!==input.captureId||state.generation!==input.generation)throw Error('capture_generation_drift')}catch{state=withholdMacSpeakerLiveState(createMacSpeakerLiveState(input.captureId,input.generation,input.modelSHA256,input.profileSHA256,99),'recovered_state_invalid')}}
29
+ state??=createMacSpeakerLiveState(input.captureId,input.generation,input.modelSHA256,input.profileSHA256,input.voiceHighWater)
30
+ if(state.modelSHA256!==input.modelSHA256||state.profileSHA256!==input.profileSHA256)state=withholdMacSpeakerLiveState(state,'model_or_profile_drift')
31
+ if(input.humanHistory||input.chunksIndexed.some(e=>hasMacSpeakerProtectedRows(e.c)))state=withholdMacSpeakerLiveState(state,'human_history')
32
+ const plan=state.withheldReason?null:planMacSpeakerWindows({sidecar:{sessionId:input.captureId,chunkEntries:input.chunksIndexed.map(e=>({chunkIndex:e.i,chunk:e.c}))as unknown as SidecarEntry[]},rawChunks:input.rawChunks,correctionHistory:'verified-clear'})
33
+ if(plan&&state.evidence.some(e=>!plan.windows.some(w=>w.id===e.window.id)))throw Error('retained_source_plan_drift')
34
+ const pending=plan?.windows.filter(w=>!state!.evidence.some(e=>e.window.id===w.id))??[]
35
+ const prepared:MacSpeakerLiveComputePrepared={...(pending[0]?{window:pending[0]}:{}),plannedWindows:plan?.windows.length??0,pendingWindows:pending.length,rejectedChunks:plan?.rejected.reduce((n,r)=>n+r.chunkIndexes.length,0)??0,...(state.withheldReason?{reason:state.withheldReason}:{})}
36
+ let finished=false
37
+ return{prepared,finish(receipt?:MacWindowEmbeddingReceipt):MacSpeakerLiveComputeResult{
38
+ if(finished)throw Error('compute_already_finished');finished=true
39
+ let remaining=pending.length
40
+ if(receipt){if(!pending[0]||receipt.windowId!==pending[0].id)throw Error('compute_unrequested_receipt')
41
+ state=appendMacSpeakerLiveEvidence(state!,{window:pending[0],receipt,nativeReceiptSHA256:hashMacSpeakerLive({window:pending[0],receipt})},profiles,input.voiceHighWater);remaining--
42
+ }else if(!pending.length)state=refreshMacSpeakerLiveState(state!,profiles)
43
+ const complete=!state!.withheldReason&&remaining===0
44
+ const publication=prepareMacSpeakerLivePublication({captureId:input.captureId,generation:input.generation,chunksIndexed:input.chunksIndexed as {i:number;c:MacSpeakerLiveSourceChunk&Record<string,unknown>}[],state,validated:complete,revision:input.revision,previousDigest:input.previousDigest})
45
+ state=publication.macSpeakerLive??state!
46
+ return{state,publication,reason:state.withheldReason??(remaining?'eligible_evidence_pending':state.projections.length?'qualified':'no_qualified_identity'),plannedWindows:prepared.plannedWindows,pendingWindows:remaining,rejectedChunks:prepared.rejectedChunks,completeEligibleSnapshot:complete}
47
+ }}
48
+ }
49
+ if(!isMainThread&&parentPort){
50
+ const port=parentPort;let job:string|undefined,computation:ReturnType<typeof prepareMacSpeakerLiveCompute>|undefined
51
+ port.on('message',(message:{id:number;job:string;op:string;input?:MacSpeakerLiveComputeInput;receipt?:MacWindowEmbeddingReceipt})=>{
52
+ try{
53
+ if(message.op==='prepare'){if(computation)throw Error('compute_duplicate_prepare');job=message.job;computation=prepareMacSpeakerLiveCompute(message.input!);port.postMessage({id:message.id,job,ok:true,value:computation.prepared})}
54
+ else if(message.op==='finish'){if(!computation||job!==message.job)throw Error('compute_job_binding');port.postMessage({id:message.id,job,ok:true,value:computation.finish(message.receipt)})}
55
+ else throw Error('compute_unknown_operation')
56
+ }catch(error){port.postMessage({id:message.id,job:message.job,ok:false,error:error instanceof Error?error.message:'compute_failed'})}
57
+ })
58
+ }
@@ -0,0 +1,175 @@
1
+ /** Async read-only live extraction adapter. The route owns durable publication;
2
+ * this module never writes recordings, profiles, corrections or transcripts. */
3
+ import { constants, lstatSync, type BigIntStats } from 'node:fs'
4
+ import { open } from 'node:fs/promises'
5
+ import { join } from 'node:path'
6
+ import { createHash } from 'node:crypto'
7
+ import { dataPath } from './data-dir.js'
8
+ import { type VerifiedMacRawChunk, type MacWindowEmbeddingReceipt } from './mac-speaker-windows.js'
9
+ import { extractQualifiedMacWindow } from './mac-speaker-profile-extractor.js'
10
+ import { readPcm16Mono, pcm16ToWav } from './nemotron-segments.js'
11
+ import { startMacSpeakerLiveCompute, type MacSpeakerLivePublicationCapability } from './mac-speaker-live-compute.js'
12
+ import { hashMacSpeakerLive, type MacSpeakerLiveState } from './mac-speaker-live-memory.js'
13
+ const sha=(bytes:Buffer)=>createHash('sha256').update(bytes).digest('hex')
14
+ const yieldLoop=()=>new Promise<void>(resolve=>setImmediate(resolve))
15
+ async function shaYielding(bytes:Buffer):Promise<string>{const hash=createHash('sha256');for(let at=0;at<bytes.length;at+=1024*1024){hash.update(bytes.subarray(at,at+1024*1024));await yieldLoop()}return hash.digest('hex')}
16
+ export const macSpeakerLiveEnabled=(env:NodeJS.ProcessEnv=process.env):boolean=>env.COS_MAC_SPEAKER_LIVE==='1'
17
+ export interface MacSpeakerLiveInput {
18
+ captureId:string;generation:string;audioDir:string;ledgerPath:string
19
+ chunksIndexed:readonly {i:number;c:Record<string,unknown>}[]
20
+ state?:MacSpeakerLiveState;voiceHighWater:number;revision?:number;previousDigest?:string
21
+ }
22
+ export interface MacSpeakerLiveFileBinding {path:string;exists:boolean;stamp?:string}
23
+ export const macSpeakerLiveFileStamp=(s:BigIntStats)=>[s.dev,s.ino,s.mode,s.size,s.mtimeNs,s.ctimeNs].join(':')
24
+ /** No awaits between this cheap final CAS and the caller's atomic publication. */
25
+ export function assertMacSpeakerLivePublicationBindings(bindings:readonly MacSpeakerLiveFileBinding[]):void{
26
+ if(!Array.isArray(bindings)||!bindings.length||bindings.length>8200)throw Error('live_publication_bindings_missing')
27
+ for(const b of bindings){
28
+ if(!b||typeof b.path!=='string'||typeof b.exists!=='boolean')throw Error('live_publication_binding_invalid')
29
+ let stat:BigIntStats|undefined;try{stat=lstatSync(b.path,{bigint:true})}catch(error){if((error as NodeJS.ErrnoException).code!=='ENOENT')throw error}
30
+ if(b.exists?(!stat?.isFile()||macSpeakerLiveFileStamp(stat)!==b.stamp):stat!==undefined)throw Error('live_publication_source_changed')
31
+ }
32
+ }
33
+ export interface MacSpeakerLiveOutcome {
34
+ state?:MacSpeakerLiveState;reason:string;latencyMs:number;plannedWindows:number;attemptedWindows:number;pendingWindows:number
35
+ /** Completeness of this snapshot's canonical eligible plan, never every second of speech. */
36
+ completeEligibleSnapshot:boolean;rejectedChunks:number;sourceBindingSHA256:string;fileBindings:MacSpeakerLiveFileBinding[];capability?:MacSpeakerLivePublicationCapability
37
+ }
38
+ export interface MacSpeakerLiveDeps {
39
+ runtime?:()=>Promise<{modelPath:string;profilesPath:string}|null>
40
+ extract?:(pcm:Buffer,modelPath:string,timeoutMs:number)=>Promise<readonly number[][]|null>
41
+ now?:()=>number
42
+ /** Rechecked immediately before child launch; offline tests supply explicit admission. */
43
+ admit?:(captureId:string)=>boolean
44
+ }
45
+ // Pins are an I/O optimization only: every use reopens O_NOFOLLOW and checks
46
+ // inode/ctime/mtime/size. Changed metadata always causes fresh bytes+hash. Final
47
+ // CAS checks each same pin again; no elapsed-time trust or caller-provided hash.
48
+ const pins=new Map<string,{stamp:string;hash:string;raw?:{pcmSHA256:string;sampleCount:number}}>()
49
+ const stampOf=macSpeakerLiveFileStamp
50
+ async function pinned(path:string,max:number,raw=false):Promise<{stamp:string;hash:string;raw?:{pcmSHA256:string;sampleCount:number}}>{
51
+ const f=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK)
52
+ try{const before=await f.stat({bigint:true});if(!before.isFile()||before.size>BigInt(max))throw Error('unsafe_or_oversized_source')
53
+ const stamp=stampOf(before),prior=pins.get(path)
54
+ if(prior?.stamp===stamp&&(!raw||prior.raw))return prior
55
+ const storage=Buffer.alloc(Number(before.size)+1);let offset=0
56
+ while(offset<storage.length){const{bytesRead}=await f.read(storage,offset,storage.length-offset,null);if(!bytesRead)break;offset+=bytesRead}
57
+ const bytes=storage.subarray(0,offset),after=await f.stat({bigint:true})
58
+ if(bytes.length!==Number(before.size)||stampOf(after)!==stamp)throw Error('source_changed_during_read')
59
+ const pcm=raw?pcmOf(bytes):undefined
60
+ const value={stamp,hash:await shaYielding(bytes),...(pcm?{raw:{pcmSHA256:sha(pcm),sampleCount:pcm.length/2}}:{})}
61
+ if(pins.size>=10000)pins.delete(pins.keys().next().value!);pins.set(path,value);return value
62
+ }finally{await f.close()}
63
+ }
64
+ async function readBounded(path:string,max:number):Promise<Buffer>{
65
+ const file=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK)
66
+ try{
67
+ const before=await file.stat({bigint:true});if(!before.isFile()||before.size>BigInt(max))throw Error('unsafe_or_oversized_source')
68
+ const buffer=Buffer.alloc(Number(before.size)+1);let offset=0
69
+ while(offset<buffer.length){const{bytesRead}=await file.read(buffer,offset,buffer.length-offset,null);if(!bytesRead)break;offset+=bytesRead}
70
+ const after=await file.stat({bigint:true})
71
+ if(offset!==Number(before.size)||stampOf(after)!==stampOf(before))throw Error('source_changed_during_read')
72
+ const bytes=buffer.subarray(0,offset),prior=pins.get(path)
73
+ if(pins.size>=10000)pins.delete(pins.keys().next().value!)
74
+ const hash=await shaYielding(bytes)
75
+ pins.set(path,{stamp:stampOf(before),hash,...(prior?.hash===hash&&prior.raw?{raw:prior.raw}:{})})
76
+ return bytes
77
+ }finally{await file.close()}
78
+ }
79
+ async function ledgerBytes(path:string):Promise<Buffer|null>{try{return await readBounded(path,1024*1024)}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT')return null;throw error}}
80
+ function pcmOf(wav:Buffer):Buffer{
81
+ if(wav.length<44||wav.toString('ascii',0,4)!=='RIFF'||wav.readUInt32LE(4)!==wav.length-8||wav.toString('ascii',8,12)!=='WAVE'||wav.toString('ascii',12,16)!=='fmt '||wav.readUInt32LE(16)!==16||wav.readUInt16LE(20)!==1
82
+ ||wav.readUInt16LE(22)!==1||wav.readUInt32LE(24)!==16000||wav.readUInt16LE(34)!==16||wav.toString('ascii',36,40)!=='data'||wav.readUInt32LE(40)!==wav.length-44||(wav.length-44)%2)throw Error('invalid_raw_audio')
83
+ const pcm=readPcm16Mono(wav);if(!pcm||pcm.sampleRate!==16000)throw Error('invalid_raw_audio');return pcm.pcm
84
+ }
85
+ async function defaultRuntime(){
86
+ // speakerModelState()/isEmbeddingAvailable() initialize the legacy native
87
+ // extractor synchronously. Candidate discovery here must never call either.
88
+ const{speakerModelCandidates}=await import('./speaker-embeddings.js')
89
+ for(const modelPath of speakerModelCandidates()){
90
+ let file:Awaited<ReturnType<typeof open>>|undefined
91
+ try{file=await open(modelPath,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);const info=await file.stat()
92
+ if(info.isFile()&&info.size>=1000000&&info.size<=128*1024*1024)return{modelPath,profilesPath:dataPath('voice-profiles.json')}
93
+ }catch{/* try the next configured local candidate */}finally{await file?.close()}
94
+ }
95
+ return null
96
+ }
97
+ let busy=false
98
+ /** One new disjoint window per invocation; the scheduler drains backlog without
99
+ * blocking transcription. No sampled12-window plan and no first-N-success claim. */
100
+ export async function runMacSpeakerLivePass(input:MacSpeakerLiveInput,deps:MacSpeakerLiveDeps={}):Promise<MacSpeakerLiveOutcome>{
101
+ const now=deps.now??Date.now,start=now(),sourceBindingSHA256=hashMacSpeakerLive(input.chunksIndexed)
102
+ let compute:Awaited<ReturnType<typeof startMacSpeakerLiveCompute>>|undefined,capability:MacSpeakerLivePublicationCapability|undefined,fileBindings:MacSpeakerLiveFileBinding[]=[],state=input.state,plannedWindows=0,attemptedWindows=0,pendingWindows=0,rejectedChunks=0,owns=false
103
+ const result=(reason:string,completeEligibleSnapshot=false):MacSpeakerLiveOutcome=>({state,reason,latencyMs:now()-start,plannedWindows,attemptedWindows,pendingWindows,completeEligibleSnapshot,rejectedChunks,sourceBindingSHA256,fileBindings,capability})
104
+ try{
105
+ const admitted=()=> (deps.admit??(()=>macSpeakerLiveEnabled()))(input.captureId)
106
+ if(!admitted())return result('live_admission_closed')
107
+ if(input.chunksIndexed.length>8192)throw Error('raw_source_budget_exhausted')
108
+ const ledger=await ledgerBytes(input.ledgerPath)
109
+ const runtime=await(deps.runtime??defaultRuntime)();if(!runtime)throw Error('model_unavailable')
110
+ const profileBytes=await readBounded(runtime.profilesPath,32*1024*1024),profileSHA256=await shaYielding(profileBytes)
111
+ const initialProfileStamp=pins.get(runtime.profilesPath)!.stamp
112
+ const modelPin=await pinned(runtime.modelPath,128*1024*1024),modelSHA256=modelPin.hash
113
+ const initialLedgerStamp=ledger!==null?pins.get(input.ledgerPath)!.stamp:undefined
114
+ await yieldLoop()
115
+ const raw:VerifiedMacRawChunk[]=[],bindings=new Map<number,{hash:string;stamp:string}>()
116
+ for(const entry of input.chunksIndexed){
117
+ if(!Number.isSafeInteger(entry.i)||entry.i<0||entry.i>=10000)throw Error('invalid_raw_index')
118
+ // Unhashed legacy placeholders cannot become support; planner reports them.
119
+ if(typeof entry.c.audioSha256!=='string')continue
120
+ const binding=await pinned(join(input.audioDir,`chunk_${String(entry.i).padStart(4,'0')}.wav`),960044,true),audioSHA256=binding.hash
121
+ if(audioSHA256!==entry.c.audioSha256)throw Error('raw_audio_drift')
122
+ bindings.set(entry.i,{hash:audioSHA256,stamp:binding.stamp})
123
+ raw.push({chunkIndex:entry.i,audioSHA256,pcmSHA256:binding.raw!.pcmSHA256,sampleRate:16000,channels:1,format:'pcm_s16le',sampleCount:binding.raw!.sampleCount,verified:true})
124
+ await yieldLoop();if(now()-start>15000)throw Error('live_pass_deadline')
125
+ }
126
+ fileBindings=[{path:runtime.modelPath,exists:true,stamp:modelPin.stamp},{path:runtime.profilesPath,exists:true,stamp:initialProfileStamp},
127
+ ...[...bindings].map(([index,binding])=>({path:join(input.audioDir,`chunk_${String(index).padStart(4,'0')}.wav`),exists:true,stamp:binding.stamp}))]
128
+ fileBindings.push({path:input.ledgerPath,exists:ledger!==null,...(ledger!==null?{stamp:initialLedgerStamp}:{})})
129
+ compute=await startMacSpeakerLiveCompute({captureId:input.captureId,generation:input.generation,sourceBindingSHA256,fileBindingsSHA256:hashMacSpeakerLive(fileBindings),revision:input.revision??0,previousDigest:input.previousDigest,
130
+ voiceHighWater:input.voiceHighWater,state:input.state,modelSHA256,profileSHA256,profileJSONBytes:new Uint8Array(profileBytes.buffer,profileBytes.byteOffset,profileBytes.byteLength),chunksIndexed:input.chunksIndexed,rawChunks:raw,humanHistory:!!ledger?.toString().trim()},
131
+ {timeoutMs:Math.max(1,Math.min(5000,15000-(now()-start)))})
132
+ const prepared=compute.prepared;plannedWindows=prepared.plannedWindows;pendingWindows=prepared.pendingWindows;rejectedChunks=prepared.rejectedChunks
133
+ let receipt:MacWindowEmbeddingReceipt|undefined
134
+ if(prepared.window){
135
+ if(busy)return result('native_lane_busy')
136
+ busy=true;owns=true;const window=prepared.window,buffers:Buffer[]=[]
137
+ for(const source of window.sources){const wav=await readBounded(join(input.audioDir,`chunk_${String(source.chunkIndex).padStart(4,'0')}.wav`),960044);if(sha(wav)!==source.audioSHA256)throw Error('raw_audio_drift');buffers.push(pcmOf(wav))}
138
+ const pcm=Buffer.concat(buffers);attemptedWindows++
139
+ if(!admitted())return result('live_admission_closed')
140
+ const vectors=await(deps.extract??extractQualifiedMacWindow)(pcm,runtime.modelPath,Math.max(1,Math.min(8000,15000-(now()-start))))
141
+ if(vectors)receipt={windowId:window.id,windowBindingSHA256:window.bindingSHA256,modelSHA256,audioSHA256:sha(pcm16ToWav(pcm,16000)),pcmSHA256:sha(pcm),sampleCount:pcm.length/2,embedding:vectors[0],halfEmbeddings:[vectors[1],vectors[2]]}
142
+ }
143
+ const finished=await compute.finish(receipt)
144
+ state=finished.state;capability=finished.capability;pendingWindows=finished.pendingWindows
145
+ // Final complete source/model/profile/history CAS, before route publication.
146
+ if(await shaYielding(await readBounded(runtime.profilesPath,32*1024*1024))!==profileSHA256||(await pinned(runtime.modelPath,128*1024*1024)).hash!==modelSHA256
147
+ ||hashMacSpeakerLive(await ledgerBytes(input.ledgerPath))!==hashMacSpeakerLive(ledger))throw Error('model_profile_or_history_changed')
148
+ for(const[index,expected]of bindings){if((await pinned(join(input.audioDir,`chunk_${String(index).padStart(4,'0')}.wav`),960044,true)).hash!==expected.hash)throw Error('raw_audio_drift');await yieldLoop();if(now()-start>15000)throw Error('live_pass_deadline')}
149
+ assertMacSpeakerLivePublicationBindings(fileBindings)
150
+ if(!admitted())throw Error('live_admission_closed')
151
+ if(now()-start>15000)throw Error('live_pass_deadline')
152
+ return result(prepared.window&&!receipt?'extraction_unavailable':finished.reason,finished.completeEligibleSnapshot)
153
+ }catch(error){
154
+ const reason=error instanceof Error?error.message:'live_unavailable'
155
+ state=undefined;capability=undefined
156
+ return result(reason)
157
+ }finally{await compute?.close();if(owns)busy=false}
158
+ }
159
+ /** Coalescing async runner; no spin on unavailable models or failed extraction. */
160
+ export function createMacSpeakerLiveScheduler(deps:{snapshot:(captureId:string)=>MacSpeakerLiveInput|null;commit:(captureId:string,outcome:MacSpeakerLiveOutcome)=>boolean;runtime?:MacSpeakerLiveDeps;lease?:()=>{release():void}}){
161
+ const pending=new Set<string>();let running=false
162
+ const pump=async()=>{if(running)return;running=true;try{
163
+ while(pending.size){const id=pending.values().next().value!;pending.delete(id);const input=deps.snapshot(id);if(!input)continue
164
+ let lease:{release():void}|undefined
165
+ try{
166
+ lease=deps.lease?.()
167
+ const outcome=await runMacSpeakerLivePass(input,deps.runtime)
168
+ const applied=deps.commit(id,outcome)
169
+ if(applied&&outcome.state&&outcome.reason==='eligible_evidence_pending')pending.add(id)
170
+ }catch{/* Closed maintenance admission defers work until a new capture event. */}finally{lease?.release()}
171
+ await yieldLoop()
172
+ }
173
+ }finally{running=false}}
174
+ return{schedule(captureId:string){pending.add(captureId);setImmediate(()=>{void pump().catch(()=>{})})},async drain(){await pump();while(running)await new Promise<void>(r=>setTimeout(r,5))}}
175
+ }
@@ -17,25 +17,30 @@ process.stdin.on('end',()=>{
17
17
  if(!extractor.isReady(stream))throw Error('not ready');return Array.from(extractor.compute(stream));};
18
18
  const half=Math.floor(pcm.length/4)*2; process.stdout.write(JSON.stringify([embed(pcm),embed(pcm.subarray(0,half)),embed(pcm.subarray(half))]));
19
19
  });`
20
+ let qualifiedExtractionBusy=false
20
21
  export async function extractQualifiedMacWindow(pcm: Buffer, modelPath: string, timeoutMs: number, deps: { spawn?: typeof spawn; modulePath?: string } = {}): Promise<readonly number[][] | null> {
21
- if (pcm.length < 896000 || pcm.length > 960000 || pcm.length % 2 || timeoutMs <= 0) return null
22
+ if (pcm.length < 896000 || pcm.length > 960000 || pcm.length % 2 || !Number.isFinite(timeoutMs) || timeoutMs <= 0) return null
22
23
  let modulePath: string
23
24
  try { modulePath = deps.modulePath ?? require.resolve('sherpa-onnx-node') } catch { return null }
24
- return new Promise(resolve => {
25
- const child = (deps.spawn ?? spawn)(process.execPath, ['-e', worker, modulePath, modelPath], { stdio: ['pipe', 'pipe', 'pipe'] })
25
+ if(qualifiedExtractionBusy)return null
26
+ qualifiedExtractionBusy=true
27
+ const deadline=Date.now()+timeoutMs
28
+ try{return await new Promise<readonly number[][]|null>(resolve => {
29
+ let child:ReturnType<typeof spawn>
30
+ try{child=(deps.spawn ?? spawn)(process.execPath, ['-e', worker, modulePath, modelPath], { stdio: ['pipe', 'pipe', 'pipe'] })}catch{resolve(null);return}
26
31
  const output: Buffer[] = []; let size = 0, settled = false, aborted = false
27
32
  const finish = (result: readonly number[][] | null) => { if (settled) return; settled = true; clearTimeout(timer); resolve(result) }
28
33
  const abort = () => { aborted = true; child.kill('SIGKILL') }
29
34
  // Keep ownership until close confirms native termination. Releasing on
30
35
  // kill() alone permits a second native job while the first still exits.
31
- const timer = setTimeout(abort, timeoutMs)
32
- child.stdout.on('data', b => { size += b.length; if (size > 131072) abort(); else if (!aborted) output.push(Buffer.from(b)) })
36
+ const timer = setTimeout(abort, Math.max(0,deadline-Date.now()))
37
+ child.stdout!.on('data', b => { size += b.length; if (size > 131072 || Date.now()>=deadline) abort(); else if (!aborted) output.push(Buffer.from(b)) })
33
38
  // Drain diagnostics without retaining native stderr or logging private input.
34
- child.stderr.on('data', () => {})
35
- child.on('error', () => { aborted = true; if (!child.pid) finish(null); else child.kill('SIGKILL') }); child.stdin.on('error', () => {})
39
+ child.stderr!.on('data', () => {})
40
+ child.on('error', () => { aborted = true; if (!child.pid) finish(null); else child.kill('SIGKILL') }); child.stdin!.on('error', () => {})
36
41
  child.on('close', code => {
37
- try { const rows = JSON.parse(Buffer.concat(output).toString()); finish(!aborted && code === 0 && Array.isArray(rows) && rows.length === 3 ? rows : null) } catch { finish(null) }
42
+ try { const rows = JSON.parse(Buffer.concat(output).toString()); finish(!aborted && Date.now()<deadline && code === 0 && Array.isArray(rows) && rows.length === 3 ? rows : null) } catch { finish(null) }
38
43
  })
39
- child.stdin.end(pcm)
40
- })
44
+ child.stdin!.end(pcm)
45
+ })}finally{qualifiedExtractionBusy=false}
41
46
  }