@gotcos/glasses-server 6.45.5 → 6.46.1
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 +20 -0
- package/package.json +2 -2
- package/server/index.ts +2 -0
- package/server/lib/cos-operations-meetings.ts +1 -1
- package/server/lib/g2-ops-handoff.ts +42 -6
- package/server/lib/held-naming-batches.ts +527 -0
- package/server/lib/held-voice-groups.ts +103 -12
- package/server/lib/meeting-batch-persistence.ts +1 -0
- package/server/lib/meeting-corrections.ts +43 -5
- package/server/lib/meeting-speaker-labels.ts +192 -0
- package/server/lib/meeting-speaker-review.ts +18 -5
- package/server/lib/meeting-sync-lock.ts +26 -0
- package/server/lib/speaker-embeddings.ts +1 -1
- package/server/lib/speaker-rename-fanout.ts +91 -177
- package/server/lib/training-audio-save.ts +24 -0
- package/server/lib/voice-directory.ts +2 -1
- package/server/lib/voice-enrolment-selection.ts +25 -0
- package/server/lib/voice-profile-store.ts +8 -3
- package/server/routes/meeting.ts +5 -1
- package/server/routes/meetings.ts +50 -9
- package/server/routes/transcribe-stream.ts +13 -41
- package/server/routes/voice.ts +29 -43
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## 6.46.1
|
|
2
|
+
|
|
3
|
+
G2 recordings show in a multi-folder Meetings library that has no COS pipeline to file them. No Control or glasses update is needed.
|
|
4
|
+
|
|
5
|
+
- A Meetings library on a multi-folder operations tree (COS_OPERATIONS_DIR), on a Mac without a working COS pipeline (no COS_SCRIPTS_DIR, or no venv Python or sync_meetings.py in it), listed only that tree. G2 recordings saved there never reach it, so Control's Meetings window showed an empty month while the recordings sat in the server's own recordings store. GET /api/meetings now lists them beside the operations rows, with their months and day counts.
|
|
6
|
+
- A recording that also reached the operations tree is listed once, as the operations copy matched by the session id in its sidecar, and its day is counted once.
|
|
7
|
+
- Where the COS pipeline can run, the list is unchanged: sync_meetings.py decides which recordings join the operations tree. Search and meeting detail already read the recordings store in every layout.
|
|
8
|
+
|
|
9
|
+
## 6.46.0
|
|
10
|
+
|
|
11
|
+
Name held voice samples and preview the meeting labels they will change. Paired Control build: 0.5.223.
|
|
12
|
+
|
|
13
|
+
- Individual held samples can suggest an existing voice using the same second-best-sample scoring as groups, with a clear margin and owner-proximity caution. Suggestions remain proposals.
|
|
14
|
+
- Naming returns a server-stored preview first. Apply checks the preview, voice store, raw chunk map and each meeting copy again, holds the COS sync lock where available, and writes chunk-scoped confirmations. Wider matches must agree with the named samples and the updated voice profile in their source meeting.
|
|
15
|
+
- Corrected labels reach HQ word speakers, live chunks and operations copies. Transcript words remain unchanged; only fully aligned turn labels change. Newer speaker corrections survive re-import. Search catches up through the existing indexing queue; graph staleness is explicit.
|
|
16
|
+
- Each naming has durable copy receipts and Undo. Interrupted work requires review. Undo restores eligible labels, retains enrolled voice samples, and never resurrects deleted audio. Held audio is deleted after successful meeting receipts.
|
|
17
|
+
- Existing-name matching resolves stored spelling before preview. Owner naming requires an explicit acknowledgement and stronger acoustic match. Older clients cannot apply the new naming contract without a preview.
|
|
18
|
+
- Voice-profile merges retain stronger source evidence before applying oldest-first eviction within a source tier. Training audio has its own retention capacity, independent of profile size, and freed slots are usable without a restart.
|
|
19
|
+
- Compatibility: 6.45.5 safely ignores chunk-scoped confirmations. It does not understand reverted batch history; keep 6.46.0 for naming recovery and Undo history after rollback.
|
|
20
|
+
|
|
1
21
|
## 6.45.5
|
|
2
22
|
|
|
3
23
|
Sessions carry their last real activity and last tool (2026-09-13), for the phone Sessions list in COS Glasses 6.9.470.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
4
|
-
"description": "COS Glasses
|
|
3
|
+
"version": "6.46.1",
|
|
4
|
+
"description": "COS Glasses \u2014 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": {
|
|
7
7
|
"glasses-server": "bin/cli.cjs",
|
package/server/index.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { promptDraftsRouter } from './routes/prompt-drafts.js'
|
|
|
51
51
|
import { cliDebugRouter } from './routes/cli-debug.js'
|
|
52
52
|
import { maintenanceRouter } from './routes/maintenance.js'
|
|
53
53
|
import { ttsRouter } from './routes/tts.js'
|
|
54
|
+
import { recoverInterruptedNaming } from './lib/held-naming-batches.js'
|
|
54
55
|
import { voiceRouter } from './routes/voice.js'
|
|
55
56
|
import { glossaryRouter } from './routes/glossary.js'
|
|
56
57
|
import { handoffsRouter } from './routes/handoffs.js'
|
|
@@ -684,6 +685,7 @@ app.use('/api', promptDraftsRouter)
|
|
|
684
685
|
app.use('/api', cliDebugRouter)
|
|
685
686
|
app.use('/api', maintenanceRouter)
|
|
686
687
|
app.use('/api', ttsRouter)
|
|
688
|
+
recoverInterruptedNaming()
|
|
687
689
|
app.use('/api', voiceRouter)
|
|
688
690
|
app.use('/api', glossaryRouter)
|
|
689
691
|
app.use('/api', handoffsRouter)
|
|
@@ -462,7 +462,7 @@ export function findCosOperationsMeetingBySessionId(sessionId: string): {
|
|
|
462
462
|
const monthDir = join(meetingsBase, month)
|
|
463
463
|
let sidecars: string[]
|
|
464
464
|
try {
|
|
465
|
-
sidecars = readdirSync(monthDir).filter(f => f.endsWith('.g2-chunks.json')).sort().reverse()
|
|
465
|
+
sidecars = readdirSync(monthDir).filter(f => f.endsWith('.g2-chunks.json') && !/ \d+(\.[A-Za-z0-9-]+)*\.json$/.test(f)).sort().reverse()
|
|
466
466
|
} catch { continue }
|
|
467
467
|
|
|
468
468
|
for (const sidecarName of sidecars) {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// 3. Run sync_meetings.py --g2-only --g2-file with the private-app retry helper.
|
|
12
12
|
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
14
|
-
import { basename, dirname, join } from 'node:path'
|
|
14
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
15
15
|
import { durableAtomicWriteFileSync } from './atomic-fs.js'
|
|
16
16
|
import { resolveCosOperationsDir } from './cos-operations-meetings.js'
|
|
17
17
|
import { runG2EnrichmentWithRetry } from './g2-enrichment-runner.js'
|
|
@@ -23,6 +23,9 @@ const DOMAIN_REVIEW_MARKER = '<!-- g2-needs-domain-review -->'
|
|
|
23
23
|
const HQ_PENDING_MARKER = '<!-- g2-hq-state: pending -->'
|
|
24
24
|
const OPERATIONS_OWNED_SIDECAR_FIELDS = new Set([
|
|
25
25
|
'blended_into',
|
|
26
|
+
'claimed_parent',
|
|
27
|
+
'correctionRevision',
|
|
28
|
+
'labelsNewerThanGraph',
|
|
26
29
|
'dedupEvidence',
|
|
27
30
|
'enrichmentState',
|
|
28
31
|
'finalPath',
|
|
@@ -124,6 +127,12 @@ function parseSidecar(path: string): Record<string, unknown> | null {
|
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
129
|
|
|
130
|
+
export function g2Revision(value: unknown): number {
|
|
131
|
+
if (value === undefined || value === null) return 0
|
|
132
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) throw new Error('Invalid G2 revision')
|
|
133
|
+
return value
|
|
134
|
+
}
|
|
135
|
+
|
|
127
136
|
/** Merge server-owned capture truth while preserving operations-owned match state. */
|
|
128
137
|
export function mergeG2OperationsSidecar(
|
|
129
138
|
sourcePath: string,
|
|
@@ -139,8 +148,8 @@ export function mergeG2OperationsSidecar(
|
|
|
139
148
|
throw new Error(`Refusing G2 sidecar merge across sessions (${existingSession} != ${sourceSession})`)
|
|
140
149
|
}
|
|
141
150
|
|
|
142
|
-
const priorRevision =
|
|
143
|
-
const nextRevision = Math.max(
|
|
151
|
+
const priorRevision = g2Revision(existing.lifecycleRevision)
|
|
152
|
+
const nextRevision = Math.max(g2Revision(options.revision), g2Revision(source.lifecycleRevision))
|
|
144
153
|
if (Number.isFinite(priorRevision) && Number.isFinite(nextRevision) && nextRevision < priorRevision) {
|
|
145
154
|
throw new Error(`Refusing regressing G2 sidecar revision ${nextRevision} < ${priorRevision}`)
|
|
146
155
|
}
|
|
@@ -149,10 +158,19 @@ export function mergeG2OperationsSidecar(
|
|
|
149
158
|
for (const field of OPERATIONS_OWNED_SIDECAR_FIELDS) {
|
|
150
159
|
if (Object.prototype.hasOwnProperty.call(existing, field)) merged[field] = existing[field]
|
|
151
160
|
}
|
|
161
|
+
const correctionFields = ['chunks', 'chunkEntries', 'speakers', 'batchSegments', 'batchApplied', 'correctionRevision', 'labelsNewerThanGraph']
|
|
162
|
+
if (g2Revision(existing.correctionRevision) > g2Revision(source.correctionRevision)) {
|
|
163
|
+
for (const field of correctionFields) {
|
|
164
|
+
if (Object.prototype.hasOwnProperty.call(existing, field)) merged[field] = existing[field]
|
|
165
|
+
else delete merged[field]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
merged.correctionRevision = Math.max(g2Revision(existing.correctionRevision), g2Revision(source.correctionRevision))
|
|
169
|
+
merged.labelsNewerThanGraph = existing.labelsNewerThanGraph === true || source.labelsNewerThanGraph === true
|
|
152
170
|
merged.lifecycleRevision = Math.max(priorRevision || 0, nextRevision || 0)
|
|
153
|
-
const inferredFinalHqState =
|
|
171
|
+
const inferredFinalHqState = merged.batchApplied === true
|
|
154
172
|
? 'accepted'
|
|
155
|
-
:
|
|
173
|
+
: merged.batchQualityReport
|
|
156
174
|
? 'rejected'
|
|
157
175
|
: 'unavailable'
|
|
158
176
|
merged.hqState = options.hqState
|
|
@@ -184,7 +202,11 @@ export function stageRecordingIntoOperations(
|
|
|
184
202
|
const destDir = join(operationsDir, 'personal', 'meetings', month)
|
|
185
203
|
mkdirSync(destDir, { recursive: true })
|
|
186
204
|
const destPath = join(destDir, basename(localMeetingPath))
|
|
187
|
-
const
|
|
205
|
+
const sourceSidecar = parseSidecar(localMeetingPath.replace(/\.md$/, '.g2-chunks.json'))
|
|
206
|
+
const destinationSidecar = parseSidecar(destPath.replace(/\.md$/, '.g2-chunks.json'))
|
|
207
|
+
const keepMarkdown = g2Revision(destinationSidecar?.correctionRevision) > g2Revision(sourceSidecar?.correctionRevision)
|
|
208
|
+
if (keepMarkdown && !existsSync(destPath)) throw new Error('Corrected operations markdown missing; refusing stale import')
|
|
209
|
+
const patched = keepMarkdown ? readFileSync(destPath, 'utf8') : patchRecordingForG2Pipeline(readFileSync(localMeetingPath, 'utf8'), options)
|
|
188
210
|
|
|
189
211
|
const stem = basename(localMeetingPath, '.md')
|
|
190
212
|
const localDir = dirname(localMeetingPath)
|
|
@@ -200,6 +222,20 @@ export function stageRecordingIntoOperations(
|
|
|
200
222
|
return destPath
|
|
201
223
|
}
|
|
202
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Whether a G2 recording saved now can reach operations/ at all. The save path skips the pipeline when
|
|
227
|
+
* COS_SCRIPTS_DIR is unset, and runOperationsSync throws when the venv Python or sync_meetings.py is missing, so in
|
|
228
|
+
* each of those cases the recording stays only in the server's own store. GET /api/meetings reads this to decide
|
|
229
|
+
* whether a multi-folder library must list that store itself. Reads env live, like cosOpsPipelineConfigured in
|
|
230
|
+
* routes/meeting.ts.
|
|
231
|
+
*/
|
|
232
|
+
export function g2RecordingsReachOperations(): boolean {
|
|
233
|
+
const raw = process.env.COS_SCRIPTS_DIR?.trim()
|
|
234
|
+
if (!raw) return false
|
|
235
|
+
const scriptsDir = resolve(raw)
|
|
236
|
+
return existsSync(resolve(scriptsDir, 'venv/bin/python3')) && existsSync(join(scriptsDir, 'sync_meetings.py'))
|
|
237
|
+
}
|
|
238
|
+
|
|
203
239
|
async function runOperationsSync(localMeetingPath: string, claimOnly: boolean): Promise<void> {
|
|
204
240
|
if (!COS_SCRIPTS_DIR) {
|
|
205
241
|
console.log('[meeting/save] Standalone mode — skipping G2 sync pipeline')
|