@gotcos/glasses-server 6.45.5 → 6.46.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,15 @@
1
+ ## 6.46.0
2
+
3
+ Name held voice samples and preview the meeting labels they will change. Paired Control build: 0.5.223.
4
+
5
+ - 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.
6
+ - 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.
7
+ - 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.
8
+ - 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.
9
+ - 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.
10
+ - 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.
11
+ - 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.
12
+
1
13
  ## 6.45.5
2
14
 
3
15
  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.45.5",
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",
3
+ "version": "6.46.0",
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) {
@@ -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 = Number(existing.lifecycleRevision ?? 0)
143
- const nextRevision = Math.max(Number(options.revision ?? 0), Number(source.lifecycleRevision ?? 0))
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 = source.batchApplied === true
171
+ const inferredFinalHqState = merged.batchApplied === true
154
172
  ? 'accepted'
155
- : source.batchQualityReport
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 patched = patchRecordingForG2Pipeline(readFileSync(localMeetingPath, 'utf8'), options)
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)