@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 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.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.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 = 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)
@@ -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')