@gotcos/glasses-server 6.93.0 → 6.94.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## 6.94.0 - Saved Mac recordings are filed into the COS meeting library
2
+
3
+ 2026-10-27. Pairs with COS operations `sync_meetings.py` on branch `claude/mac-library-cos` (the `--mac-import-file` import and `--capabilities`). Works with COS Control 0.5.351; Control needs no change.
4
+
5
+ - **Why:** a saved Mac system-audio recording never reached `operations/`. `routes/meeting.ts` skipped the hand-off for every Mac capture (the `MAC_CAPTURE_ID` checks added with Mac capture in 6.73.x), and `sync_meetings.py` only recognised files whose Source is G2 Glasses. Mac meetings were missing from vector search, LightRAG and task extraction, and kept the save-time domain, which is `personal` whenever the profile defines no domain keywords. On 2026-10-27 the glasses copy of the Quilt QBR was filed under quilt while the Mac copy of the same meeting stayed a personal standalone recording.
6
+ - **What:** after a Mac recording finishes (its standalone summary first, unchanged), the server runs `sync_meetings.py --mac-import-file <recording> --quiet` with the same retry ladder and result line as the G2 hand-off. The pipeline stages it under `personal/meetings/YYYY-MM/`, runs the same summary, title and domain step a G2 recording gets, and moves it to the domain the model picks, titled with a `(Mac)` suffix. The filed copy keeps `Source: Mac System Audio`, `Capture source: mac_system_audio` and its `.g2-chunks.json` sidecar with `sourceType`, so `GET /api/meetings` shows one row for it (the operations row, keyed by the same `mac_` session) and Control still treats it as a Mac recording. The same path runs after orphan recovery of a Mac capture.
7
+ - **Only when the pipeline says it can:** the server reads `sync_meetings.py --capabilities` and files Mac recordings only when it reports both `mac_import: 1` and `mac_twins: 2` or later, cached per script version. An older `sync_meetings.py` has no `--capabilities`, is never spawned for it, and the recording stays standalone exactly as in 6.93.0. A script with the import but an older twin rule (`mac_twins: 1` knew only glasses twins) is treated the same way.
8
+ - **One meeting, recorded twice:** when the glasses or Fireflies (or Granola) also recorded the meeting, the pipeline files the Mac copy as a twin (`<!-- twin-of: <file> -->` in its markdown): it is searchable, but never queued for LightRAG, never scanned for task completions and never turned into Work. Twin means both the existing G2 time gates (60 minute window, overlap at least 0.50, duration ratio at least 0.50; a cloud scribe uses its own Date and Duration) and text-anchor agreement with the merge engine's pairing evidence (`auto_merge` tier: at least 30 shared phrases at a consistent offset and three times the next different meeting, glasses or cloud). A cloud scribe needs its word-timed Fireflies sentences to be scored; Granola scribes carry none. A glasses copy and a cloud scribe of the same call are one meeting, and the marker names the cloud scribe. Copies of one recording (a session filed twice, a capture and the scribe it was blended into) count once. The marker also carries the recording's identity (`g2:<sessionId>` or `fireflies:<id>`), so a retitled or moved target is followed. A twin is released only when its recording is absent on two checks at least 24 hours apart; a file that exists but cannot be read counts as unknown, never as gone, and a marker with no identity is never released. The marker names a cloud scribe only when that scribe itself carries the shared phrases. Overlapping time with different words is not a twin and is processed fully. Either recording may arrive first: a Mac recording with no twin yet is held from those consumers for 12 hours after it ends (`COS_MAC_TWIN_HOLD_HOURS`), and filing the glasses copy or saving the cloud scribe marks it. `mac_twins.py release <file>` re-admits a twin, idempotently and safely across a crash. 13 of the 17 Mac recordings saved in October are twins.
9
+ - **Never a G2 file:** a Mac or iPhone recording is refused by every G2 path. `patchRecordingForG2Pipeline` refuses to relabel one as G2 Glasses, `stageRecordingIntoOperations` refuses a Mac or iPhone sidecar, and `handoffMeetingToOperations` refuses one before spawning anything. On the COS side the G2 import, the G2 sync, the retroactive blend, `g2_blend_backfill.py` and `meeting_dedup_cleanup.py` skip any sidecar whose source is Mac or iPhone, including one that carries speaker names. The merge engine already excluded Mac captures, and the merge trigger still never fires for one.
10
+ - **Failure:** filing starts only after the capture's own finalization (or orphan recovery) is complete and its job or lease released, so it holds no maintenance lease (a restart drain never waits on it) and a restart mid-filing never re-runs the standalone summary. A filing that fails is logged and never fails the capture. If the pipeline's model step fails, the staged copy stays held (out of LightRAG and Work, untitled titles never settle into Work) and `sync_meetings.py` exits 2 so the server retries; the hourly sync then retries it at most 3 times (2 per run, `COS_MAC_IMPORT_RETRY=0` turns that off), and `mac_library_backfill.py` files it on demand. A hand-off that never reached the pipeline (three attempts that all wait out the sync lock, or a restart before staging) is caught by the hourly sync, which files any finished Mac capture of the last two days the library does not hold, inside the same per-run budget.
11
+ - **Idempotent:** a re-save of a saved capture answers its receipt and files nothing; the pipeline finds a capture already in the library by session id, whatever it was renamed or moved to, and refuses to overwrite a file that belongs to another capture.
12
+ - **Existing recordings:** `operations/scripts/mac_library_backfill.py` lists every Mac recording in `~/.cos-glasses/data/recordings` with its staging destination and a keyword domain guess (dry run is the default), and `--apply` files each one through the same `--mac-import-file` command.
13
+ - **Unchanged:** a G2 save files byte for byte as in 6.93.0 (pinned by `server/routes/g2-filing-golden.test.ts` against a fixture recorded on 6.93.0, for both the locked import and an older sync script). The local Mac recording is written exactly as before. Two recordings of one meeting (glasses and Mac) are filed as two meetings; merging them is later work.
14
+ - **Publish order:** merge the COS `sync_meetings.py` change first, then publish this server. Either alone is safe: this server against the older script leaves Mac recordings standalone, and the new script does nothing until a server or the backfill asks.
15
+
1
16
  ## 6.93.0 - Live Mac speaker numbering on by default, assign a Mac voice to an existing voice profile, and iPhone microphone meetings (opt-in), plus the unpublished 6.92.1
2
17
 
3
18
  2026-10-26. Pairs with COS Control 0.5.351. Includes everything in 6.92.1 (never published on its own): the per-chunk anonymous anchors (`COS_MAC_LIVE_VOICES=anchors`), final-pass crosswalk of live numbers, the body-parser floor, and the isolated continuous-speaker protocol modules (still unwired).
package/README.md CHANGED
@@ -712,6 +712,18 @@ This is the microphone only, not iOS system audio, so it cannot record Zoom,
712
712
  YouTube or call audio playing on the phone. COS Control must be 0.5.351 or newer
713
713
  before this is switched on. Protocol: `docs/iphone-microphone-protocol.md`.
714
714
 
715
+ ### Mac recordings in the COS meeting library (6.94.0)
716
+
717
+ With COS operations configured (`COS_SCRIPTS_DIR`), a saved Mac recording is filed
718
+ into `operations/{domain}/meetings/YYYY-MM/` after its standalone summary, through
719
+ `sync_meetings.py --mac-import-file`: the same summary, title and domain step as a
720
+ glasses meeting, titled `... (Mac)`, never matched or blended into a Fireflies or
721
+ Granola meeting. The server checks `sync_meetings.py --capabilities` first (it needs
722
+ `mac_import` and `mac_twins` 2); an older script leaves Mac recordings standalone. A Mac
723
+ recording of a meeting the glasses or Fireflies also recorded is filed as a twin:
724
+ searchable, but kept out of LightRAG, task scans and Work (`operations/scripts/mac_twins.py`). Recordings saved before 6.94.0 are filed with
725
+ `operations/scripts/mac_library_backfill.py` (dry run by default, `--apply` to file).
726
+
715
727
  ## Speech-to-text settings (6.72.0)
716
728
 
717
729
  One settings object chooses which engine previews live meeting speech, which
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.93.0",
3
+ "version": "6.94.0",
4
4
  "description": "COS Glasses: self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,6 +36,8 @@ export interface G2EnrichmentOptions {
36
36
  onAttempt?: (message: string) => void
37
37
  claimOnly?: boolean
38
38
  importLocal?: boolean
39
+ /** 6.94.0: file a saved Mac recording (`--mac-import-file`). Never combined with claimOnly. */
40
+ macImport?: boolean
39
41
  }
40
42
 
41
43
  export interface G2EnrichmentDependencies {
@@ -79,7 +81,11 @@ export function buildExactG2SyncArgs(
79
81
  meetingFile: string,
80
82
  claimOnly = false,
81
83
  importLocal = false,
84
+ macImport = false,
82
85
  ): string[] {
86
+ // A Mac recording has its own import mode in sync_meetings.py: it never runs
87
+ // the G2 paths, so it never takes --g2-only, --g2-file or --g2-claim-only.
88
+ if (macImport) return [syncScript, '--mac-import-file', meetingFile, '--quiet']
83
89
  return [
84
90
  syncScript,
85
91
  '--g2-only',
@@ -109,6 +115,7 @@ export async function spawnG2SyncAttempt(options: G2EnrichmentOptions): Promise<
109
115
  options.meetingFile,
110
116
  options.claimOnly,
111
117
  options.importLocal,
118
+ options.macImport,
112
119
  ),
113
120
  {
114
121
  cwd: options.scriptsDir,
@@ -10,7 +10,8 @@
10
10
  // lets sync_meetings reclassify).
11
11
  // 3. Run sync_meetings.py --g2-only --g2-file with the private-app retry helper.
12
12
 
13
- import { existsSync, mkdirSync, readFileSync } from 'node:fs'
13
+ import { execFile } from 'node:child_process'
14
+ import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs'
14
15
  import { basename, dirname, join, resolve } from 'node:path'
15
16
  import { durableAtomicWriteFileSync } from './atomic-fs.js'
16
17
  import { resolveCosOperationsDir } from './cos-operations-meetings.js'
@@ -77,10 +78,38 @@ export interface G2StageOptions {
77
78
  revision?: number
78
79
  }
79
80
 
81
+ /** Sources that share the `.g2-chunks.json` sidecar shape but are not G2 glasses audio. */
82
+ const NON_G2_SOURCE_TYPES = new Set(['mac_system_audio', 'iphone_microphone'])
83
+ const NON_G2_HEADER = /\|\s*\*\*(?:Source\*\*\s*\|\s*(?:Mac System Audio|iPhone Microphone)|Capture source\*\*\s*\|\s*(?:mac_system_audio|iphone_microphone))\s*\|/i
84
+
85
+ function sidecarSourceType(sidecar: Record<string, unknown> | null): string | null {
86
+ if (!sidecar) return null
87
+ if (typeof sidecar.sourceType === 'string') return sidecar.sourceType
88
+ const capture = sidecar.captureSource
89
+ if (capture && typeof capture === 'object' && typeof (capture as Record<string, unknown>).sourceType === 'string') {
90
+ return (capture as Record<string, unknown>).sourceType as string
91
+ }
92
+ return null
93
+ }
94
+
95
+ /**
96
+ * A Mac or iPhone capture, read from its markdown header or its sidecar. Neither is G2 audio,
97
+ * so neither may be relabelled "G2 Glasses" or handed to the G2 import, where sync_meetings
98
+ * would make it a candidate to blend into a Fireflies or Granola meeting.
99
+ */
100
+ export function isNonG2CaptureRecording(markdown: string, sidecar: Record<string, unknown> | null = null): boolean {
101
+ if (NON_G2_HEADER.test(markdown)) return true
102
+ const sourceType = sidecarSourceType(sidecar)
103
+ return sourceType !== null && NON_G2_SOURCE_TYPES.has(sourceType)
104
+ }
105
+
80
106
  export function patchRecordingForG2Pipeline(
81
107
  markdown: string,
82
108
  options: G2StageOptions = {},
83
109
  ): string {
110
+ if (isNonG2CaptureRecording(markdown)) {
111
+ throw new Error('Refusing to relabel a Mac or iPhone recording as G2 Glasses')
112
+ }
84
113
  let text = markdown.replace(/\r\n?/g, '\n')
85
114
  if (!/\|\s*\*\*Source\*\*\s*\|\s*G2 Glasses\s*\|/i.test(text) && !text.includes('| G2 Glasses')) {
86
115
  text = text.replace(
@@ -115,6 +144,11 @@ export function patchRecordingForG2Pipeline(
115
144
  return text
116
145
  }
117
146
 
147
+ /** A missing or unreadable file reads as empty: the guard only refuses on positive evidence. */
148
+ function readTextOrEmpty(path: string): string {
149
+ try { return readFileSync(path, 'utf8') } catch { return '' }
150
+ }
151
+
118
152
  function parseSidecar(path: string): Record<string, unknown> | null {
119
153
  if (!existsSync(path)) return null
120
154
  try {
@@ -199,10 +233,13 @@ export function stageRecordingIntoOperations(
199
233
  return null
200
234
  }
201
235
 
236
+ const sourceSidecar = parseSidecar(localMeetingPath.replace(/\.md$/, '.g2-chunks.json'))
237
+ if (isNonG2CaptureRecording(readFileSync(localMeetingPath, 'utf8'), sourceSidecar)) {
238
+ throw new Error('Refusing to stage a Mac or iPhone recording through the G2 import')
239
+ }
202
240
  const destDir = join(operationsDir, 'personal', 'meetings', month)
203
241
  mkdirSync(destDir, { recursive: true })
204
242
  const destPath = join(destDir, basename(localMeetingPath))
205
- const sourceSidecar = parseSidecar(localMeetingPath.replace(/\.md$/, '.g2-chunks.json'))
206
243
  const destinationSidecar = parseSidecar(destPath.replace(/\.md$/, '.g2-chunks.json'))
207
244
  const keepMarkdown = g2Revision(destinationSidecar?.correctionRevision) > g2Revision(sourceSidecar?.correctionRevision)
208
245
  if (keepMarkdown && !existsSync(destPath)) throw new Error('Corrected operations markdown missing; refusing stale import')
@@ -241,6 +278,10 @@ async function runOperationsSync(localMeetingPath: string, claimOnly: boolean):
241
278
  console.log('[meeting/save] Standalone mode — skipping G2 sync pipeline')
242
279
  return
243
280
  }
281
+ // 6.94.0: a Mac or iPhone capture never takes the G2 import, whatever the caller asked.
282
+ if (isNonG2CaptureRecording(readTextOrEmpty(localMeetingPath), parseSidecar(localMeetingPath.replace(/\.md$/, '.g2-chunks.json')))) {
283
+ throw new Error('Refusing a Mac or iPhone recording on the G2 import; it has its own import')
284
+ }
244
285
  if (!PYTHON_BIN) throw new Error('COS operations configured but Python bridge is unavailable')
245
286
  if (!existsSync(PYTHON_BIN)) {
246
287
  throw new Error(`COS python missing at ${PYTHON_BIN}`)
@@ -331,3 +372,164 @@ export async function handoffMeetingToOperations(localMeetingPath: string): Prom
331
372
  earlySyncRuntime.lastError = null
332
373
  earlySyncRuntime.lastAt = new Date().toISOString()
333
374
  }
375
+
376
+ // ── 6.94.0: Mac recordings into the COS meeting library ─────────────────────
377
+ //
378
+ // A saved Mac system-audio recording is filed through the same pipeline as a G2 one
379
+ // (sync_meetings.py stages it, runs the LLM summary/title/domain step, renames and moves
380
+ // it), but through its own import, `--mac-import-file`, which never takes part in G2 to
381
+ // Fireflies/Granola matching. The server uses it only when the installed sync_meetings.py
382
+ // says it can (`--capabilities`); an older script leaves the recording standalone, exactly
383
+ // as before 6.94.0, and is never given a Mac file on the G2 import.
384
+
385
+ export interface SyncCapabilities {
386
+ macImport: boolean
387
+ /** Why Mac filing is unavailable; null when it is available. */
388
+ reason: string | null
389
+ }
390
+
391
+ export interface CapabilityProbeResult {
392
+ code: number | null
393
+ stdout: string
394
+ }
395
+
396
+ export type CapabilityProbe = (pythonBin: string, syncScript: string, cwd: string) => Promise<CapabilityProbeResult>
397
+
398
+ const CAPABILITY_PROBE_TIMEOUT_MS = 30_000
399
+ /** sync_meetings.py `mac_twins` version the server needs: 2 = twins of G2 recordings AND cloud scribes. */
400
+ export const MAC_TWINS_REQUIRED = 2
401
+ const capabilityCache = new Map<string, SyncCapabilities>()
402
+
403
+ function spawnCapabilityProbe(pythonBin: string, syncScript: string, cwd: string): Promise<CapabilityProbeResult> {
404
+ return new Promise(resolvePromise => {
405
+ execFile(
406
+ pythonBin,
407
+ [syncScript, '--capabilities'],
408
+ { cwd, timeout: CAPABILITY_PROBE_TIMEOUT_MS, maxBuffer: 64 * 1024, env: { ...process.env, PYTHONUNBUFFERED: '1' } },
409
+ (error, stdout) => {
410
+ const code = error ? (typeof (error as NodeJS.ErrnoException & { code?: unknown }).code === 'number' ? (error as unknown as { code: number }).code : null) : 0
411
+ resolvePromise({ code, stdout: String(stdout ?? '') })
412
+ },
413
+ )
414
+ })
415
+ }
416
+
417
+ /** Test seam: forget probed capabilities. */
418
+ export function resetSyncCapabilitiesCache(): void {
419
+ capabilityCache.clear()
420
+ }
421
+
422
+ /**
423
+ * What the installed sync_meetings.py can do, read from `--capabilities` rather than guessed.
424
+ * Mac filing needs `mac_import: 1` and `mac_twins: 2` or later.
425
+ *
426
+ * Cached per python + script + mtime + size, so an upgraded script is probed again. A script
427
+ * whose text never mentions `--capabilities` is an older one: it would take the sync lock
428
+ * before rejecting the flag, so it is never spawned and reads as "no Mac import". A failed or
429
+ * unreadable probe is not cached, so a transient failure is retried on the next recording.
430
+ */
431
+ export async function readSyncCapabilities(
432
+ pythonBin: string,
433
+ syncScript: string,
434
+ probe: CapabilityProbe = spawnCapabilityProbe,
435
+ ): Promise<SyncCapabilities> {
436
+ let key: string
437
+ let text: string
438
+ try {
439
+ const stat = statSync(syncScript)
440
+ key = `${pythonBin}\0${syncScript}\0${stat.mtimeMs}\0${stat.size}`
441
+ text = readFileSync(syncScript, 'utf8')
442
+ } catch {
443
+ return { macImport: false, reason: 'sync_script_missing' }
444
+ }
445
+ const cached = capabilityCache.get(key)
446
+ if (cached) return cached
447
+ if (!text.includes('--capabilities')) {
448
+ const result = { macImport: false, reason: 'sync_script_upgrade_required' }
449
+ capabilityCache.set(key, result)
450
+ return result
451
+ }
452
+ const probed = await probe(pythonBin, syncScript, dirname(syncScript))
453
+ if (probed.code !== 0) return { macImport: false, reason: 'capability_probe_failed' }
454
+ const line = probed.stdout.trim().split(/\r?\n/).filter(Boolean).pop() ?? ''
455
+ let parsed: unknown
456
+ try { parsed = JSON.parse(line) } catch { return { macImport: false, reason: 'capability_probe_unreadable' } }
457
+ // Both: the import, AND the twin rule (version 2: G2 recordings and Fireflies/Granola scribes)
458
+ // that keeps a Mac copy of an already-recorded meeting out of LightRAG and Work. A script with
459
+ // the import but an older twin rule would file a Mac + Fireflies meeting twice.
460
+ const caps = !!parsed && typeof parsed === 'object' ? parsed as Record<string, unknown> : {}
461
+ const macImport = caps.mac_import === 1 && typeof caps.mac_twins === 'number' && caps.mac_twins >= MAC_TWINS_REQUIRED
462
+ const result = { macImport, reason: macImport ? null : 'sync_script_upgrade_required' }
463
+ capabilityCache.set(key, result)
464
+ return result
465
+ }
466
+
467
+ export type MacLibraryFiling =
468
+ | { outcome: 'filed'; path: string; status: string }
469
+ | { outcome: 'standalone'; reason: string }
470
+
471
+ export interface MacLibraryDependencies {
472
+ probe?: CapabilityProbe
473
+ runEnrichment?: typeof runG2EnrichmentWithRetry
474
+ }
475
+
476
+ /** True only for a Mac capture: the sidecar says mac_system_audio AND the header says Mac System Audio. */
477
+ export function isMacSystemAudioRecording(markdown: string, sidecar: Record<string, unknown> | null): boolean {
478
+ return sidecarSourceType(sidecar) === 'mac_system_audio'
479
+ && /\|\s*\*\*Source\*\*\s*\|\s*Mac System Audio\s*\|/i.test(markdown)
480
+ }
481
+
482
+ /**
483
+ * File one saved Mac recording into the COS meeting library, or say why it stays standalone.
484
+ *
485
+ * Reads the operations env live (like cosOpsPipelineConfigured), so a Control-updated env is
486
+ * seen. Throws when the pipeline ran and failed; returns `standalone` when it never could run.
487
+ */
488
+ export async function fileMacRecordingInLibrary(
489
+ localMeetingPath: string,
490
+ deps: MacLibraryDependencies = {},
491
+ ): Promise<MacLibraryFiling> {
492
+ const raw = process.env.COS_SCRIPTS_DIR?.trim()
493
+ if (!raw) return { outcome: 'standalone', reason: 'operations_not_configured' }
494
+ const scriptsDir = resolve(raw)
495
+ const pythonBin = resolve(scriptsDir, 'venv/bin/python3')
496
+ const syncScript = join(scriptsDir, 'sync_meetings.py')
497
+ if (!existsSync(pythonBin)) return { outcome: 'standalone', reason: 'python_unavailable' }
498
+ if (!existsSync(syncScript)) return { outcome: 'standalone', reason: 'sync_script_missing' }
499
+
500
+ const markdown = readFileSync(localMeetingPath, 'utf8')
501
+ const sidecar = parseSidecar(localMeetingPath.replace(/\.md$/, '.g2-chunks.json'))
502
+ if (!isMacSystemAudioRecording(markdown, sidecar)) {
503
+ throw new Error(`Not a Mac system-audio recording: ${basename(localMeetingPath)}`)
504
+ }
505
+
506
+ const capabilities = await readSyncCapabilities(pythonBin, syncScript, deps.probe)
507
+ if (!capabilities.macImport) return { outcome: 'standalone', reason: capabilities.reason ?? 'sync_script_upgrade_required' }
508
+
509
+ const spawnPath = process.env.PATH?.includes('/opt/homebrew/bin')
510
+ ? process.env.PATH
511
+ : `/opt/homebrew/bin:${process.env.PATH || ''}`
512
+ const enrichment = await (deps.runEnrichment ?? runG2EnrichmentWithRetry)({
513
+ pythonBin,
514
+ syncScript,
515
+ scriptsDir,
516
+ meetingFile: localMeetingPath,
517
+ env: {
518
+ ...process.env,
519
+ PYTHONUNBUFFERED: '1',
520
+ PATH: spawnPath,
521
+ COS_G2_IMPORT_ROOT: dirname(dirname(localMeetingPath)),
522
+ COS_SERVER_INSTANCE_ID: getServerInstanceId() ?? '',
523
+ },
524
+ importLocal: true,
525
+ macImport: true,
526
+ onAttempt: message => console.log(`[meeting/save] Mac library filing: ${message}`),
527
+ })
528
+ if (!enrichment.ok || !enrichment.outcome) {
529
+ throw new Error(
530
+ `Mac library filing failed after ${enrichment.attempts} attempt(s): `
531
+ + `${enrichment.error ?? 'unknown exact-file sync failure'}`,
532
+ )
533
+ }
534
+ return { outcome: 'filed', path: enrichment.outcome.path, status: enrichment.outcome.status }
535
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "locked": {
3
+ "status": 200,
4
+ "response": {
5
+ "receiptVersion": 1,
6
+ "serverInstanceId": "<ID>",
7
+ "saved": true,
8
+ "filepath": "recordings/2026-10/2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.md",
9
+ "filename": "2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.md",
10
+ "durationMin": 0,
11
+ "domain": "personal",
12
+ "transcriptionQuality": "streaming",
13
+ "transferIntegrity": {
14
+ "completeness": 100,
15
+ "received": 3,
16
+ "expected": 3,
17
+ "missingChunks": 0,
18
+ "missingIndices": []
19
+ }
20
+ },
21
+ "recordings": {
22
+ "2026-10/2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.g2-chunks.json": "{\n \"schemaVersion\": 2,\n \"sessionId\": \"meeting_1793113298813_d0bm6p\",\n \"startTime\": 1793113298813,\n \"durationMs\": 20000,\n \"domain\": \"personal\",\n \"title\": \"G2 Recording 2026-10-27 1001\",\n \"canonicalProvider\": \"server-whisper\",\n \"providers\": [\n \"server-whisper\"\n ],\n \"providerCandidates\": {},\n \"speakers\": [\n \"Jeremy Sokolic\"\n ],\n \"chunks\": [\n {\n \"text\": \"ARR crossed one hundred million, line 0.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 0,\n \"similarity\": 0.91\n },\n {\n \"text\": \"ARR crossed one hundred million, line 1.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 10000,\n \"similarity\": 0.91\n },\n {\n \"text\": \"ARR crossed one hundred million, line 2.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 20000,\n \"similarity\": 0.91\n }\n ],\n \"chunkEntries\": [\n {\n \"chunkIndex\": 0,\n \"chunk\": {\n \"text\": \"ARR crossed one hundred million, line 0.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 0,\n \"similarity\": 0.91\n }\n },\n {\n \"chunkIndex\": 1,\n \"chunk\": {\n \"text\": \"ARR crossed one hundred million, line 1.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 10000,\n \"similarity\": 0.91\n }\n },\n {\n \"chunkIndex\": 2,\n \"chunk\": {\n \"text\": \"ARR crossed one hundred million, line 2.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 20000,\n \"similarity\": 0.91\n }\n }\n ],\n \"transferIntegrity\": {\n \"received\": 3,\n \"stored\": 3,\n \"maxIndex\": 2,\n \"expected\": 3,\n \"missingIndices\": [],\n \"completeness\": 1\n },\n \"transcriptionQuality\": \"streaming\",\n \"batchApplied\": false,\n \"streamingWordCount\": 27,\n \"finalizationState\": \"complete\",\n \"claimPending\": false,\n \"finalizationUpdatedAt\": \"<ISO>\"\n}\n",
23
+ "2026-10/2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.md": "# G2 Recording 2026-10-27 1001\n\n| Field | Value |\n|-------|-------|\n| **Date** | 2026-10-27 |\n| **Time** | 10:01 AM |\n| **Duration** | 0 minutes |\n| **Source** | G2 Glasses |\n| **Domain** | personal |\n| **Transfer integrity** | 100% |\n| **Transcription quality** | streaming |\n\n## Summary\n\n<!-- g2-needs-domain-review -->\n\n*G2 recording — summary pending pipeline processing.*\n\n## Transcript\n\n[Jeremy Sokolic]: ARR crossed one hundred million, line 0.\n[Jeremy Sokolic]: ARR crossed one hundred million, line 1.\n[Jeremy Sokolic]: ARR crossed one hundred million, line 2.\n"
24
+ },
25
+ "operations": {
26
+ "quilt/meetings/2026-10/Filed_(G2).md": "# G2 Recording 2026-10-27 1001\n\n| Field | Value |\n|-------|-------|\n| **Date** | 2026-10-27 |\n| **Time** | 10:01 AM |\n| **Duration** | 0 minutes |\n| **Source** | G2 Glasses |\n| **Domain** | personal |\n| **Transfer integrity** | 100% |\n| **Transcription quality** | streaming |\n\n## Summary\n\n<!-- g2-needs-domain-review -->\n\n*G2 recording — summary pending pipeline processing.*\n\n## Transcript\n\n[Jeremy Sokolic]: ARR crossed one hundred million, line 0.\n[Jeremy Sokolic]: ARR crossed one hundred million, line 1.\n[Jeremy Sokolic]: ARR crossed one hundred million, line 2.\n",
27
+ "scripts/invocations.jsonl": "{\"args\":[\"--g2-only\",\"--g2-import-file\",\"<ROOT>/data/recordings/2026-10/2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.md\",\"--quiet\"],\"importRoot\":\"<ROOT>/data/recordings\",\"unbuffered\":\"1\"}\n",
28
+ "scripts/sync_meetings.py": "\nconst fs = require('fs'), path = require('path'); const args = process.argv.slice(2)\nfs.appendFileSync(path.join(__dirname, 'invocations.jsonl'), JSON.stringify({ args, importRoot: process.env.COS_G2_IMPORT_ROOT || null, unbuffered: process.env.PYTHONUNBUFFERED || null }) + '\\n')\n// supports --g2-import-file\nconst at = args.indexOf(\"--g2-import-file\"); const target = args[at + 1]\nconst dest = path.join(__dirname, '..', 'quilt', 'meetings', '2026-10', 'Filed_(G2).md')\nfs.mkdirSync(path.dirname(dest), { recursive: true }); fs.writeFileSync(dest, fs.readFileSync(target))\nprocess.stdout.write('COS_G2_RESULT=' + JSON.stringify({ status: 'enriched', path: dest, title: 'Filed (G2)' }) + '\\n')\n"
29
+ }
30
+ },
31
+ "older": {
32
+ "status": 200,
33
+ "response": {
34
+ "receiptVersion": 1,
35
+ "serverInstanceId": "<ID>",
36
+ "saved": true,
37
+ "filepath": "recordings/2026-10/2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.md",
38
+ "filename": "2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.md",
39
+ "durationMin": 0,
40
+ "domain": "personal",
41
+ "transcriptionQuality": "streaming",
42
+ "transferIntegrity": {
43
+ "completeness": 100,
44
+ "received": 3,
45
+ "expected": 3,
46
+ "missingChunks": 0,
47
+ "missingIndices": []
48
+ }
49
+ },
50
+ "recordings": {
51
+ "2026-10/2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.g2-chunks.json": "{\n \"schemaVersion\": 2,\n \"sessionId\": \"meeting_1793113298813_d0bm6p\",\n \"startTime\": 1793113298813,\n \"durationMs\": 20000,\n \"domain\": \"personal\",\n \"title\": \"G2 Recording 2026-10-27 1001\",\n \"canonicalProvider\": \"server-whisper\",\n \"providers\": [\n \"server-whisper\"\n ],\n \"providerCandidates\": {},\n \"speakers\": [\n \"Jeremy Sokolic\"\n ],\n \"chunks\": [\n {\n \"text\": \"ARR crossed one hundred million, line 0.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 0,\n \"similarity\": 0.91\n },\n {\n \"text\": \"ARR crossed one hundred million, line 1.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 10000,\n \"similarity\": 0.91\n },\n {\n \"text\": \"ARR crossed one hundred million, line 2.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 20000,\n \"similarity\": 0.91\n }\n ],\n \"chunkEntries\": [\n {\n \"chunkIndex\": 0,\n \"chunk\": {\n \"text\": \"ARR crossed one hundred million, line 0.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 0,\n \"similarity\": 0.91\n }\n },\n {\n \"chunkIndex\": 1,\n \"chunk\": {\n \"text\": \"ARR crossed one hundred million, line 1.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 10000,\n \"similarity\": 0.91\n }\n },\n {\n \"chunkIndex\": 2,\n \"chunk\": {\n \"text\": \"ARR crossed one hundred million, line 2.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 20000,\n \"similarity\": 0.91\n }\n }\n ],\n \"transferIntegrity\": {\n \"received\": 3,\n \"stored\": 3,\n \"maxIndex\": 2,\n \"expected\": 3,\n \"missingIndices\": [],\n \"completeness\": 1\n },\n \"transcriptionQuality\": \"streaming\",\n \"batchApplied\": false,\n \"streamingWordCount\": 27,\n \"finalizationState\": \"complete\",\n \"claimPending\": false,\n \"finalizationUpdatedAt\": \"<ISO>\"\n}\n",
52
+ "2026-10/2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.md": "# G2 Recording 2026-10-27 1001\n\n| Field | Value |\n|-------|-------|\n| **Date** | 2026-10-27 |\n| **Time** | 10:01 AM |\n| **Duration** | 0 minutes |\n| **Source** | G2 Glasses |\n| **Domain** | personal |\n| **Transfer integrity** | 100% |\n| **Transcription quality** | streaming |\n\n## Summary\n\n<!-- g2-needs-domain-review -->\n\n*G2 recording — summary pending pipeline processing.*\n\n## Transcript\n\n[Jeremy Sokolic]: ARR crossed one hundred million, line 0.\n[Jeremy Sokolic]: ARR crossed one hundred million, line 1.\n[Jeremy Sokolic]: ARR crossed one hundred million, line 2.\n"
53
+ },
54
+ "operations": {
55
+ "personal/meetings/2026-10/2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.g2-chunks.json": "{\n \"schemaVersion\": 2,\n \"sessionId\": \"meeting_1793113298813_d0bm6p\",\n \"startTime\": 1793113298813,\n \"durationMs\": 20000,\n \"domain\": \"personal\",\n \"title\": \"G2 Recording 2026-10-27 1001\",\n \"canonicalProvider\": \"server-whisper\",\n \"providers\": [\n \"server-whisper\"\n ],\n \"providerCandidates\": {},\n \"speakers\": [\n \"Jeremy Sokolic\"\n ],\n \"chunks\": [\n {\n \"text\": \"ARR crossed one hundred million, line 0.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 0,\n \"similarity\": 0.91\n },\n {\n \"text\": \"ARR crossed one hundred million, line 1.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 10000,\n \"similarity\": 0.91\n },\n {\n \"text\": \"ARR crossed one hundred million, line 2.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 20000,\n \"similarity\": 0.91\n }\n ],\n \"chunkEntries\": [\n {\n \"chunkIndex\": 0,\n \"chunk\": {\n \"text\": \"ARR crossed one hundred million, line 0.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 0,\n \"similarity\": 0.91\n }\n },\n {\n \"chunkIndex\": 1,\n \"chunk\": {\n \"text\": \"ARR crossed one hundred million, line 1.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 10000,\n \"similarity\": 0.91\n }\n },\n {\n \"chunkIndex\": 2,\n \"chunk\": {\n \"text\": \"ARR crossed one hundred million, line 2.\",\n \"speaker\": \"Jeremy Sokolic\",\n \"elapsed\": 20000,\n \"similarity\": 0.91\n }\n }\n ],\n \"transferIntegrity\": {\n \"received\": 3,\n \"stored\": 3,\n \"maxIndex\": 2,\n \"expected\": 3,\n \"missingIndices\": [],\n \"completeness\": 1\n },\n \"transcriptionQuality\": \"streaming\",\n \"batchApplied\": false,\n \"streamingWordCount\": 27,\n \"finalizationState\": \"ops_pending\",\n \"claimPending\": false,\n \"finalizationUpdatedAt\": \"<ISO>\",\n \"correctionRevision\": 0,\n \"labelsNewerThanGraph\": false,\n \"lifecycleRevision\": 0,\n \"hqState\": \"unavailable\",\n \"syncState\": \"finalized\",\n \"serverInstanceId\":\"<ID>\",\n \"lifecycleUpdatedAt\": \"<ISO>\"\n}",
56
+ "personal/meetings/2026-10/2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.md": "# G2 Recording 2026-10-27 1001\n\n| Field | Value |\n|-------|-------|\n| **Date** | 2026-10-27 |\n| **Time** | 10:01 AM |\n| **Duration** | 0 minutes |\n| **Source** | G2 Glasses |\n| **Domain** | personal |\n| **Transfer integrity** | 100% |\n| **Transcription quality** | streaming |\n\n## Summary\n\n<!-- g2-needs-domain-review -->\n\n*G2 recording — summary pending pipeline processing.*\n\n## Transcript\n\n[Jeremy Sokolic]: ARR crossed one hundred million, line 0.\n[Jeremy Sokolic]: ARR crossed one hundred million, line 1.\n[Jeremy Sokolic]: ARR crossed one hundred million, line 2.\n",
57
+ "quilt/meetings/2026-10/Filed_(G2).md": "# G2 Recording 2026-10-27 1001\n\n| Field | Value |\n|-------|-------|\n| **Date** | 2026-10-27 |\n| **Time** | 10:01 AM |\n| **Duration** | 0 minutes |\n| **Source** | G2 Glasses |\n| **Domain** | personal |\n| **Transfer integrity** | 100% |\n| **Transcription quality** | streaming |\n\n## Summary\n\n<!-- g2-needs-domain-review -->\n\n*G2 recording — summary pending pipeline processing.*\n\n## Transcript\n\n[Jeremy Sokolic]: ARR crossed one hundred million, line 0.\n[Jeremy Sokolic]: ARR crossed one hundred million, line 1.\n[Jeremy Sokolic]: ARR crossed one hundred million, line 2.\n",
58
+ "scripts/invocations.jsonl": "{\"args\":[\"--g2-only\",\"--g2-file\",\"<ROOT>/Ukaoma Chief Of Staff/MU-Chief.Staff/operations/personal/meetings/2026-10/2026-10-27_G2_Recording_2026-10-27_1001_0dd775ff.md\",\"--quiet\"],\"importRoot\":\"<ROOT>/data/recordings\",\"unbuffered\":\"1\"}\n",
59
+ "scripts/sync_meetings.py": "\nconst fs = require('fs'), path = require('path'); const args = process.argv.slice(2)\nfs.appendFileSync(path.join(__dirname, 'invocations.jsonl'), JSON.stringify({ args, importRoot: process.env.COS_G2_IMPORT_ROOT || null, unbuffered: process.env.PYTHONUNBUFFERED || null }) + '\\n')\n\nconst at = args.indexOf(\"--g2-file\"); const target = args[at + 1]\nconst dest = path.join(__dirname, '..', 'quilt', 'meetings', '2026-10', 'Filed_(G2).md')\nfs.mkdirSync(path.dirname(dest), { recursive: true }); fs.writeFileSync(dest, fs.readFileSync(target))\nprocess.stdout.write('COS_G2_RESULT=' + JSON.stringify({ status: 'enriched', path: dest, title: 'Filed (G2)' }) + '\\n')\n"
60
+ }
61
+ }
62
+ }
@@ -187,6 +187,7 @@ import {
187
187
  import {
188
188
  claimMeetingInOperations,
189
189
  earlyMeetingSyncEnabled,
190
+ fileMacRecordingInLibrary,
190
191
  handoffMeetingToOperations,
191
192
  } from '../lib/g2-ops-handoff.js'
192
193
  import {
@@ -215,6 +216,33 @@ function cosOpsPipelineConfigured(): boolean {
215
216
  return Boolean(process.env.COS_SCRIPTS_DIR?.trim())
216
217
  }
217
218
 
219
+ /**
220
+ * 6.94.0: file a finished Mac recording into the COS meeting library.
221
+ *
222
+ * Runs AFTER the standalone enrichment, so the local recording is exactly what 6.93.0 left
223
+ * whether or not filing works. A Mac recording is never handed to the G2 import: it takes
224
+ * sync_meetings.py --mac-import-file, and only when that script reports the capability.
225
+ * A failure is logged and never fails the capture's own finalization, because a retry would
226
+ * re-run the standalone summary; mac_library_backfill.py files it later, idempotently.
227
+ */
228
+ async function fileFinishedMacRecording(sessionId: string, meetingPath: string, label: string): Promise<void> {
229
+ // Callers pass Mac captures only (finalization and orphan recovery both check MAC_CAPTURE_ID).
230
+ if (!cosOpsPipelineConfigured()) return
231
+ try {
232
+ const filing = await fileMacRecordingInLibrary(meetingPath)
233
+ if (filing.outcome === 'filed') {
234
+ console.log(`[${label}] Mac recording ${sessionId} filed into the library (${filing.status}): ${filing.path}`)
235
+ } else {
236
+ console.log(`[${label}] Mac recording ${sessionId} stays standalone: ${filing.reason}`)
237
+ }
238
+ } catch (error) {
239
+ console.warn(
240
+ `[${label}] Mac recording ${sessionId} was not filed into the library: `
241
+ + `${error instanceof Error ? error.message : String(error)}`,
242
+ )
243
+ }
244
+ }
245
+
218
246
  /**
219
247
  * Why a correction left the meeting's markdown alone when it could not read it.
220
248
  *
@@ -463,6 +491,13 @@ function scheduleFinalizationJob(job: MeetingFinalizationJob, runtime: Finalizat
463
491
  // 6.78.0: finished, so its crash-loop count (lib/boot-resume.ts) is done.
464
492
  bootResumeGate().forget('meeting_finalization', current.sessionId)
465
493
  finalizationRetryCounts.delete(key)
494
+ // Filed AFTER the capture's own finalization is complete and its job removed: the
495
+ // filing holds no maintenance lease (a restart drain never waits on it) and a restart
496
+ // mid-filing never re-runs the standalone summary. An interrupted filing leaves the
497
+ // staged copy unfinished, and sync_meetings' broad sync retries it.
498
+ if (MAC_CAPTURE_ID.test(current.sessionId)) {
499
+ runtime.scheduleBackground(fileFinishedMacRecording(current.sessionId, current.meetingPath, 'meeting/save'))
500
+ }
466
501
  }).catch(error => {
467
502
  const current = runtime.finalizationJobs.get(job.sessionId) ?? job
468
503
  try {
@@ -2926,6 +2961,7 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
2926
2961
  + 'ran for this capture. The capture leaves the unsaved count once its scribe is durable.',
2927
2962
  })
2928
2963
 
2964
+ let macFilingPath: string | null = null
2929
2965
  const task = Promise.resolve().then(async () => {
2930
2966
  lease.setPhase('active')
2931
2967
  const startedAt = Date.now()
@@ -3031,6 +3067,7 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
3031
3067
  // already holds the long-running orphan_recovery lease that COS
3032
3068
  // Control surfaces and warns on before committing a drain.
3033
3069
  await enrichStandaloneMeeting(saved.filepath, Date.now())
3070
+ if (macCapture) macFilingPath = saved.filepath
3034
3071
  }
3035
3072
  if (!macCapture && !phoneCapture) triggerMeetingMergeRun('orphan_recovered')
3036
3073
  }).catch(error => {
@@ -3050,7 +3087,11 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
3050
3087
  recoveringOrphans.delete(sessionId)
3051
3088
  lease.release()
3052
3089
  })
3053
- scheduleBackground(task)
3090
+ // As on the save path: a recovered Mac capture is filed only after the recovery has
3091
+ // finished and released its orphan_recovery lease, so a drain never waits on it.
3092
+ scheduleBackground(task.then(() => macFilingPath
3093
+ ? fileFinishedMacRecording(sessionId, macFilingPath, 'meeting/orphans')
3094
+ : undefined))
3054
3095
  })
3055
3096
 
3056
3097
  return router