@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.
|
|
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 {
|
|
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
|
+
}
|
package/server/routes/meeting.ts
CHANGED
|
@@ -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
|
-
|
|
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
|