@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
@@ -454,8 +454,49 @@ export function sweepParakeetStaging(): { removed: number; restored: boolean } {
454
454
  // ── Inference ─────────────────────────────────────────────────────────────────
455
455
 
456
456
  export type ParakeetFailure = 'not_installed' | 'sandbox_unavailable' | 'busy' | 'timeout' | 'cli_failed' | 'bad_output' | 'spawn_failed' | 'aborted'
457
+ export interface ParakeetCliDiagnostics {
458
+ exitCode: number | null
459
+ signal: string | null
460
+ /** Safe categories extracted from stderr, never arbitrary native output. */
461
+ stderr: string
462
+ }
463
+
464
+ function safeCliDiagnostics(message: string, diagnostics?: ParakeetCliDiagnostics): ParakeetCliDiagnostics {
465
+ const raw = (diagnostics?.stderr ?? message).slice(-32_768)
466
+ // Native stderr can contain paths, credentials, or recognized speech. Keep
467
+ // only fixed diagnostic vocabulary; redacting a few secret patterns would
468
+ // still leave arbitrary transcript text in the durable save error/log.
469
+ const categories = [
470
+ /unknown option/i, /not supported/i, /transcription failed/i,
471
+ /invalidAudioData/i, /initializationFailed/i, /modelNotFound/i,
472
+ /modelLoadingFailed/i, /predictionFailed/i, /invalidModel/i,
473
+ /failed to load model/i, /failed to initialize/i, /CoreML/i,
474
+ /out of memory/i, /allocation failed/i, /operation not permitted/i,
475
+ /permission denied/i, /no such file or directory/i, /library not loaded/i,
476
+ /sandbox.*deny/i, /fatal error/i, /EXC_BAD_ACCESS/, /SIGABRT/, /SIGSEGV/,
477
+ /NSCocoaErrorDomain/, /MLModelErrorDomain/,
478
+ ].filter(pattern => pattern.test(raw)).map(pattern => {
479
+ // The pattern itself is trusted text; never copy the matching stderr.
480
+ return pattern.source.replace('.*', ' ').replace(/\\/g, '')
481
+ })
482
+ const legacyExit = /\bexit\s+(-?\d+|SIG[A-Z0-9]+)\b/.exec(message.slice(0, 100))?.[1]
483
+ const code = diagnostics?.exitCode ?? (legacyExit && /^-?\d+$/.test(legacyExit) ? Number(legacyExit) : null)
484
+ const signal = diagnostics?.signal ?? (legacyExit?.startsWith('SIG') ? legacyExit : null)
485
+ return {
486
+ exitCode: Number.isSafeInteger(code) ? code : null,
487
+ signal: typeof signal === 'string' && /^SIG[A-Z0-9]{1,12}$/.test(signal) ? signal : null,
488
+ stderr: categories.join(' | ').slice(0, 240) || 'unrecognized stderr omitted',
489
+ }
490
+ }
491
+
457
492
  export class ParakeetError extends Error {
458
- constructor(readonly reason: ParakeetFailure, message: string) { super(message); this.name = 'ParakeetError' }
493
+ readonly cliDiagnostics?: ParakeetCliDiagnostics
494
+ constructor(readonly reason: ParakeetFailure, message: string, diagnostics?: ParakeetCliDiagnostics) {
495
+ const safe = reason === 'cli_failed' ? safeCliDiagnostics(message, diagnostics) : undefined
496
+ super(safe ? `exit code=${safe.exitCode ?? 'unknown'} signal=${safe.signal ?? 'none'}; stderr=${safe.stderr}` : message)
497
+ this.name = 'ParakeetError'
498
+ this.cliDiagnostics = safe
499
+ }
459
500
  }
460
501
 
461
502
  export interface ParakeetResult {
@@ -721,9 +762,14 @@ function runParakeetChecked(wav: Buffer, options: ParakeetRunOptions): Promise<P
721
762
  if (child.pid && options.lane !== 'preview' && options.lane !== 'interactive') {
722
763
  try { setPriority(child.pid, 10) } catch { /* runs at normal priority */ }
723
764
  }
724
- let stderr = ''
765
+ let stderr = '', stderrTail = ''
725
766
  child.stdout?.on('data', () => { /* the JSON file is the output */ })
726
- child.stderr?.on('data', (d: Buffer) => { if (stderr.length < 32_768) stderr += d.toString() })
767
+ child.stderr?.on('data', (d: Buffer) => {
768
+ // Retain the existing leading-output option-failure check; keep a
769
+ // separate bounded tail for diagnostics from a nonzero native exit.
770
+ if (stderr.length < 32_768) stderr = (stderr + d.subarray(0, 32_768).toString()).slice(0, 32_768)
771
+ stderrTail = (stderrTail + d.subarray(-32_768).toString()).slice(-32_768)
772
+ })
727
773
  timer = setTimeout(() => {
728
774
  // SIGKILL: a CoreML compile does not stop for TERM.
729
775
  try { child.kill('SIGKILL') } catch { /* gone */ }
@@ -733,8 +779,8 @@ function runParakeetChecked(wav: Buffer, options: ParakeetRunOptions): Promise<P
733
779
  child.on('error', error => finish(new ParakeetError('spawn_failed', error.message)))
734
780
  child.on('close', (code, signal) => {
735
781
  if (settled) return
736
- if (code !== 0) { finish(new ParakeetError('cli_failed', `exit ${code ?? signal}: ${stderr.trim().split('\n').slice(-2).join(' | ').slice(0, 240)}`)); return }
737
- if (/Unknown option|not supported|transcription failed/i.test(stderr)) { finish(new ParakeetError('cli_failed', 'the CLI rejected an option')); return }
782
+ if (code !== 0) { finish(new ParakeetError('cli_failed', '', { exitCode: code, signal, stderr: stderrTail })); return }
783
+ if (/Unknown option|not supported|transcription failed/i.test(stderr)) { finish(new ParakeetError('cli_failed', '', { exitCode: code, signal, stderr })); return }
738
784
  try {
739
785
  const parsed = parseParakeetJson(readFileSync(output, 'utf8'))
740
786
  finish(null, { model: options.model, ...parsed, wallMs: Date.now() - started })
@@ -0,0 +1,58 @@
1
+ // How long a saved meeting's pending HQ audio stays in pending-batch.
2
+ //
3
+ // A directory is stale 12 hours after its _batch_pending.marker was last refreshed, and normally
4
+ // goes then. A saved meeting still waiting on HQ is the exception: deleting its audio finishes the
5
+ // job on streaming text the next time it resumes, which made "audio is preserved for retry" untrue
6
+ // (2026-10-16 and 2026-10-29). It keeps its audio until a person or a fix can finish it, and for a
7
+ // bounded time, so one job that can never finish does not hold disk forever.
8
+
9
+ import { existsSync, readdirSync, rmSync, statSync } from 'node:fs'
10
+ import { basename, resolve } from 'node:path'
11
+ import { MeetingFinalizationJobStore, type MeetingFinalizationJob } from './meeting-finalization-jobs.js'
12
+
13
+ export const PENDING_BATCH_STALE_MS = 12 * 60 * 60 * 1000
14
+ export const WAITING_JOB_AUDIO_HOLD_MS = 30 * 24 * 60 * 60 * 1000
15
+
16
+ /** Directory names of meetings still waiting on HQ, inside the hold. */
17
+ export function pendingBatchDirsHeldForJobs(jobs: readonly MeetingFinalizationJob[], now: number): Set<string> {
18
+ const held = new Set<string>()
19
+ for (const job of jobs) {
20
+ if (job.phase !== 'batch_pending') continue
21
+ // An unreadable date cannot prove the hold ended, so the audio stays.
22
+ const created = Date.parse(job.createdAt)
23
+ if (Number.isFinite(created) && now - created > WAITING_JOB_AUDIO_HOLD_MS) continue
24
+ held.add(job.sessionId)
25
+ if (job.audioDir) held.add(basename(job.audioDir))
26
+ }
27
+ return held
28
+ }
29
+
30
+ export function sweepStalePendingBatch(root: string, options: {
31
+ now: number
32
+ batchBusy: boolean
33
+ jobs?: readonly MeetingFinalizationJob[]
34
+ }): void {
35
+ const held = pendingBatchDirsHeldForJobs(options.jobs ?? new MeetingFinalizationJobStore().list(), options.now)
36
+ for (const dir of readdirSync(root)) {
37
+ const dirPath = resolve(root, dir)
38
+ try {
39
+ const files = readdirSync(dirPath)
40
+ if (files.length === 0) { rmSync(dirPath, { recursive: true, force: true }); continue }
41
+ // Prefer the marker file mtime over first chunk mtime
42
+ const markerPath = resolve(dirPath, '_batch_pending.marker')
43
+ // Fallback for pre-v5.4.3 dirs: use directory ctime (changes on rename)
44
+ const ageSource = existsSync(markerPath) ? statSync(markerPath).mtimeMs : statSync(dirPath).ctimeMs
45
+ if (options.now - ageSource <= PENDING_BATCH_STALE_MS) continue
46
+ if (held.has(dir)) {
47
+ console.warn(`[cleanup] Retaining pending-batch for a saved meeting waiting on HQ: ${dir}`)
48
+ continue
49
+ }
50
+ if (options.batchBusy) {
51
+ console.warn(`[cleanup] Retaining stale pending-batch while batch lease held: ${dir}`)
52
+ continue
53
+ }
54
+ rmSync(dirPath, { recursive: true, force: true })
55
+ console.log(`[cleanup] Purged stale pending-batch: ${dir}`)
56
+ } catch {}
57
+ }
58
+ }
@@ -0,0 +1,38 @@
1
+ // "Private" as a PRIVACY reference, not as an ordinary adjective.
2
+ //
3
+ // A blocklist of private nouns (notes, pricing, ...) lets every other private thing through ("private funding round from Blackstone",
4
+ // "my private hospital stay", "Acme's private valuation"), so this is inverted: the bare word "private" (and privately, privacy) blocks,
5
+ // EXCEPT inside a short list of public phrases: private aviation, jet(s), plane(s), pilot(s), equity, school(s), sector, label,
6
+ // market(s), company/companies, credit, and "privately held". Those are the subjects of public statistics and news.
7
+ // Even a public phrase blocks when the question also
8
+ // - has a privacy phrase anywhere: stays private, keep ... private, in private, private for now, privately told (all caught because
9
+ // they carry a bare "private"), or off the record, between us; or
10
+ // - has a possessive or a person on it: my, our, his, her, their, your, <Name>'s ("Stephen's private plane crash rate",
11
+ // "private equity ... our CEO owns").
12
+ // One helper, used by every gate that read the bare word (the Ask search gate, the public-topic and fact-check query filter, the
13
+ // public-resource request check). The live-cue deny list keeps the bare word (live-cues-general-topic.ts): its queries are chosen by a
14
+ // model, not typed by the user.
15
+ const PUBLIC_PHRASE = 'aviation|jets?|planes?|pilots?|equity|schools?|sector|label|markets?|compan(?:y|ies)|credit'
16
+ const PUBLIC_PHRASES = new RegExp(`\\bprivate\\s+(?:${PUBLIC_PHRASE})\\b|\\bprivately\\s+held\\b`, 'gi')
17
+ const MARKER = /\bprivate(?:ly)?\b|\bprivacy\b/i
18
+ // Phrases that make a question private WITHOUT the word "private" (every phrase with the word is already caught by the bare-word rule below).
19
+ const PRIVACY_PHRASES: readonly RegExp[] = [
20
+ /\boff\s+the\s+record\b/i,
21
+ /\bbetween\s+(?:us|you\s+and\s+me)\b/i,
22
+ ]
23
+ const POSSESSIVE = /\b(?:my|our|his|her|their|your|mine|ours|hers|theirs)\b/i
24
+ // <Name>'s, except the contractions that are not possessives.
25
+ const NOT_POSSESSIVE = new Set(['what', 'it', 'that', 'there', 'here', 'who', 'how', 'let', 'he', 'she', 'where', 'when', 'why', 'one', 'this'])
26
+ function hasPossessive(text: string): boolean {
27
+ if (POSSESSIVE.test(text)) return true
28
+ for (const m of text.matchAll(/\b([\p{L}][\p{L}\p{N}&.-]*)['’]s\b/gu)) if (!NOT_POSSESSIVE.has(m[1].toLowerCase())) return true
29
+ return false
30
+ }
31
+ /** True when `text` uses "private" as a privacy marker. "private aviation", "private equity", "private jet" alone are not. */
32
+ export function privateReference(text: string): boolean {
33
+ if (!MARKER.test(text)) return false
34
+ if (PRIVACY_PHRASES.some(p => p.test(text))) return true
35
+ const rest = text.replace(PUBLIC_PHRASES, ' ')
36
+ if (MARKER.test(rest)) return true // any other "private": notes, funding round, valuation, dinner, hospital ...
37
+ return hasPossessive(text) // only public phrases are left: still private if it is someone's
38
+ }