@gotcos/glasses-server 6.21.31 → 6.21.33

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.
@@ -4,17 +4,17 @@
4
4
  * When configured, G2 "Review Meetings" reads markdown from a COS-style tree:
5
5
  * {operationsDir}/{domain}/meetings/YYYY-MM/*.md
6
6
  *
7
- * Resolution order:
8
- * 1. COS_OPERATIONS_DIR — explicit operations/ root (preferred)
9
- * 2. COS_MEETINGS_ROOT alias for the same path
10
- * 3. COS_SCRIPTS_DIR/.. — classic Starter Kit layout (operations/scripts → operations)
7
+ * Read resolution accepts COS_MEETINGS_ROOT as a direct YYYY-MM library.
8
+ * Write/enrichment resolution remains COS_OPERATIONS_DIR, a legacy
9
+ * multi-domain COS_MEETINGS_ROOT, then COS_SCRIPTS_DIR/...
11
10
  *
12
11
  * Standalone installs leave all of these unset and keep using MeetingStore
13
12
  * (~/.cos-glasses/data/recordings).
14
13
  */
15
14
 
16
- import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync } from 'node:fs'
17
- import { basename, join, resolve } from 'node:path'
15
+ import { closeSync, existsSync, lstatSync, openSync, readdirSync, readFileSync, readSync, realpathSync, statSync } from 'node:fs'
16
+ import { createHash } from 'node:crypto'
17
+ import { basename, dirname, join, resolve } from 'node:path'
18
18
  import { discoveredDomains, domainAbbreviation as deriveAbbr, isSafeDomainName as safeName } from './domains.js'
19
19
  import type { MeetingDetail, MeetingMeta } from './meeting-store.js'
20
20
  import { MEETING_SOURCE_MAX_BYTES } from './meeting-store.js'
@@ -41,6 +41,103 @@ const DETAIL_CHUNK_ESTIMATE_CHARS = 1700
41
41
 
42
42
  export type CosOperationsMeetingMeta = MeetingMeta & { time?: string }
43
43
 
44
+ export type MeetingLibraryLayout = 'direct' | 'multi_domain' | 'standalone' | 'invalid_explicit_root'
45
+
46
+ export interface MeetingLibraryInspection {
47
+ layout: MeetingLibraryLayout
48
+ root: string | null
49
+ rootFingerprint: string | null
50
+ meetingCount: number
51
+ warnings: string[]
52
+ domains: string[]
53
+ }
54
+
55
+ const MONTH_PATTERN = /^\d{4}-(0[1-9]|1[0-2])$/
56
+ const MAX_LIST_CANDIDATES = 2_000
57
+ const MAX_LIST_FILE_BYTES = 100_000
58
+ const MAX_DETAIL_FILE_BYTES = 10 * 1024 * 1024
59
+
60
+ function rootFingerprint(root: string): string {
61
+ return createHash('sha256').update(root).digest('hex').slice(0, 16)
62
+ }
63
+
64
+ function safeRoot(path: string): string | null {
65
+ try {
66
+ const stat = lstatSync(path)
67
+ if (stat.isSymbolicLink() || !stat.isDirectory()) return null
68
+ return realpathSync(path)
69
+ } catch { return null }
70
+ }
71
+
72
+ function safeChildDirectory(parentReal: string, path: string): string | null {
73
+ try {
74
+ const stat = lstatSync(path)
75
+ if (stat.isSymbolicLink() || !stat.isDirectory()) return null
76
+ const real = realpathSync(path)
77
+ return dirname(real) === parentReal ? real : null
78
+ } catch { return null }
79
+ }
80
+
81
+ function safeRegularFile(parentReal: string, path: string, maxBytes = MAX_LIST_FILE_BYTES): string | null {
82
+ try {
83
+ const stat = lstatSync(path)
84
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.size > maxBytes) return null
85
+ const real = realpathSync(path)
86
+ if (dirname(real) !== parentReal) return null
87
+ return readFileSync(real, 'utf8')
88
+ } catch { return null }
89
+ }
90
+
91
+ function safeBoundedRegularFile(parentReal: string, path: string, maxReadBytes: number): { content: string; truncated: boolean } | null {
92
+ let fd: number | null = null
93
+ try {
94
+ const stat = lstatSync(path)
95
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_DETAIL_FILE_BYTES) return null
96
+ const real = realpathSync(path)
97
+ if (dirname(real) !== parentReal) return null
98
+ fd = openSync(real, 'r')
99
+ const buffer = Buffer.alloc(Math.min(maxReadBytes, stat.size))
100
+ const read = readSync(fd, buffer, 0, buffer.length, 0)
101
+ return { content: buffer.subarray(0, read).toString('utf8'), truncated: stat.size > read }
102
+ } catch { return null }
103
+ finally {
104
+ if (fd !== null) { try { closeSync(fd) } catch { /* already closed */ } }
105
+ }
106
+ }
107
+
108
+ export function inspectMeetingLibraryPath(path: string): MeetingLibraryInspection {
109
+ const requested = resolve(path)
110
+ const root = safeRoot(requested)
111
+ if (!root) {
112
+ return { layout: 'invalid_explicit_root', root: null, rootFingerprint: null, meetingCount: 0, warnings: ['folder is missing, unreadable, or unsafe'], domains: [] }
113
+ }
114
+ const entries = (() => { try { return readdirSync(root) } catch { return [] } })()
115
+ const directMonths = entries.filter(name => MONTH_PATTERN.test(name) && safeChildDirectory(root, join(root, name))).sort()
116
+ const domains = entries.filter(safeName).filter(name => {
117
+ const domainReal = safeChildDirectory(root, join(root, name))
118
+ if (!domainReal) return false
119
+ return safeChildDirectory(domainReal, join(domainReal, 'meetings')) != null
120
+ }).sort()
121
+ const warnings: string[] = []
122
+ if (directMonths.length > 0 && domains.length > 0) {
123
+ return { layout: 'invalid_explicit_root', root, rootFingerprint: rootFingerprint(root), meetingCount: 0, warnings: ['folder mixes direct months and domain/meetings trees'], domains }
124
+ }
125
+ const layout: MeetingLibraryLayout = directMonths.length > 0 ? 'direct' : domains.length > 0 ? 'multi_domain' : 'invalid_explicit_root'
126
+ if (layout === 'invalid_explicit_root') {
127
+ const nearMisses = entries.filter(safeName).filter(name => {
128
+ const child = safeChildDirectory(root, join(root, name))
129
+ if (!child) return false
130
+ try {
131
+ return readdirSync(child).some(entry => MONTH_PATTERN.test(entry) && safeChildDirectory(child, join(child, entry)))
132
+ } catch { return false }
133
+ })
134
+ warnings.push(nearMisses.length > 0
135
+ ? `missing meetings/ inside: ${nearMisses.slice(0, 6).join(', ')}`
136
+ : 'expected YYYY-MM folders or <domain>/meetings/YYYY-MM')
137
+ }
138
+ return { layout, root, rootFingerprint: rootFingerprint(root), meetingCount: 0, warnings, domains }
139
+ }
140
+
44
141
  /** Enough to clear the sidecar's leading metadata keys whatever their order. */
45
142
  const SIDECAR_HEAD_BYTES = 4096
46
143
 
@@ -60,8 +157,11 @@ function sidecarSessionId(monthDir: string, meetingFilename: string): string | u
60
157
  const path = join(monthDir, sidecarName)
61
158
  let fd: number | null = null
62
159
  try {
63
- const stat = statSync(path)
64
- if (!stat.isFile() || stat.size === 0) return undefined
160
+ const linkStat = lstatSync(path)
161
+ if (linkStat.isSymbolicLink() || !linkStat.isFile() || linkStat.size === 0) return undefined
162
+ const real = realpathSync(path)
163
+ if (dirname(real) !== realpathSync(monthDir)) return undefined
164
+ const stat = statSync(real)
65
165
  fd = openSync(path, 'r')
66
166
  const buffer = Buffer.alloc(Math.min(SIDECAR_HEAD_BYTES, stat.size))
67
167
  const read = readSync(fd, buffer, 0, buffer.length, 0)
@@ -84,12 +184,17 @@ function envPath(name: string): string | null {
84
184
  /** Resolve the COS operations directory, or null in standalone mode.
85
185
  * Reads process.env on each call so tests and Control env updates stay live. */
86
186
  export function resolveCosOperationsDir(): string | null {
87
- const explicit = envPath('COS_OPERATIONS_DIR') || envPath('COS_MEETINGS_ROOT')
88
- if (explicit && existsSync(explicit)) return explicit
187
+ const operations = envPath('COS_OPERATIONS_DIR')
188
+ if (operations && inspectMeetingLibraryPath(operations).layout === 'multi_domain') return safeRoot(operations)
189
+ // Backward compatibility: before 6.21.33 COS_MEETINGS_ROOT was documented as
190
+ // an alias for COS_OPERATIONS_DIR. Preserve that meaning only for a real
191
+ // multi-domain tree; a direct library is never a write destination.
192
+ const legacy = envPath('COS_MEETINGS_ROOT')
193
+ if (legacy && inspectMeetingLibraryPath(legacy).layout === 'multi_domain') return safeRoot(legacy)
89
194
  const scriptsDir = envPath('COS_SCRIPTS_DIR')
90
195
  if (scriptsDir) {
91
196
  const inferred = resolve(scriptsDir, '..')
92
- if (existsSync(inferred)) return inferred
197
+ if (inspectMeetingLibraryPath(inferred).layout === 'multi_domain') return safeRoot(inferred)
93
198
  }
94
199
  return null
95
200
  }
@@ -98,6 +203,19 @@ export function cosOperationsMeetingsConfigured(): boolean {
98
203
  return resolveCosOperationsDir() != null
99
204
  }
100
205
 
206
+ export function resolveMeetingLibrary(): MeetingLibraryInspection {
207
+ const explicit = envPath('COS_MEETINGS_ROOT')
208
+ if (explicit) return inspectMeetingLibraryPath(explicit)
209
+ const operations = resolveCosOperationsDir()
210
+ if (operations) return inspectMeetingLibraryPath(operations)
211
+ return { layout: 'standalone', root: null, rootFingerprint: null, meetingCount: 0, warnings: [], domains: [] }
212
+ }
213
+
214
+ export function meetingLibraryConfigured(): boolean {
215
+ const layout = resolveMeetingLibrary().layout
216
+ return layout === 'direct' || layout === 'multi_domain'
217
+ }
218
+
101
219
  function boundedMeetingSource(content: string): { sourceContent: string; sourceTruncated: boolean } {
102
220
  const bytes = Buffer.from(content, 'utf8')
103
221
  if (bytes.length <= MEETING_SOURCE_MAX_BYTES) return { sourceContent: content, sourceTruncated: false }
@@ -364,6 +482,41 @@ export function findCosOperationsMeetingBySessionId(sessionId: string): {
364
482
  return null
365
483
  }
366
484
 
485
+ export type MeetingLibraryRecord = NonNullable<ReturnType<typeof findCosOperationsMeetingBySessionId>> & {
486
+ recordId?: string
487
+ librarySource?: 'direct_library' | 'cos_operations'
488
+ mutable?: boolean
489
+ }
490
+
491
+ /** Read resolver for direct libraries. Mutations must require recordId and
492
+ * mutable=true; the legacy operations resolver above remains writable. */
493
+ export function findDirectLibraryMeetingBySessionId(sessionId: string): MeetingLibraryRecord | null {
494
+ const inspection = resolveMeetingLibrary()
495
+ if (inspection.layout !== 'direct' || !inspection.root) return null
496
+ const root = inspection.root
497
+ const months = readdirSync(root).filter(name => MONTH_PATTERN.test(name)).sort().reverse()
498
+ for (const month of months) {
499
+ const monthDir = safeChildDirectory(root, join(root, month))
500
+ if (!monthDir) continue
501
+ for (const sidecarName of readdirSync(monthDir).filter(name => name.endsWith('.g2-chunks.json')).sort().reverse()) {
502
+ const filename = sidecarName.replace(/\.g2-chunks\.json$/, '.md')
503
+ if (sidecarSessionId(monthDir, filename) !== sessionId) continue
504
+ const meetingPath = join(monthDir, filename)
505
+ const bounded = safeBoundedRegularFile(monthDir, meetingPath, MEETING_SOURCE_MAX_BYTES)
506
+ if (bounded == null) continue
507
+ const title = bounded.content.match(/^#\s+(.+)$/m)?.[1]?.trim() || filename.replace(/\.md$/, '')
508
+ return {
509
+ sidecarPath: join(monthDir, sidecarName), meetingPath, filename,
510
+ domain: 'library', month, title,
511
+ recordId: `direct:${month}:${filename}`,
512
+ librarySource: 'direct_library',
513
+ mutable: false,
514
+ }
515
+ }
516
+ }
517
+ return null
518
+ }
519
+
367
520
  export function listCosOperationsMeetings(options: {
368
521
  limit?: number
369
522
  domain?: string
@@ -387,7 +540,6 @@ export function listCosOperationsMeetings(options: {
387
540
  .filter(d => /^\d{4}-\d{2}$/.test(d))
388
541
  .sort()
389
542
  .reverse()
390
- .slice(0, 3)
391
543
 
392
544
  for (const month of months) {
393
545
  const monthDir = join(meetingsBase, month)
@@ -403,6 +555,10 @@ export function listCosOperationsMeetings(options: {
403
555
  const content = readFileSync(filepath, 'utf-8')
404
556
  const meta = withMeetingListInsights(parseMeetingMeta(content.slice(0, 4000), file, domain), content)
405
557
  meta.month = month
558
+ meta.librarySource = 'cos_operations'
559
+ meta.recordId = `ops:${domain}:${month}:${file}`
560
+ meta.mutable = true
561
+ meta.canonicalRecord = `operations/${domain}/meetings/${month}/${file}`
406
562
  const sessionId = sidecarSessionId(monthDir, file)
407
563
  if (sessionId) meta.sessionId = sessionId
408
564
  allMeetings.push(meta)
@@ -417,6 +573,35 @@ export function listCosOperationsMeetings(options: {
417
573
  return allMeetings.slice(0, limit)
418
574
  }
419
575
 
576
+ export function listDirectLibraryMeetings(options: { limit?: number } = {}): CosOperationsMeetingMeta[] {
577
+ const inspection = resolveMeetingLibrary()
578
+ if (inspection.layout !== 'direct' || !inspection.root) return []
579
+ const limit = Math.min(Math.max(options.limit ?? 20, 1), 50)
580
+ const all: CosOperationsMeetingMeta[] = []
581
+ let candidates = 0
582
+ for (const month of readdirSync(inspection.root).filter(name => MONTH_PATTERN.test(name)).sort().reverse()) {
583
+ const monthDir = safeChildDirectory(inspection.root, join(inspection.root, month))
584
+ if (!monthDir) continue
585
+ for (const file of readdirSync(monthDir).filter(name => name.endsWith('.md')).sort().reverse()) {
586
+ if (++candidates > MAX_LIST_CANDIDATES) break
587
+ const bounded = safeBoundedRegularFile(monthDir, join(monthDir, file), MAX_LIST_FILE_BYTES)
588
+ if (bounded == null) continue
589
+ const content = bounded.content
590
+ const meta = withMeetingListInsights(parseMeetingMeta(content.slice(0, 4000), file, 'library'), content)
591
+ meta.month = month
592
+ meta.librarySource = 'direct_library'
593
+ meta.recordId = `direct:${month}:${file}`
594
+ meta.mutable = false
595
+ const sessionId = sidecarSessionId(monthDir, file)
596
+ if (sessionId) meta.sessionId = sessionId
597
+ all.push(meta)
598
+ }
599
+ if (candidates > MAX_LIST_CANDIDATES) break
600
+ }
601
+ all.sort(compareMeetingsNewestFirst)
602
+ return all.slice(0, limit)
603
+ }
604
+
420
605
  export function getCosOperationsMeetingDetail(
421
606
  domain: string,
422
607
  month: string,
@@ -473,3 +658,36 @@ export function getCosOperationsMeetingDetail(
473
658
  ...source,
474
659
  }
475
660
  }
661
+
662
+ export function getDirectLibraryMeetingDetail(month: string, filename: string): MeetingDetail | null {
663
+ const inspection = resolveMeetingLibrary()
664
+ if (inspection.layout !== 'direct' || !inspection.root) return null
665
+ if (!MONTH_PATTERN.test(month) || basename(filename) !== filename || !filename.endsWith('.md')) return null
666
+ const monthDir = safeChildDirectory(inspection.root, join(inspection.root, month))
667
+ if (!monthDir) return null
668
+ let resolvedFilename = filename
669
+ let bounded = safeBoundedRegularFile(monthDir, join(monthDir, resolvedFilename), MEETING_SOURCE_MAX_BYTES)
670
+ if (bounded == null) {
671
+ const candidates = readdirSync(monthDir).filter(name => name.endsWith('.md')).slice(0, MAX_LIST_CANDIDATES)
672
+ .map(name => ({ filename: name, content: safeRegularFile(monthDir, join(monthDir, name), 4_000) ?? '' }))
673
+ const renamed = matchRenamedMeetingFilename(filename, candidates)
674
+ if (!renamed) return null
675
+ resolvedFilename = renamed
676
+ bounded = safeBoundedRegularFile(monthDir, join(monthDir, resolvedFilename), MEETING_SOURCE_MAX_BYTES)
677
+ if (bounded == null) return null
678
+ }
679
+ const content = bounded.content
680
+ const meta = parseMeetingMeta(content, resolvedFilename, 'library')
681
+ meta.month = month
682
+ meta.librarySource = 'direct_library'
683
+ meta.recordId = `direct:${month}:${resolvedFilename}`
684
+ meta.mutable = false
685
+ const sessionId = sidecarSessionId(monthDir, resolvedFilename)
686
+ if (sessionId) meta.sessionId = sessionId
687
+ const source = { sourceContent: content, sourceTruncated: bounded.truncated }
688
+ return {
689
+ ...meta,
690
+ summary: extractSummary(content), topics: extractTopics(content), decisions: extractDecisions(content),
691
+ actionItems: extractActionItems(content), attendees: extractAttendees(content), transcript: content, ...source,
692
+ }
693
+ }
@@ -60,6 +60,11 @@ function isChunkWav(name: string): boolean {
60
60
  return /^chunk_\d+\.wav$/.test(name)
61
61
  }
62
62
 
63
+ /** Derived playback copies count against the cap but never define retention age. */
64
+ function isDerivedPlaybackWav(name: string): boolean {
65
+ return /^playback_v\d+_\d+\.wav$/.test(name)
66
+ }
67
+
63
68
  export interface ArchiveResult {
64
69
  linked: number
65
70
  /** Files that had to be copied because the link failed (e.g. cross-device). */
@@ -110,19 +115,40 @@ export function archiveSessionAudio(sessionId: string, sourceDir: string): Archi
110
115
  }
111
116
 
112
117
  /** Bytes and age for one archived session. */
113
- function sessionSize(dir: string): { bytes: number; mtimeMs: number; files: number } {
114
- let bytes = 0, mtimeMs = 0, files = 0
118
+ function sessionSize(dir: string): {
119
+ bytes: number
120
+ mtimeMs: number
121
+ files: number
122
+ derivedFiles: number
123
+ otherEntries: number
124
+ statFailures: number
125
+ readable: boolean
126
+ } {
127
+ let bytes = 0, mtimeMs = 0, files = 0, derivedFiles = 0
128
+ let otherEntries = 0, statFailures = 0, readable = false
115
129
  try {
116
- for (const name of readdirSync(dir).filter(isChunkWav)) {
130
+ const names = readdirSync(dir)
131
+ readable = true
132
+ for (const name of names) {
133
+ if (!isChunkWav(name) && !isDerivedPlaybackWav(name)) {
134
+ otherEntries++
135
+ continue
136
+ }
117
137
  try {
118
138
  const st = statSync(join(dir, name))
119
139
  bytes += st.size
120
- files++
121
- mtimeMs = Math.max(mtimeMs, st.mtimeMs)
122
- } catch { /* skip unreadable */ }
140
+ // A replay created six days after capture must not buy the raw evidence
141
+ // another seven days. Only immutable raw chunks determine session age.
142
+ if (isChunkWav(name)) {
143
+ files++
144
+ mtimeMs = Math.max(mtimeMs, st.mtimeMs)
145
+ } else {
146
+ derivedFiles++
147
+ }
148
+ } catch { statFailures++ }
123
149
  }
124
150
  } catch { /* unreadable dir reports zero */ }
125
- return { bytes, mtimeMs, files }
151
+ return { bytes, mtimeMs, files, derivedFiles, otherEntries, statFailures, readable }
126
152
  }
127
153
 
128
154
  export interface SweepResult {
@@ -145,7 +171,14 @@ export function sweepMeetingAudio(nowMs: number, ttlMs = meetingAudioTtlMs()): S
145
171
  try { names = readdirSync(root) } catch { return out }
146
172
  for (const name of names) {
147
173
  const dir = join(root, name)
148
- const { bytes, mtimeMs } = sessionSize(dir)
174
+ const { bytes, mtimeMs, files, derivedFiles, otherEntries, statFailures, readable } = sessionSize(dir)
175
+ // Delete only a provably cache-only directory. Any unknown entry or failed
176
+ // stat may be retained evidence, so ambiguity fails closed to preservation.
177
+ if (readable && files === 0 && derivedFiles > 0 && otherEntries === 0 && statFailures === 0) {
178
+ try { rmSync(dir, { recursive: true, force: true }); out.removed.push(name); out.bytesFreed += bytes }
179
+ catch { out.retained.push(name) }
180
+ continue
181
+ }
149
182
  if (mtimeMs <= 0) { out.retained.push(name); continue }
150
183
  if (nowMs - mtimeMs > ttlMs) {
151
184
  try { rmSync(dir, { recursive: true, force: true }); out.removed.push(name); out.bytesFreed += bytes }
@@ -52,6 +52,7 @@ export interface MeetingMeta {
52
52
  sessionId?: string
53
53
  title: string
54
54
  date: string
55
+ time?: string
55
56
  domain: string
56
57
  domainAbbr: string
57
58
  source: string
@@ -64,6 +65,12 @@ export interface MeetingMeta {
64
65
  decisionCount?: number
65
66
  actionCount?: number
66
67
  attendeeCount?: number
68
+ /** Additive archive identity. Older companions ignore these fields. */
69
+ librarySource?: 'direct_library' | 'cos_operations' | 'standalone_recordings'
70
+ recordId?: string
71
+ mutable?: boolean
72
+ /** Present only when the server can state a truthful local record. */
73
+ canonicalRecord?: string
67
74
  }
68
75
 
69
76
  export interface MeetingActionItem {
@@ -437,7 +444,7 @@ export class MeetingStore {
437
444
  // When COS ops is configured, use the private-app pipeline markers so
438
445
  // sync_meetings.py --g2-file will enrich (and reclassify domain). Plain
439
446
  // "Standalone recording" summaries are treated as already-final and skipped.
440
- ...(process.env.COS_SCRIPTS_DIR || process.env.COS_OPERATIONS_DIR || process.env.COS_MEETINGS_ROOT
447
+ ...(process.env.COS_SCRIPTS_DIR
441
448
  ? [
442
449
  '<!-- g2-needs-domain-review -->',
443
450
  '',
@@ -10,6 +10,7 @@ import { profileProvenanceSummary, speakerModelState, speakerReadiness } from '.
10
10
  import { chunkEmbeddingStoreStats } from '../lib/chunk-embedding-store.js'
11
11
  import { correctionStoreStats } from '../lib/meeting-corrections.js'
12
12
  import { meetingAudioStats } from '../lib/meeting-audio-archive.js'
13
+ import { adaptivePlaybackStatus } from '../lib/adaptive-playback-audio.js'
13
14
  import { getAvailableCliSessionId } from '../lib/claude-bridge.js'
14
15
  import {
15
16
  isWhisperLocalAvailable,
@@ -46,6 +47,7 @@ import { getHealthStaticProbes } from '../lib/health-static-probes.js'
46
47
  import { getEarlyMeetingSyncSnapshot } from '../lib/g2-ops-handoff.js'
47
48
  import { getProgressiveHqSnapshot } from '../lib/meeting-batch-transcribe.js'
48
49
  import { getMeetingFinalizationSnapshot } from '../lib/meeting-finalization-jobs.js'
50
+ import { resolveMeetingLibrary } from '../lib/cos-operations-meetings.js'
49
51
 
50
52
  export const healthRouter = Router()
51
53
 
@@ -125,7 +127,10 @@ healthRouter.get('/health', async (_req, res) => {
125
127
  // `pending` is the number that matters here: an intent that never closed means
126
128
  // some meeting's files may be half-rewritten.
127
129
  const speakerCorrections = correctionStoreStats()
128
- const reviewAudio = meetingAudioStats()
130
+ const reviewAudio = {
131
+ ...meetingAudioStats(),
132
+ adaptivePlayback: adaptivePlaybackStatus(),
133
+ }
129
134
  // `noHumanSample` is the one to read: a profile with no human-verified sample
130
135
  // is trained entirely on labels the system chose for itself.
131
136
  const voiceProvenance = speakerId.state === 'active' ? profileProvenanceSummary() : null
@@ -230,6 +235,7 @@ healthRouter.get('/health', async (_req, res) => {
230
235
  recovered: item.recovered,
231
236
  })),
232
237
  }
238
+ const meetingLibrary = resolveMeetingLibrary()
233
239
  res.json({
234
240
  ...checks,
235
241
  server_version: managedServerVersion(),
@@ -245,6 +251,11 @@ healthRouter.get('/health', async (_req, res) => {
245
251
  codex_models,
246
252
  cursor_models,
247
253
  meeting_sync,
254
+ meeting_library: {
255
+ layout: meetingLibrary.layout,
256
+ ready: meetingLibrary.layout !== 'invalid_explicit_root',
257
+ warningCount: meetingLibrary.warnings.length,
258
+ },
248
259
  unsaved_captures,
249
260
  chunk_embeddings: chunkEmbeddings,
250
261
  speaker_corrections: speakerCorrections,