@gotcos/glasses-server 6.24.2 → 6.24.3

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,38 @@
1
+ ## 6.24.3
2
+
3
+ Auto-recovery of quarantined audio has never run in production. Miles saw the symptom
4
+ for three turns: "1 recoverable" that opening the phone app could not clear.
5
+
6
+ - **My call sat inside a bare `catch {}`.** `autoRecoverOneQuarantinedCapture()` was one
7
+ line after `purgeExpiredQuarantine()` inside the orphan-audio sweep's
8
+ `try { ... } catch {}`, so any throw in that sweep meant auto-recovery silently never
9
+ executed — on 6.23.1, 6.24.0, 6.24.1 and 6.24.2. Zero `[quarantine]` lines in a 48 MB
10
+ log across every one of those releases. It now has its own try, because recovering
11
+ quarantined audio has nothing to do with sweeping orphaned session-audio dirs and must
12
+ not depend on that succeeding.
13
+ - **That bare catch is why it took three turns to find.** Three minutes of watching a
14
+ live server produced no recovery, no log, and nothing to reason about, because the
15
+ error was discarded. Both catches now report. I chased three wrong causes first — a
16
+ closed admissions gate (`admissionsOpen` was `true`), a stale npm cache (real, but a
17
+ different bug), and a broken picker (it selects the item correctly against live data).
18
+ - **A one-chunk capture is no longer advertised as recoverable.**
19
+ `meeting_1786393815060_tp693w` held ONE 5.6-second chunk that transcribed to silence.
20
+ Recovering it would have produced an empty meeting titled "Recovered capture (audio
21
+ only)"; advertising it produced a badge with instructions that cannot work, since a
22
+ server-side quarantine has no deferred phone save to land. `MIN_RECOVERABLE_CHUNKS`
23
+ is 2, and `isWorthRecovering` is the SINGLE definition used by the picker AND by both
24
+ warning counts — two definitions would let the badge claim something the sweeper has
25
+ already decided to skip.
26
+ - No audio is deleted by any of this. Quarantine retention still owns expiry.
27
+
28
+ Coverage: 8 mutations, all caught. The placement mutations were first measured against a
29
+ RED baseline and re-run once green, because a mutation against a failing tree proves
30
+ nothing. Three of those red iterations were my own test windowing, never the fix: a
31
+ file-wide ban that hit a second legitimate bare catch, a fixed-width slice that ran past
32
+ the fix, and an `indexOf` that matched the function definition instead of the call site.
33
+
34
+ Full suite 1505 serially, tsc clean, gate after the bump.
35
+
1
36
  ## 6.24.2
2
37
 
3
38
  The empty-recording restart lock, split out of 6.24.1.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.24.2",
3
+ "version": "6.24.3",
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": {
@@ -27,6 +27,34 @@
27
27
 
28
28
  import type { UnsavedCapture } from './unsaved-audio-quarantine.js'
29
29
 
30
+ /**
31
+ * Chunks below which a capture is not worth turning into a meeting.
32
+ *
33
+ * A chunk covers roughly 5 to 10 seconds, so one chunk is a recording that started and
34
+ * stopped almost immediately. Observed 2026-08-10: `meeting_1786393815060_tp693w` held
35
+ * ONE 5.6-second chunk that transcribed to silence, and the panel advertised it as
36
+ * "1 recoverable" with instructions to open the phone app — which cannot clear a
37
+ * server-side quarantine, so the badge simply persisted.
38
+ *
39
+ * Recovering it would run a full batch transcription and produce an empty meeting
40
+ * titled "Recovered capture (audio only)". That is noise, not rescue. The audio is NOT
41
+ * deleted here — quarantine retention still owns that decision and expires it on its
42
+ * own clock. This only decides what is worth acting on and worth warning about.
43
+ */
44
+ export const MIN_RECOVERABLE_CHUNKS = 2
45
+
46
+ /**
47
+ * Is this capture substantial enough to act on?
48
+ *
49
+ * One definition, used by the auto-recover picker AND by the counts that drive the
50
+ * "unsaved captures" warning, so the badge cannot claim something is recoverable that
51
+ * the sweeper has already decided to leave alone.
52
+ */
53
+ export function isWorthRecovering(item: { recovered: boolean; chunkFiles: number }): boolean {
54
+ if (item.recovered) return false
55
+ return item.chunkFiles >= MIN_RECOVERABLE_CHUNKS
56
+ }
57
+
30
58
  /** Attempts per capture before the sweep stops trying on its own. */
31
59
  export const MAX_AUTO_RECOVER_ATTEMPTS = 3
32
60
 
@@ -58,10 +86,8 @@ export function pickQuarantineToRecover(
58
86
  state: AutoRecoverState,
59
87
  ): UnsavedCapture | null {
60
88
  const eligible = items.filter(item => {
61
- // Already a meeting. Recovering again would duplicate it.
62
- if (item.recovered) return false
63
- // Nothing to transcribe: a chunk-less dir is residue, not evidence.
64
- if (item.chunkFiles <= 0) return false
89
+ // Already a meeting, chunk-less residue, or too small to be a meeting at all.
90
+ if (!isWorthRecovering(item)) return false
65
91
  // Another recovery owns this one.
66
92
  if (state.inFlight.has(item.sessionId)) return false
67
93
  return (state.attempts.get(item.sessionId) ?? 0) < MAX_AUTO_RECOVER_ATTEMPTS
@@ -1,4 +1,5 @@
1
1
  import { Router } from 'express'
2
+ import { isWorthRecovering } from '../lib/quarantine-auto-recover.js'
2
3
  import { statSync } from 'node:fs'
3
4
  import { resolve } from 'node:path'
4
5
  import { COS_SCRIPTS_DIR, COS_MODE } from '../lib/python-bridge.js'
@@ -227,7 +228,7 @@ healthRouter.get('/health', async (_req, res) => {
227
228
  // plus the recover action live on the authenticated /api/meeting/orphans.
228
229
  const unsavedList = listUnsavedCaptures()
229
230
  const unsaved_captures = {
230
- count: unsavedList.filter(item => !item.recovered).length,
231
+ count: unsavedList.filter(isWorthRecovering).length,
231
232
  items: unsavedList.slice(0, 10).map(item => ({
232
233
  sessionId: item.sessionId,
233
234
  ageHours: item.ageHours,
@@ -3,6 +3,7 @@
3
3
  // are durable before the session is closed; batch improvement runs afterward.
4
4
 
5
5
  import { existsSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync } from 'node:fs'
6
+ import { isWorthRecovering } from '../lib/quarantine-auto-recover.js'
6
7
  import { resolve } from 'node:path'
7
8
  import { Router } from 'express'
8
9
  import { emitDisplay } from '../lib/display-bus.js'
@@ -1672,7 +1673,7 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
1672
1673
  // route answered count: 0.
1673
1674
  const stranded = getStrandedCaptures()
1674
1675
  res.json({
1675
- count: items.filter(item => !item.recovered).length,
1676
+ count: items.filter(isWorthRecovering).length,
1676
1677
  strandedCount: stranded.length,
1677
1678
  stranded,
1678
1679
  recovering: [...recoveringOrphans],
@@ -1015,8 +1015,22 @@ setInterval(() => {
1015
1015
  }
1016
1016
  }
1017
1017
  purgeExpiredQuarantine()
1018
+ } catch (error) {
1019
+ // Was a bare `catch {}`. That is what made the auto-recover failure below
1020
+ // undiagnosable: three minutes of watching a live server produced no recovery, no
1021
+ // log, and nothing to reason about, because any throw in this block vanished.
1022
+ console.error(`[cleanup] Orphan-audio sweep failed: ${error instanceof Error ? error.message : error}`)
1023
+ }
1024
+ // DELIBERATELY ITS OWN TRY. This used to sit inside the block above, one line after
1025
+ // purgeExpiredQuarantine, so a throw anywhere in that sweep meant auto-recovery
1026
+ // silently never ran — which is exactly what happened in production on 6.23.1
1027
+ // through 6.24.2. Recovering quarantined audio has nothing to do with sweeping
1028
+ // orphaned session-audio dirs and must not depend on it succeeding.
1029
+ try {
1018
1030
  autoRecoverOneQuarantinedCapture()
1019
- } catch {}
1031
+ } catch (error) {
1032
+ console.error(`[quarantine] Auto-recover pass failed: ${error instanceof Error ? error.message : error}`)
1033
+ }
1020
1034
  // Purge stale pending-batch dirs. HQ large-v3 on long meetings + Control
1021
1035
  // restart can exceed 2h (2026-07-27: two sessions purged before batch).
1022
1036
  // Use 12h, and never purge while a meeting_batch_finalization lease is held.