@gotcos/glasses-server 6.46.0 → 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,11 @@
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
+
1
9
  ## 6.46.0
2
10
 
3
11
  Name held voice samples and preview the meeting labels they will change. Paired Control build: 0.5.223.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.46.0",
3
+ "version": "6.46.1",
4
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": {
@@ -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'
@@ -222,6 +222,20 @@ export function stageRecordingIntoOperations(
222
222
  return destPath
223
223
  }
224
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
+
225
239
  async function runOperationsSync(localMeetingPath: string, claimOnly: boolean): Promise<void> {
226
240
  if (!COS_SCRIPTS_DIR) {
227
241
  console.log('[meeting/save] Standalone mode — skipping G2 sync pipeline')
@@ -17,6 +17,7 @@ import {
17
17
  import type { MeetingMeta } from '../lib/meeting-store.js'
18
18
  import { meetingListLimit } from '../lib/meeting-store.js'
19
19
  import { searchMeetingLibrary } from '../lib/meeting-library-search.js'
20
+ import { g2RecordingsReachOperations } from '../lib/g2-ops-handoff.js'
20
21
 
21
22
  const MONTH_QUERY = /^\d{4}-(0[1-9]|1[0-2])$/
22
23
  const DAY_QUERY = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/
@@ -87,6 +88,10 @@ function mergeDayCounts(
87
88
  .map(([date, count]) => ({ date, count }))
88
89
  }
89
90
 
91
+ function dayCountsOf(rows: MeetingMeta[]): Array<{ date: string; count: number }> {
92
+ return mergeDayCounts([rows.map(row => ({ date: row.date, count: 1 }))])
93
+ }
94
+
90
95
  function uniqueSortedMonths(groups: string[][]): string[] {
91
96
  return [...new Set(groups.flat())].sort().reverse()
92
97
  }
@@ -161,17 +166,53 @@ export function createMeetingsRouter(store: MeetingStore = getMeetingStore()): R
161
166
  }
162
167
 
163
168
  if (library.layout === 'multi_domain') {
164
- const meetings = listCosOperationsMeetings({
165
- limit,
166
- domain,
167
- month: filters.month,
168
- day: filters.day,
169
- })
169
+ if (g2RecordingsReachOperations()) {
170
+ const meetings = listCosOperationsMeetings({
171
+ limit,
172
+ domain,
173
+ month: filters.month,
174
+ day: filters.day,
175
+ })
176
+ res.json({
177
+ meetings,
178
+ months: listCosOperationsMeetingMonths(domain),
179
+ days: filters.month ? listCosOperationsMeetingDays(filters.month, domain) : [],
180
+ source: 'cos_operations',
181
+ layout: 'multi_domain',
182
+ root: library.root,
183
+ rootFingerprint: library.rootFingerprint,
184
+ meetingCount: meetings.length,
185
+ warnings: library.warnings,
186
+ })
187
+ return
188
+ }
189
+
190
+ // No COS pipeline can file this Mac's G2 recordings into the operations folder (a Starter Kit library
191
+ // without COS_SCRIPTS_DIR, 2026-09-14), so they live only in this server's store. List them beside the
192
+ // operations rows. Operations rows go first, so a copy that did reach operations wins by sessionId, and
193
+ // the day counts skip the store row it covers. Day counts always describe the whole month.
194
+ const operations = listCosOperationsMeetings(listOptions)
195
+ const standalone = store.list(listOptions).map(withStandaloneIdentity)
196
+ const meetings = mergeMeetingSources([operations, standalone], limit, sourceLimit)
197
+ let days: Array<{ date: string; count: number }> = []
198
+ if (filters.month) {
199
+ const monthOperations = filters.day
200
+ ? listCosOperationsMeetings({ limit: sourceLimit, domain, month: filters.month })
201
+ : operations
202
+ const monthStore = filters.day
203
+ ? store.list({ limit: sourceLimit, domain, month: filters.month })
204
+ : standalone
205
+ const covered = new Set(monthOperations.flatMap(row => (row.sessionId ? [row.sessionId] : [])))
206
+ days = mergeDayCounts([
207
+ listCosOperationsMeetingDays(filters.month, domain),
208
+ dayCountsOf(monthStore.filter(row => !row.sessionId || !covered.has(row.sessionId))),
209
+ ])
210
+ }
170
211
  res.json({
171
212
  meetings,
172
- months: listCosOperationsMeetingMonths(domain),
173
- days: filters.month ? listCosOperationsMeetingDays(filters.month, domain) : [],
174
- source: 'cos_operations',
213
+ months: uniqueSortedMonths([listCosOperationsMeetingMonths(domain), store.listMonths()]),
214
+ days,
215
+ source: standalone.length > 0 ? 'mixed_library' : 'cos_operations',
175
216
  layout: 'multi_domain',
176
217
  root: library.root,
177
218
  rootFingerprint: library.rootFingerprint,