@gotcos/glasses-server 6.24.0 → 6.24.2

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,46 @@
1
+ ## 6.24.2
2
+
3
+ The empty-recording restart lock, split out of 6.24.1.
4
+
5
+ **Correction to 6.24.1's own entry.** These bullets were written under 6.24.1 while it
6
+ was still unpublished, but it had already gone to the registry by then, so that heading
7
+ described a published artifact that does not contain this code. Verified by downloading
8
+ the published tarball: 6.24.1 carries the `project` field and has no
9
+ `EMPTY_SESSION_STALE_MS`. Moved here rather than left standing.
10
+
11
+ - **A 6-second aborted recording no longer locks the restart for 30 minutes.**
12
+ `getTranscriptionSessionLiveness` gated purely on elapsed time. The 30-minute grace
13
+ exists to protect a real recording that has gone briefly quiet — a backgrounded phone
14
+ buffering to IndexedDB — but a session with NO canonical text has nothing to protect.
15
+ Observed 2026-08-10: `meeting_1786393815060_tp693w` started, received one 5.6s chunk
16
+ that transcribed to empty, stopped 6.2 seconds later, and then blocked Update Server.
17
+ A session idle past `EMPTY_SESSION_STALE_MS` (2 minutes) with zero canonical chunks
18
+ now stops blocking a restart.
19
+ - **Why that cannot reap a live recording.** `lastActivityAt` is bumped on chunk
20
+ ARRIVAL, before any text filtering, so a live recording in a silent room keeps
21
+ arriving every ~10s and never goes idle at all; its silent chunks land in
22
+ `emptyCompletions`, not `chunks`. Gating on emptiness ALONE would kill exactly that
23
+ session, which is why the rule requires emptiness AND idleness. Counted on canonical
24
+ text rather than array length, because the array is sparse and a silent chunk carries
25
+ no text — the precise shape an aborted recording leaves behind.
26
+
27
+ ## 6.24.1
28
+
29
+ Surfaces `project` on session rows, so the list can group the way Claude Code's own
30
+ sidebar does.
31
+
32
+ - The indexer was scanning ONE project directory. `PROJECTS_DIR` was hardcoded to
33
+ MU-Chief-Staff, so 17 of 18 project dirs — `cos-glasses-app`, `cos-glasses-server`,
34
+ the 119 projects, the COS examples — were never indexed at all. That is why the
35
+ Sessions list could never match the sidebar: entire projects were absent, not just
36
+ mislabelled. Fixed in session_indexer.py (COS repo); 1,212 sessions now carry a
37
+ project where 1,000 were visible before.
38
+ - `project` comes from the `cwd` on the session records, not from decoding the
39
+ directory name. Claude Code replaces path separators with dashes and a real dash is
40
+ indistinguishable from a separator afterwards, so
41
+ `-Users-ukaoma-Documents-GitHub-Ukaoma-Chief-Of-Staff-MU-Chief-Staff` decoded to
42
+ "Ukaoma-Chief-Of-Staff-MU-Chief-Staff" where the sidebar says "MU-Chief-Staff".
43
+
1
44
  ## 6.24.0
2
45
 
3
46
  The Sessions tab stops 404ing, and a read-only presence view of Claude Code
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.24.0",
3
+ "version": "6.24.2",
4
4
  "description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, or Cursor Agent CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,6 +54,8 @@ export interface SessionEntry {
54
54
  display_label: string
55
55
  /** True when the harness opened this session rather than a person. */
56
56
  machine_spawned: boolean
57
+ /** Project name, matching the group heading in Claude Code's own sidebar. */
58
+ project: string
57
59
  glasses_session_id: string
58
60
  slug: string
59
61
  created: string
@@ -210,6 +212,7 @@ sessionIndexRouter.get('/session-index', async (req, res) => {
210
212
  display_label: str(entry.display_label) || str(entry.custom_title)
211
213
  || str(entry.first_prompt) || str(entry.slug) || entry.session_id.slice(0, 8),
212
214
  machine_spawned: entry.machine_spawned === true,
215
+ project: str(entry.project),
213
216
  domain,
214
217
  device_id: deviceId,
215
218
  })
@@ -260,6 +263,7 @@ sessionIndexRouter.get('/session-index/:session_id', async (req, res) => {
260
263
  display_label: str(entry.display_label) || str(entry.custom_title)
261
264
  || str(entry.first_prompt) || str(entry.slug) || entry.session_id.slice(0, 8),
262
265
  machine_spawned: entry.machine_spawned === true,
266
+ project: str(entry.project),
263
267
  tools_used: (entry.tools_used && typeof entry.tools_used === 'object') ? entry.tools_used : {},
264
268
  files_touched: Array.isArray(entry.files_touched) ? entry.files_touched : [],
265
269
  domain: str(entry.domain, 'unknown') || 'unknown',
@@ -387,6 +387,26 @@ export function getActiveTranscriptionSessionCount(): number {
387
387
  */
388
388
  export const RECORDING_SESSION_STALE_MS = STRANDED_STALE_MS
389
389
 
390
+ /**
391
+ * How long an EMPTY session may hold the restart lock.
392
+ *
393
+ * The 30-minute grace above protects a real recording that has gone briefly quiet — a
394
+ * backgrounded phone buffering to IndexedDB, a network drop, a long pause. A session
395
+ * that has produced NO canonical text has nothing to protect, so it must not lock
396
+ * Install, Repair, Restart and Update Server for half an hour.
397
+ *
398
+ * Observed 2026-08-10: meeting_1786393815060_tp693w started, took ONE 5.6s chunk that
399
+ * transcribed to empty, stopped 6.2s later, then blocked every restart for 30 minutes.
400
+ *
401
+ * WHY THIS CANNOT REAP A LIVE RECORDING. `lastActivityAt` is bumped on chunk ARRIVAL
402
+ * in processStreamChunk, before any text filtering, so a live recording in a silent
403
+ * room keeps arriving every ~10s and never goes idle — its silent chunks land in
404
+ * `emptyCompletions`, not `chunks`. Gating on emptiness ALONE would kill exactly that
405
+ * session; gating on emptiness AND idleness cannot. Two minutes is comfortably more
406
+ * than one chunk interval, so a recording that has just started is safe.
407
+ */
408
+ export const EMPTY_SESSION_STALE_MS = 2 * 60_000
409
+
390
410
  export interface TranscriptionSessionLiveness {
391
411
  /** Sessions still plausibly recording. These block a restart. */
392
412
  live: number
@@ -479,8 +499,12 @@ export function getTranscriptionSessionLiveness(now = Date.now()): Transcription
479
499
  const staleSessions: TranscriptionSessionLiveness['staleSessions'] = []
480
500
  for (const [sessionId, session] of sessions) {
481
501
  const silentForMs = now - session.lastActivityAt
482
- if (silentForMs >= RECORDING_SESSION_STALE_MS) {
483
- staleSessions.push({ sessionId, silentForMs, chunks: session.chunks.length })
502
+ // Canonical text, not array length: the array is sparse and silent chunks carry no
503
+ // text, which is exactly the shape an aborted recording leaves behind.
504
+ const canonicalChunks = session.chunks.filter(chunk => chunk && chunk.text).length
505
+ const emptyAndIdle = canonicalChunks === 0 && silentForMs >= EMPTY_SESSION_STALE_MS
506
+ if (silentForMs >= RECORDING_SESSION_STALE_MS || emptyAndIdle) {
507
+ staleSessions.push({ sessionId, silentForMs, chunks: canonicalChunks })
484
508
  } else {
485
509
  live++
486
510
  }