@gotcos/glasses-server 6.94.4 → 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 (109) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/README.md +52 -0
  3. package/managed-runtime-contract.json +13 -0
  4. package/package.json +3 -1
  5. package/server/index.ts +19 -1
  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/dictation-clean-models.ts +2 -0
  12. package/server/lib/dictation-clean.ts +2 -2
  13. package/server/lib/dictation-polish-pipeline.ts +22 -2
  14. package/server/lib/dictation-prewarm.ts +16 -0
  15. package/server/lib/direct-bridge.ts +14 -2
  16. package/server/lib/direct-sse.ts +6 -1
  17. package/server/lib/display-bus.ts +1 -1
  18. package/server/lib/fork-thread.ts +32 -1
  19. package/server/lib/held-naming-batches.ts +36 -0
  20. package/server/lib/job-audit-caller.ts +20 -0
  21. package/server/lib/lens-gist-engines.ts +17 -5
  22. package/server/lib/live-cos-ask.ts +29 -6
  23. package/server/lib/live-cue-verdicts.ts +43 -8
  24. package/server/lib/live-cues-engine.ts +4 -1
  25. package/server/lib/live-cues-filter.ts +50 -0
  26. package/server/lib/live-cues-general-prompts.ts +137 -0
  27. package/server/lib/live-cues-general-topic.ts +245 -0
  28. package/server/lib/live-cues-general.ts +715 -0
  29. package/server/lib/live-cues-v2-data.ts +453 -0
  30. package/server/lib/live-cues-v2-grounded.ts +262 -0
  31. package/server/lib/live-cues-v2-judge-b.ts +59 -0
  32. package/server/lib/live-cues-v2-names.ts +49 -0
  33. package/server/lib/live-cues-v2-notes.ts +8 -4
  34. package/server/lib/live-cues-v2-open-items.ts +132 -0
  35. package/server/lib/live-cues-v2-pacing.ts +45 -0
  36. package/server/lib/live-cues-v2-prompts-kinds.ts +9 -4
  37. package/server/lib/live-cues-v2-settings.ts +89 -10
  38. package/server/lib/live-cues-v2.ts +570 -47
  39. package/server/lib/mac-recording-asr.ts +6 -3
  40. package/server/lib/mac-speaker-call-human.ts +371 -0
  41. package/server/lib/mac-speaker-call-interval-human.ts +499 -0
  42. package/server/lib/mac-speaker-call-interval-projection.ts +233 -0
  43. package/server/lib/mac-speaker-call-original-view.ts +328 -0
  44. package/server/lib/mac-speaker-call-producer-worker.ts +615 -0
  45. package/server/lib/mac-speaker-call-producer.ts +539 -0
  46. package/server/lib/mac-speaker-call-store.ts +264 -0
  47. package/server/lib/mac-speaker-call-view.ts +373 -0
  48. package/server/lib/mac-speaker-live-compute.ts +72 -0
  49. package/server/lib/mac-speaker-live-memory.ts +414 -0
  50. package/server/lib/mac-speaker-live-projection.ts +126 -0
  51. package/server/lib/mac-speaker-live-save.ts +90 -0
  52. package/server/lib/mac-speaker-live-store.ts +71 -0
  53. package/server/lib/mac-speaker-live-worker.ts +58 -0
  54. package/server/lib/mac-speaker-live.ts +175 -0
  55. package/server/lib/mac-speaker-profile-extractor.ts +15 -10
  56. package/server/lib/mac-speaker-shadow-repair.ts +307 -0
  57. package/server/lib/mac-voice-assignment.ts +44 -26
  58. package/server/lib/mac-voice-display.ts +195 -0
  59. package/server/lib/mac-voice-naming-batch.ts +1067 -0
  60. package/server/lib/mac-voice-suggestions.ts +548 -0
  61. package/server/lib/maintenance-lifecycle.ts +2 -0
  62. package/server/lib/media-store.ts +2 -1
  63. package/server/lib/meeting-ask-pasted.ts +135 -0
  64. package/server/lib/meeting-ask-review.ts +8 -1
  65. package/server/lib/meeting-ask-runner.ts +4 -1
  66. package/server/lib/meeting-ask-runtime.ts +189 -17
  67. package/server/lib/meeting-ask-store.ts +2 -0
  68. package/server/lib/meeting-ask-web.ts +284 -13
  69. package/server/lib/meeting-batch-progress.ts +51 -5
  70. package/server/lib/meeting-batch-transcribe.ts +35 -2
  71. package/server/lib/meeting-fact-check.ts +3 -0
  72. package/server/lib/meeting-finalization-jobs.ts +69 -2
  73. package/server/lib/meeting-public-topic.ts +6 -1
  74. package/server/lib/meeting-speaker-review.ts +182 -17
  75. package/server/lib/meeting-store.ts +6 -1
  76. package/server/lib/parakeet.ts +51 -5
  77. package/server/lib/pending-batch-retention.ts +58 -0
  78. package/server/lib/private-reference.ts +38 -0
  79. package/server/lib/provider-usage.ts +561 -0
  80. package/server/lib/public-https-url.ts +32 -0
  81. package/server/lib/query-job-runtime.ts +3 -0
  82. package/server/lib/rich-media-safety.ts +23 -2
  83. package/server/lib/speaker-shadow-decide.ts +503 -0
  84. package/server/lib/speaker-shadow-models.ts +150 -0
  85. package/server/lib/speaker-shadow-numeric.ts +143 -0
  86. package/server/lib/speaker-shadow-pair-source.ts +91 -0
  87. package/server/lib/speaker-shadow-paired.ts +268 -0
  88. package/server/lib/speaker-shadow-report.ts +113 -0
  89. package/server/lib/speaker-shadow-timeline.ts +210 -0
  90. package/server/lib/speaker-shadow-worker.ts +312 -0
  91. package/server/lib/speaker-shadow.ts +788 -0
  92. package/server/lib/token-audit.ts +36 -6
  93. package/server/lib/transcript-cleanup.ts +3 -2
  94. package/server/lib/voice-directory.ts +8 -4
  95. package/server/lib/web-lookup-ledger.ts +151 -0
  96. package/server/lib/work-review-runtime.ts +2 -0
  97. package/server/lib/work-search.ts +3 -1
  98. package/server/lib/xlsx-text.ts +277 -0
  99. package/server/routes/health.ts +5 -0
  100. package/server/routes/live-cues.ts +36 -14
  101. package/server/routes/meeting.ts +506 -49
  102. package/server/routes/speaker-shadow.ts +79 -0
  103. package/server/routes/transcribe-stream.ts +114 -33
  104. package/server/routes/voice.ts +3 -0
  105. package/server/scripts/live-cues-v3-mutation-gate.mjs +311 -0
  106. package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +5 -5
  107. package/shared/live-cues-v2.ts +38 -5
  108. package/shared/media-attachment.ts +2 -0
  109. package/shared/meeting-ask.ts +11 -0
@@ -0,0 +1,539 @@
1
+ /** Independent, default-OFF consumer of one shared native execution. Shadow
2
+ * reports remain log-only; this queue alone may ask the reviewed store to
3
+ * publish anonymous derived views. No model download, name or enrollment API. */
4
+ import { spawn, execFile, type ChildProcess } from 'node:child_process';
5
+ import { createHash, randomUUID } from 'node:crypto';
6
+ import { existsSync, mkdirSync, readFileSync, realpathSync, lstatSync } from 'node:fs';
7
+ import { createRequire } from 'node:module';
8
+ import { dirname, join, resolve } from 'node:path';
9
+ import { fileURLToPath, pathToFileURL } from 'node:url';
10
+ import { durableAtomicWriteFileSync } from './atomic-fs.js';
11
+ import { dataPath } from './data-dir.js';
12
+ import { acquireMaintenanceWork, maintenanceAdmissionsOpen } from './maintenance-lifecycle.js';
13
+ import { onCorrectionApplied } from './meeting-corrections.js';
14
+ import type { MacSpeakerCallNativeRef, MacSpeakerCallProducerRequest } from './mac-speaker-call-producer-worker.js';
15
+ const sha = (b: string | Buffer) => createHash('sha256').update(b).digest('hex');
16
+ const GIB = 1024 ** 3;
17
+ let childOwned = false;
18
+ export const macSpeakerCallProducerEnabled = (env: NodeJS.ProcessEnv = process.env) => /^(?:1|true|yes|on)$/i.test(env.COS_MAC_SPEAKER_CALL_PRODUCER?.trim() ?? '');
19
+ export type MacSpeakerCallProducerJob = {
20
+ action: 'freeze';
21
+ request: MacSpeakerCallProducerRequest;
22
+ } | {
23
+ action: 'publish';
24
+ request: MacSpeakerCallProducerRequest;
25
+ ref: MacSpeakerCallNativeRef;
26
+ };
27
+ export function macSpeakerCallProducerRequestValid(r: MacSpeakerCallProducerRequest, root = dataPath('speaker-call')): boolean {
28
+ try {
29
+ return !!r && /^mac_[A-Za-z0-9_-]{1,100}$/.test(r.captureId) && r.root === root && realpathSync(root) === root && r.policyPath === join(root, 'admission.json') && /^[a-f0-9]{64}$/.test(r.policySHA256) && typeof r.sidecarPath === 'string' && r.sidecarPath.length <= 4096 && resolve(r.sidecarPath) === r.sidecarPath && realpathSync(dirname(r.sidecarPath)) === dirname(r.sidecarPath) && (!existsSync(r.sidecarPath) || !lstatSync(r.sidecarPath).isSymbolicLink()) && r.sidecarPath.endsWith('.g2-chunks.json') && r.ledgerPath === dataPath('meeting-corrections', `${r.captureId}.jsonl`) && r.humanJournalPath === r.sidecarPath.replace(/\.g2-chunks\.json$/, '.speaker-call-human-transaction.json');
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ export interface MacSpeakerCallResources {
36
+ availableBytes: number;
37
+ swapBytes: number;
38
+ rssBytes?: number;
39
+ }
40
+ export interface MacSpeakerCallProducerDeps {
41
+ enabled: () => boolean;
42
+ admit: () => boolean;
43
+ resources: (pid?: number) => Promise<MacSpeakerCallResources | null>;
44
+ spawn: () => ChildProcess;
45
+ now: () => number;
46
+ kill: (pid: number) => void;
47
+ parentExit: (cleanup: () => void) => () => void;
48
+ }
49
+ function command(path: string, args: string[]): Promise<string> { return new Promise((ok, bad) => execFile(path, args, { timeout: 1500, maxBuffer: 1024 * 1024 }, (e, s) => e ? bad(e) : ok(s))); }
50
+ export async function sampleMacSpeakerCallResources(pid?: number): Promise<MacSpeakerCallResources | null> {
51
+ if (process.platform !== 'darwin')
52
+ return null;
53
+ try {
54
+ const [vm, swap, rss] = await Promise.all([command('/usr/bin/vm_stat', []), command('/usr/sbin/sysctl', ['-n', 'vm.swapusage']), pid ? command('/bin/ps', ['-axo', 'pgid=,rss=']) : Promise.resolve('')]);
55
+ const page = Number(vm.match(/page size of (\d+)/)?.[1]), count = (name: string) => Number(vm.match(new RegExp(`${name}:\\s+(\\d+)`))?.[1] ?? NaN);
56
+ const used = swap.match(/used\s*=\s*([\d.]+)([MG])/), swapBytes = used ? Number(used[1]) * (used[2] === 'G' ? GIB : 1024 ** 2) : NaN;
57
+ const availableBytes = (count('Pages free') + count('Pages inactive') + count('Pages speculative')) * page;
58
+ const rssBytes = pid ? rss.split('\n').reduce((n, line) => { const [g, k] = line.trim().split(/\s+/).map(Number); return n + (g === pid && Number.isFinite(k) ? k * 1024 : 0); }, 0) : undefined;
59
+ return Number.isFinite(availableBytes) && Number.isFinite(swapBytes) ? { availableBytes, swapBytes, rssBytes } : null;
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ function spawnWorker() { const tsx = createRequire(import.meta.url).resolve('tsx/esm'); return spawn(process.execPath, ['--import', pathToFileURL(tsx).href, fileURLToPath(new URL('./mac-speaker-call-producer-worker.ts', import.meta.url))], { detached: true, stdio: ['pipe', 'pipe', 'pipe'], env: { PATH: process.env.PATH, HOME: process.env.HOME, COS_DATA_DIR: process.env.COS_DATA_DIR, TMPDIR: process.env.TMPDIR, OMP_NUM_THREADS: '1', VECLIB_MAXIMUM_THREADS: '1', OPENBLAS_NUM_THREADS: '1', COS_MAC_SPEAKER_CALL_PRODUCER: '0', COS_MAC_SPEAKER_LIVE: '0', COS_TRANSCRIPT_CLEANUP: '0', COS_OPENAI_WHISPER_FALLBACK: '0', COS_LIVE_CUES: '0', COS_LIVE_CUES_V2: '0' } }); }
66
+ /** Ownership is held until actual child close, including timeout/error paths. */
67
+ export async function runMacSpeakerCallProducerChild(job: MacSpeakerCallProducerJob, overrides: Partial<MacSpeakerCallProducerDeps> = {}): Promise<{
68
+ status: 'applied' | 'frozen' | 'deferred' | 'withheld';
69
+ reason?: string;
70
+ result?: unknown;
71
+ }> {
72
+ const deps: MacSpeakerCallProducerDeps = { enabled: macSpeakerCallProducerEnabled, admit: maintenanceAdmissionsOpen, resources: sampleMacSpeakerCallResources, spawn: spawnWorker, now: Date.now, parentExit: cleanup => { process.once('exit', cleanup); return () => { process.off('exit', cleanup); }; }, kill: pid => {
73
+ try {
74
+ process.kill(-pid, 'SIGKILL');
75
+ }
76
+ catch { }
77
+ }, ...overrides };
78
+ let input: string;
79
+ let unlisten = () => { };
80
+ try {
81
+ input = JSON.stringify(job);
82
+ if (Buffer.byteLength(input) > 65536)
83
+ throw Error();
84
+ }
85
+ catch {
86
+ return { status: 'withheld', reason: 'worker_input_bound' };
87
+ }
88
+ if (childOwned)
89
+ return { status: 'deferred', reason: 'producer_child_busy' };
90
+ if (!deps.enabled() || !deps.admit())
91
+ return { status: 'deferred', reason: 'admission_closed' };
92
+ const initial = await deps.resources().catch(() => null);
93
+ if (!initial || !Number.isFinite(initial.availableBytes) || !Number.isFinite(initial.swapBytes) || initial.availableBytes < 16 * GIB)
94
+ return { status: 'deferred', reason: 'resource_admission' };
95
+ if (!deps.enabled() || !deps.admit())
96
+ return { status: 'deferred', reason: 'admission_changed' };
97
+ if (childOwned)
98
+ return { status: 'deferred', reason: 'producer_child_busy' };
99
+ const lease = acquireMaintenanceWork('mac_speaker_call'), deadline = deps.now() + (job.action === 'freeze' ? 10000 : 75000);
100
+ childOwned = true;
101
+ try {
102
+ return await new Promise(resolve => {
103
+ let child: ChildProcess;
104
+ try {
105
+ child = deps.spawn();
106
+ }
107
+ catch {
108
+ return resolve({ status: 'withheld', reason: 'worker_spawn_failed' });
109
+ }
110
+ let output = '', verdict: string | null = null, exited = false, sampling = false;
111
+ const stop = (reason: string) => {
112
+ verdict ??= reason;
113
+ if (child.pid)
114
+ deps.kill(child.pid);
115
+ };
116
+ unlisten = deps.parentExit(() => stop('parent_exit'));
117
+ const tick = setInterval(() => {
118
+ if (deps.now() > deadline) {
119
+ stop('worker_deadline');
120
+ return;
121
+ }
122
+ if (!deps.enabled() || !deps.admit()) {
123
+ stop('admission_changed');
124
+ return;
125
+ }
126
+ if (sampling || !child.pid)
127
+ return;
128
+ sampling = true;
129
+ void deps.resources(child.pid).catch(() => null).then(r => {
130
+ sampling = false;
131
+ if (exited)
132
+ return;
133
+ if (!r || !Number.isFinite(r.availableBytes) || !Number.isFinite(r.swapBytes) || !Number.isFinite(r.rssBytes) || r.availableBytes < 8 * GIB || r.swapBytes - initial.swapBytes > 256 * 1024 ** 2 || (r.rssBytes ?? Infinity) > 3 * GIB)
134
+ stop('resource_reserve');
135
+ });
136
+ }, 250);
137
+ tick.unref();
138
+ child.stdout?.on('data', (b: Buffer) => {
139
+ if (verdict)
140
+ return;
141
+ output += b.toString();
142
+ if (Buffer.byteLength(output) > 1024 * 1024) {
143
+ output = output.slice(0, 1024 * 1024);
144
+ stop('worker_output_bound');
145
+ }
146
+ });
147
+ child.stderr?.on('data', () => { });
148
+ child.on('error', () => {
149
+ stop('worker_error');
150
+ if (!child.pid) {
151
+ exited = true;
152
+ clearInterval(tick);
153
+ resolve({ status: 'withheld', reason: verdict! });
154
+ }
155
+ });
156
+ child.on('close', code => {
157
+ if (exited)
158
+ return;
159
+ exited = true;
160
+ clearInterval(tick);
161
+ if (verdict || deps.now() > deadline)
162
+ return resolve({ status: 'deferred', reason: verdict ?? 'worker_deadline' });
163
+ try {
164
+ const value = JSON.parse(output.trim());
165
+ if (code !== 0 || !value || !['applied', 'frozen', 'withheld'].includes(value.status))
166
+ throw Error();
167
+ resolve(value);
168
+ }
169
+ catch {
170
+ resolve({ status: 'withheld', reason: 'worker_result_invalid' });
171
+ }
172
+ });
173
+ child.stdin?.on('error', () => stop('worker_input_error'));
174
+ try {
175
+ child.stdin?.end(input);
176
+ }
177
+ catch {
178
+ stop('worker_input_error');
179
+ }
180
+ });
181
+ }
182
+ finally {
183
+ unlisten();
184
+ childOwned = false;
185
+ lease.release();
186
+ }
187
+ }
188
+ type Queued = ({
189
+ action: 'freeze';
190
+ request: MacSpeakerCallProducerRequest;
191
+ freezeOnly: boolean;
192
+ attempts: number;
193
+ enqueuedAt: number;
194
+ } | {
195
+ action: 'publish';
196
+ request: MacSpeakerCallProducerRequest;
197
+ ref: MacSpeakerCallNativeRef;
198
+ enqueuedAt: number;
199
+ }) & {
200
+ jobId: string;
201
+ };
202
+ export class MacSpeakerCallProducer {
203
+ private queue: Queued[] = [];
204
+ private running = false;
205
+ private blockedQueue = false;
206
+ private queueSHA256: string | null = null;
207
+ private timer: ReturnType<typeof setTimeout> | null = null;
208
+ constructor(private options: {
209
+ root: string;
210
+ enabled: () => boolean;
211
+ admit: () => boolean;
212
+ requestNative: (r: MacSpeakerCallProducerRequest) => void | Promise<void>;
213
+ run?: (job: MacSpeakerCallProducerJob) => ReturnType<typeof runMacSpeakerCallProducerChild>;
214
+ }) { }
215
+ private queuePath() { return join(this.options.root, 'producer-queue.json'); }
216
+ private write() {
217
+ try {
218
+ if (this.blockedQueue)
219
+ throw Error('producer_queue_quarantined');
220
+ const path = this.queuePath();
221
+ if (existsSync(path) && (lstatSync(path).isSymbolicLink() || lstatSync(path).size > 4 * 1024 * 1024))
222
+ throw Error('producer_queue_path');
223
+ const actual = existsSync(path) ? sha(readFileSync(path)) : null;
224
+ if (actual !== this.queueSHA256) {
225
+ this.blockedQueue = true;
226
+ throw Error('producer_queue_cas');
227
+ }
228
+ const bytes = JSON.stringify({ version: 1, queue: this.queue }) + '\n', expected = sha(bytes);
229
+ durableAtomicWriteFileSync(path, bytes);
230
+ if (sha(readFileSync(path)) !== expected) {
231
+ this.blockedQueue = true;
232
+ throw Error('producer_queue_readback_drift');
233
+ }
234
+ this.queueSHA256 = expected;
235
+ }
236
+ catch (error) {
237
+ this.blockedQueue = true;
238
+ throw error;
239
+ }
240
+ }
241
+ private remove(jobId: string): boolean {
242
+ if (!this.queue.some(q => q.jobId === jobId))
243
+ return false;
244
+ this.queue = this.queue.filter(q => q.jobId !== jobId);
245
+ this.write();
246
+ return true;
247
+ }
248
+ private async finishFreeze(q: Extract<Queued, {
249
+ action: 'freeze';
250
+ }>, result: {
251
+ status: string;
252
+ reason?: string;
253
+ }) {
254
+ if (!this.queue.some(current => current.jobId === q.jobId))
255
+ return;
256
+ if (result.status === 'deferred') {
257
+ this.schedule();
258
+ return;
259
+ }
260
+ if (result.status !== 'frozen') {
261
+ this.remove(q.jobId);
262
+ this.status(q.request.captureId, result.reason ?? 'seed_withheld');
263
+ return;
264
+ }
265
+ try {
266
+ // Keep the durable token until target registration AND the shared
267
+ // native notification have completed. Crash/restart repeats safely.
268
+ await this.seedReady(q.request, q.freezeOnly);
269
+ this.remove(q.jobId);
270
+ }
271
+ catch(error) {
272
+ const i = this.queue.findIndex(current => current.jobId === q.jobId);
273
+ if (i < 0)
274
+ return; // A newer refresh superseded this exact job.
275
+ if((error as NodeJS.ErrnoException)?.code==='MAC_CALL_NATIVE_PENDING'){
276
+ this.status(q.request.captureId,'native_active_without_prenative_provenance');this.schedule();return;
277
+ }
278
+ if((error as NodeJS.ErrnoException)?.code==='MAC_CALL_NATIVE_UNAVAILABLE'){
279
+ this.remove(q.jobId);this.status(q.request.captureId,error instanceof Error?error.message:'native_provenance_unavailable');return;
280
+ }
281
+ if (q.attempts < 2) {
282
+ this.queue[i] = { ...q, jobId: randomUUID(), attempts: q.attempts + 1 };
283
+ this.write();
284
+ this.schedule();
285
+ }
286
+ else {
287
+ this.remove(q.jobId);
288
+ this.status(q.request.captureId, 'native_request_failed');
289
+ }
290
+ }
291
+ }
292
+ private path(path: unknown) { return typeof path === 'string' && path.length <= 4096 && resolve(path) === path && realpathSync(dirname(path)) === dirname(path) && (!existsSync(path) || !lstatSync(path).isSymbolicLink()); }
293
+ private validRequest(r: MacSpeakerCallProducerRequest) { return macSpeakerCallProducerRequestValid(r, this.options.root); }
294
+ private validRef(ref: MacSpeakerCallNativeRef) { return ref && /^mac_[A-Za-z0-9_-]{1,100}$/.test(ref.captureId) && /^[a-f0-9]{64}$/.test(ref.sha256) && typeof ref.path === 'string' && ref.path.startsWith(join(this.options.root, ref.captureId, 'native-')) && /^native-[a-f0-9-]{36}$/.test(dirname(ref.path).split('/').at(-1) ?? '') && ref.path.endsWith('/receipt.json') && this.path(ref.path); }
295
+ private targets(captureId: string): MacSpeakerCallProducerRequest[] {
296
+ const path = join(this.options.root, captureId, 'targets.json');
297
+ if (!existsSync(path))
298
+ return [];
299
+ if (lstatSync(path).size > 65536)
300
+ throw Error('producer_targets_bound');
301
+ const v = JSON.parse(readFileSync(path, 'utf8'));
302
+ if (!Array.isArray(v) || v.length > 8 || new Set(v.map(r => r.sidecarPath)).size !== v.length || v.some(r => !this.validRequest(r) || r.captureId !== captureId))
303
+ throw Error('producer_targets_invalid');
304
+ return v;
305
+ }
306
+ private status(id: string, reason: string) {
307
+ try {
308
+ if (!/^mac_[A-Za-z0-9_-]{1,100}$/.test(id))
309
+ return;
310
+ const dir = join(this.options.root, id);
311
+ if (existsSync(dir))
312
+ durableAtomicWriteFileSync(join(dir, 'producer-status.json'), JSON.stringify({ status: 'withheld', reason }) + '\n');
313
+ }
314
+ catch { /* A diagnostic failure cannot fail a saved meeting or notification. */ }
315
+ }
316
+ private retry(request: MacSpeakerCallProducerRequest, freezeOnly: boolean): Extract<Queued, {
317
+ action: 'freeze';
318
+ }> {
319
+ const job: Extract<Queued, {
320
+ action: 'freeze';
321
+ }> = { jobId: randomUUID(), action: 'freeze', request, freezeOnly, attempts: 0, enqueuedAt: Date.now() };
322
+ this.queue = this.queue.filter(q => q.action !== 'freeze' || q.request.sidecarPath !== request.sidecarPath);
323
+ this.queue.push(job);
324
+ if (this.queue.length > 200)
325
+ throw Error('producer_queue_bound');
326
+ this.write();
327
+ this.schedule();
328
+ return job;
329
+ }
330
+ busy() { return this.running || childOwned; }
331
+ async refreshAvailability(captureId: string, sidecarPath: string): Promise<{
332
+ available: boolean;
333
+ reason: string;
334
+ }> {
335
+ try {
336
+ if (!this.options.enabled())
337
+ return { available: false, reason: 'producer_disabled' };
338
+ if (this.blockedQueue)
339
+ return { available: false, reason: 'producer_queue_quarantined' };
340
+ if (!/^mac_[A-Za-z0-9_-]{1,100}$/.test(captureId))
341
+ return { available: false, reason: 'capture_invalid' };
342
+ const request = this.targets(captureId).find(r => r.sidecarPath === sidecarPath);
343
+ if (!request)
344
+ return { available: false, reason: 'copy_unregistered' };
345
+ const path = join(this.options.root, captureId, 'native-latest.json');
346
+ if (!existsSync(path))
347
+ return { available: false, reason: 'cached_native_missing' };
348
+ const worker = await import('./mac-speaker-call-producer-worker.js'), ref = JSON.parse(worker.readMacSpeakerCallProducerFile(path, 4096)!.toString());
349
+ if (!this.options.enabled() || !this.validRef(ref) || ref.captureId !== captureId)
350
+ return { available: false, reason: 'cached_native_invalid' };
351
+ worker.checkMacSpeakerCallCachedNative(request, ref);
352
+ return this.options.enabled() ? { available: true, reason: 'cached_refresh_available' } : { available: false, reason: 'producer_disabled' };
353
+ }
354
+ catch {
355
+ return { available: false, reason: 'cached_refresh_unavailable' };
356
+ }
357
+ }
358
+ resume() {
359
+ if (!this.options.enabled() || !existsSync(this.queuePath()))
360
+ return;
361
+ try {
362
+ if (lstatSync(this.queuePath()).isSymbolicLink() || lstatSync(this.queuePath()).size > 4 * 1024 * 1024)
363
+ throw Error();
364
+ const bytes = readFileSync(this.queuePath());
365
+ const p = JSON.parse(bytes.toString());
366
+ if (p.version !== 1 || !Array.isArray(p.queue) || p.queue.length > 200 || new Set(p.queue.map((q: Queued) => q.jobId)).size !== p.queue.length || p.queue.some((q: Queued) => !/^[a-f0-9-]{36}$/.test(q.jobId) || !this.validRequest(q.request) || !Number.isSafeInteger(q.enqueuedAt) || q.action === 'freeze' && (!Number.isInteger(q.attempts) || q.attempts < 0 || q.attempts > 3 || typeof q.freezeOnly !== 'boolean') || q.action === 'publish' && (!this.validRef(q.ref) || q.ref.captureId !== q.request.captureId) || !['freeze', 'publish'].includes(q.action)))
367
+ throw Error();
368
+ this.queue = p.queue;
369
+ this.queueSHA256 = sha(bytes);
370
+ void this.pump();
371
+ }
372
+ catch {
373
+ this.blockedQueue = true; /* Preserve corrupt bytes; never treat them as paths or overwrite them. */
374
+ }
375
+ }
376
+ async finalized(input: {
377
+ sessionId: string;
378
+ sidecarPath: string;
379
+ freezeOnly?: boolean;
380
+ }) {
381
+ try {
382
+ if (!this.options.enabled() || this.blockedQueue || !/^mac_[A-Za-z0-9_-]{1,100}$/.test(input.sessionId))
383
+ return;
384
+ const policyPath = join(this.options.root, 'admission.json');
385
+ if (!existsSync(policyPath))
386
+ return;
387
+ const policy = readFileSync(policyPath);
388
+ if (policy.length > 256 * 1024)
389
+ return;
390
+ const request: MacSpeakerCallProducerRequest = { captureId: input.sessionId, sidecarPath: input.sidecarPath, root: this.options.root, ledgerPath: dataPath('meeting-corrections', `${input.sessionId}.jsonl`), policyPath, policySHA256: sha(policy), humanJournalPath: input.sidecarPath.replace(/\.g2-chunks\.json$/, '.speaker-call-human-transaction.json') };
391
+ if (!this.validRequest(request))
392
+ return;
393
+ // Persist FIRST, before a child can freeze the seed or this
394
+ // notification can be lost in a server crash.
395
+ const q = this.retry(request, !!input.freezeOnly);
396
+ if (this.running)
397
+ return;
398
+ this.running = true;
399
+ try {
400
+ const result = await (this.options.run ?? runMacSpeakerCallProducerChild)({ action: 'freeze', request });
401
+ await this.finishFreeze(q, result);
402
+ }
403
+ finally {
404
+ this.running = false;
405
+ this.schedule();
406
+ }
407
+ }
408
+ catch {
409
+ this.status(input.sessionId, 'finalization_notification_withheld');
410
+ }
411
+ }
412
+ private async seedReady(request: MacSpeakerCallProducerRequest, freezeOnly: boolean) {
413
+ const input = { sessionId: request.captureId };
414
+ const dir = join(this.options.root, input.sessionId), path = join(dir, 'targets.json');
415
+ let targets: MacSpeakerCallProducerRequest[] = [];
416
+ targets = this.targets(input.sessionId);
417
+ targets = targets.filter(t => t.sidecarPath !== request.sidecarPath);
418
+ targets.push(request);
419
+ if (targets.length > 8)
420
+ return;
421
+ durableAtomicWriteFileSync(path, JSON.stringify(targets) + '\n');
422
+ if (freezeOnly)
423
+ return;
424
+ if (existsSync(join(dir, 'native-latest.json'))) {
425
+ if (!this.nativeExport(JSON.parse(readFileSync(join(dir, 'native-latest.json'), 'utf8'))))
426
+ throw Error('cached_native_invalid');
427
+ }
428
+ else
429
+ await this.options.requestNative(request);
430
+ }
431
+ nativeExport(ref: MacSpeakerCallNativeRef): boolean {
432
+ try {
433
+ if (!this.options.enabled() || this.blockedQueue || !this.validRef(ref))
434
+ return false;
435
+ const dir = join(this.options.root, ref.captureId), targets = this.targets(ref.captureId);
436
+ if (!targets.length) {
437
+ this.status(ref.captureId, 'native_targets_missing');
438
+ return false;
439
+ }
440
+ durableAtomicWriteFileSync(join(dir, 'native-latest.json'), JSON.stringify(ref) + '\n');
441
+ for (const request of targets) {
442
+ this.queue = this.queue.filter(q => q.request.sidecarPath !== request.sidecarPath);
443
+ this.queue.push({ jobId: randomUUID(), action: 'publish', request, ref, enqueuedAt: Date.now() });
444
+ }
445
+ if (this.queue.length > 200)
446
+ throw Error('producer_queue_bound');
447
+ this.write();
448
+ void this.pump();
449
+ return true;
450
+ }
451
+ catch {
452
+ this.status(ref?.captureId, 'native_targets_invalid');
453
+ return false;
454
+ }
455
+ }
456
+ ledgerChanged(captureId: string) {
457
+ if (!this.options.enabled())
458
+ return;
459
+ try {
460
+ this.nativeExport(JSON.parse(readFileSync(join(this.options.root, captureId, 'native-latest.json'), 'utf8')));
461
+ }
462
+ catch { /* Never rerun native merely to name/correct. */ }
463
+ }
464
+ async pump() {
465
+ if (this.running || this.blockedQueue || !this.options.enabled() || !this.queue.length)
466
+ return;
467
+ if (!this.options.admit()) {
468
+ this.schedule();
469
+ return;
470
+ }
471
+ this.running = true;
472
+ try {
473
+ while (this.options.enabled() && this.options.admit() && this.queue.length) {
474
+ const q = this.queue[0];
475
+ if (Date.now() - q.enqueuedAt > 24 * 60 * 60 * 1000) {
476
+ this.status(q.request.captureId, 'queue_expired');
477
+ this.remove(q.jobId);
478
+ continue;
479
+ }
480
+ const result = await (this.options.run ?? (j => runMacSpeakerCallProducerChild(j, { enabled: this.options.enabled, admit: this.options.admit })))(q.action === 'freeze' ? { action: 'freeze', request: q.request } : { action: 'publish', request: q.request, ref: q.ref });
481
+ if (result.status === 'deferred') {
482
+ this.schedule();
483
+ return;
484
+ }
485
+ if (q.action === 'freeze'){
486
+ await this.finishFreeze(q, result);
487
+ if(this.queue.some(current=>current.jobId===q.jobId))return;
488
+ }
489
+ else {
490
+ // A notification may replace the head while run() awaits.
491
+ // Never dequeue a newer token or another representation.
492
+ this.remove(q.jobId);
493
+ if (result.status === 'withheld')
494
+ this.status(q.request.captureId, result.reason ?? 'worker_withheld');
495
+ }
496
+ // Fail closed without an unbounded retry loop. A new finalization,
497
+ // filing, correction or explicit resume may enqueue fresh valid work.
498
+ }
499
+ }
500
+ catch {
501
+ this.blockedQueue = true;
502
+ }
503
+ finally {
504
+ this.running = false;
505
+ }
506
+ }
507
+ private schedule() {
508
+ if (this.timer)
509
+ return;
510
+ this.timer = setTimeout(() => { this.timer = null; void this.pump(); }, 10000);
511
+ this.timer.unref();
512
+ }
513
+ }
514
+ let instance: MacSpeakerCallProducer | null = null;
515
+ let correctionListener = false;
516
+ export function configureMacSpeakerCallProducer(input: {
517
+ captureActive: () => boolean;
518
+ admissionsOpen: () => boolean;
519
+ requestNative: (r: MacSpeakerCallProducerRequest) => void;
520
+ }) {
521
+ instance ??= new MacSpeakerCallProducer({ root: dataPath('speaker-call'), enabled: macSpeakerCallProducerEnabled, admit: () => input.admissionsOpen() && !input.captureActive(), requestNative: input.requestNative });
522
+ if (!correctionListener) {
523
+ onCorrectionApplied(sessionId => instance?.ledgerChanged(sessionId));
524
+ correctionListener = true;
525
+ }
526
+ instance.resume();
527
+ }
528
+ export const notifyMacSpeakerCallFinalized = (input: {
529
+ sessionId: string;
530
+ sidecarPath: string;
531
+ freezeOnly?: boolean;
532
+ }): Promise<void> => instance?.finalized(input) ?? Promise.resolve();
533
+ export const notifyMacSpeakerCallNativeExport = (ref: MacSpeakerCallNativeRef) => instance?.nativeExport(ref);
534
+ export const notifyMacSpeakerCallLedgerChanged = (id: string) => instance?.ledgerChanged(id);
535
+ export const macSpeakerCallCachedRefreshAvailability = (captureId: string, sidecarPath: string): Promise<{
536
+ available: boolean;
537
+ reason: string;
538
+ }> => instance?.refreshAvailability(captureId, sidecarPath) ?? Promise.resolve({ available: false, reason: 'producer_not_configured' });
539
+ export const macSpeakerCallProducerBusy = () => childOwned || (instance?.busy() ?? false);