@gotcos/glasses-server 6.21.1 → 6.21.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,13 @@
1
+ ## 6.21.2
2
+
3
+ - Cache Python, Claude, Codex, and Cursor process probes for 30 seconds so the
4
+ public health endpoint remains cheap under frequent phone diagnostics. Stale
5
+ static versions are served while one background refresh runs; live recovery,
6
+ transcription, TTS, maintenance, and request fields remain fresh.
7
+ - Parse Cursor's actual `CLI Version` line instead of reporting the About
8
+ heading. Authenticated model discovery continues to be the authoritative
9
+ Cursor readiness signal.
10
+
1
11
  ## 6.21.1
2
12
 
3
13
  - Make the transactional Claude readiness proof explicitly use Haiku instead
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.21.1",
3
+ "version": "6.21.2",
4
4
  "description": "COS Glasses — 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": {
@@ -0,0 +1,177 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { PYTHON_BIN } from './python-bridge.js'
3
+ import {
4
+ getCursorModelCatalog,
5
+ isCursorProviderReady,
6
+ resolveAgentBinary,
7
+ } from './cursor-model-catalog.js'
8
+
9
+ const DEFAULT_CACHE_TTL_MS = 30_000
10
+ const PROBE_TIMEOUT_MS = 5_000
11
+
12
+ export interface HealthStaticProbeSnapshot {
13
+ python: string
14
+ claude: string
15
+ codex: string
16
+ cursor: string
17
+ claudeAvailable: boolean
18
+ codexAvailable: boolean
19
+ cursorAvailable: boolean
20
+ }
21
+
22
+ interface CachedProbe<T> {
23
+ value: T
24
+ refreshedAt: number
25
+ }
26
+
27
+ /**
28
+ * Small stale-while-revalidate cache used for process-level health probes.
29
+ * Runtime fields (recordings, recovery leases, Whisper, TTS, request counts)
30
+ * remain fresh on every /api/health request.
31
+ */
32
+ export function createStaticProbeCache<T>(
33
+ load: () => Promise<T>,
34
+ ttlMs: () => number,
35
+ now: () => number = Date.now,
36
+ ): { get: () => Promise<T>; reset: () => void } {
37
+ let cached: CachedProbe<T> | null = null
38
+ let inFlight: Promise<T> | null = null
39
+
40
+ const refresh = (): Promise<T> => {
41
+ if (inFlight) return inFlight
42
+ inFlight = load().then((value) => {
43
+ cached = { value, refreshedAt: now() }
44
+ return value
45
+ }).finally(() => {
46
+ inFlight = null
47
+ })
48
+ return inFlight
49
+ }
50
+
51
+ return {
52
+ get: async () => {
53
+ if (!cached) return refresh()
54
+ if (now() - cached.refreshedAt < ttlMs()) return cached.value
55
+ // A stale static version string is safer than making liveness wait for
56
+ // four child processes. Refresh it for the next request in background.
57
+ void refresh().catch(() => {})
58
+ return cached.value
59
+ },
60
+ reset: () => {
61
+ cached = null
62
+ inFlight = null
63
+ },
64
+ }
65
+ }
66
+
67
+ function cacheTtlMs(): number {
68
+ const raw = Number(process.env.COS_HEALTH_STATIC_PROBE_TTL_MS ?? DEFAULT_CACHE_TTL_MS)
69
+ return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : DEFAULT_CACHE_TTL_MS
70
+ }
71
+
72
+ function execute(file: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
73
+ return new Promise((resolve, reject) => {
74
+ execFile(file, args, {
75
+ encoding: 'utf8',
76
+ timeout: PROBE_TIMEOUT_MS,
77
+ maxBuffer: 256 * 1024,
78
+ killSignal: 'SIGKILL',
79
+ }, (error, stdout, stderr) => {
80
+ if (error) return reject(error)
81
+ resolve({ stdout: String(stdout), stderr: String(stderr) })
82
+ })
83
+ })
84
+ }
85
+
86
+ export function parseCursorAboutVersion(output: string): string | undefined {
87
+ for (const rawLine of output.split(/\r?\n/)) {
88
+ const line = rawLine.trim()
89
+ const match = /^CLI Version\s*:?\s*(\d{4}(?:\.\d+){1,3}(?:[-+][a-z0-9.-]+)?)$/i.exec(line)
90
+ if (match) return match[1]
91
+ }
92
+ return undefined
93
+ }
94
+
95
+ function firstNonemptyLine(output: string): string | undefined {
96
+ return output.split(/\r?\n/).map(line => line.trim()).find(Boolean)
97
+ }
98
+
99
+ async function probePython(): Promise<string> {
100
+ if (!PYTHON_BIN) return 'standalone'
101
+ try {
102
+ return firstNonemptyLine((await execute(PYTHON_BIN, ['--version'])).stdout) ?? 'available'
103
+ } catch {
104
+ return 'error'
105
+ }
106
+ }
107
+
108
+ async function probeClaude(): Promise<{ value: string; available: boolean }> {
109
+ try {
110
+ const result = await execute('claude', ['--version'])
111
+ return { value: firstNonemptyLine(result.stdout) ?? 'available', available: true }
112
+ } catch {
113
+ return { value: 'error', available: false }
114
+ }
115
+ }
116
+
117
+ async function probeCodex(): Promise<{ value: string; available: boolean }> {
118
+ try {
119
+ const result = await execute('codex', ['--version'])
120
+ const combined = `${result.stdout}\n${result.stderr}`.trim()
121
+ const versionLine = combined.split(/\r?\n/).map(line => line.trim())
122
+ .find(line => /^codex(?:-cli)?\s+/i.test(line))
123
+ return { value: versionLine ?? firstNonemptyLine(combined) ?? 'available', available: true }
124
+ } catch {
125
+ return { value: 'error', available: false }
126
+ }
127
+ }
128
+
129
+ async function probeCursor(): Promise<{ value: string; available: boolean }> {
130
+ const agentBinary = resolveAgentBinary()
131
+ if (!agentBinary) return { value: 'error', available: false }
132
+ try {
133
+ const result = await execute(agentBinary, ['about'])
134
+ const combined = `${result.stdout}\n${result.stderr}`.trim()
135
+ const version = parseCursorAboutVersion(combined)
136
+ // Catalog discovery is authenticated downstream truth and can take up to
137
+ // seven seconds. Warm it without putting that latency on public health.
138
+ void getCursorModelCatalog().catch(() => {})
139
+ const available = isCursorProviderReady()
140
+ const value = version ?? 'available'
141
+ return {
142
+ value: available ? value : `${value} (models unresolved)`,
143
+ available,
144
+ }
145
+ } catch {
146
+ return { value: 'error', available: false }
147
+ }
148
+ }
149
+
150
+ async function loadStaticHealthProbes(): Promise<HealthStaticProbeSnapshot> {
151
+ const [python, claude, codex, cursor] = await Promise.all([
152
+ probePython(),
153
+ probeClaude(),
154
+ probeCodex(),
155
+ probeCursor(),
156
+ ])
157
+ return {
158
+ python,
159
+ claude: claude.value,
160
+ codex: codex.value,
161
+ cursor: cursor.value,
162
+ claudeAvailable: claude.available,
163
+ codexAvailable: codex.available,
164
+ cursorAvailable: cursor.available,
165
+ }
166
+ }
167
+
168
+ const staticProbeCache = createStaticProbeCache(loadStaticHealthProbes, cacheTtlMs)
169
+
170
+ export function getHealthStaticProbes(): Promise<HealthStaticProbeSnapshot> {
171
+ return staticProbeCache.get()
172
+ }
173
+
174
+ /** Test hook. */
175
+ export function _resetHealthStaticProbeCache(): void {
176
+ staticProbeCache.reset()
177
+ }
@@ -1,8 +1,7 @@
1
1
  import { Router } from 'express'
2
- import { execFile } from 'node:child_process'
3
2
  import { statSync } from 'node:fs'
4
3
  import { resolve } from 'node:path'
5
- import { COS_SCRIPTS_DIR, COS_MODE, PYTHON_BIN } from '../lib/python-bridge.js'
4
+ import { COS_SCRIPTS_DIR, COS_MODE } from '../lib/python-bridge.js'
6
5
  import { serverMetrics } from '../lib/server-metrics.js'
7
6
  import { getServerInstanceId } from '../lib/server-instance-id.js'
8
7
  import { localFirstMeetingsCapability } from '../lib/local-first-meetings-contract.js'
@@ -24,7 +23,6 @@ import {
24
23
  getCursorModelCatalog,
25
24
  getCursorModelCatalogSnapshot,
26
25
  isCursorProviderReady,
27
- resolveAgentBinary,
28
26
  } from '../lib/cursor-model-catalog.js'
29
27
  import { isMediaProcessingReady } from '../lib/image-safety.js'
30
28
  import { G2_LENS_VARIANT_CAPABILITY } from '../lib/media-store.js'
@@ -41,6 +39,7 @@ import { getMeetingSyncSnapshot } from '../lib/meeting-batch-progress.js'
41
39
  import { listUnsavedCaptures } from '../lib/unsaved-audio-quarantine.js'
42
40
  import { getWhisperPreviewCapability } from '../lib/whisper-preview.js'
43
41
  import { getTranscriptionProfileStatus } from '../lib/profile.js'
42
+ import { getHealthStaticProbes } from '../lib/health-static-probes.js'
44
43
 
45
44
  export const healthRouter = Router()
46
45
 
@@ -74,101 +73,26 @@ function durableQueryJobStatus() {
74
73
  }
75
74
 
76
75
  healthRouter.get('/health', async (_req, res) => {
77
- await refreshLocalTtsHealth()
76
+ const [, staticProbes] = await Promise.all([
77
+ refreshLocalTtsHealth(),
78
+ getHealthStaticProbes(),
79
+ ])
78
80
  const checks: Record<string, string | number | boolean> = {
79
81
  status: 'ok',
80
82
  mode: COS_MODE ? 'cos' : 'standalone',
81
83
  server: 'ok',
82
- python: 'unknown',
83
- claude: 'unknown',
84
- codex: 'unknown',
85
- cursor: 'unknown',
84
+ python: staticProbes.python,
85
+ claude: staticProbes.claude,
86
+ codex: staticProbes.codex,
87
+ cursor: staticProbes.cursor,
86
88
  uptime_seconds: Math.floor((Date.now() - serverMetrics.startedAt) / 1000),
87
89
  request_count: serverMetrics.requestCount,
88
90
  }
89
91
 
90
92
  // Feature detection flags
91
- let claudeAvailable = false
92
- let codexAvailable = false
93
- let cursorAvailable = false
94
-
95
- // Check Python venv (COS mode only)
96
- if (PYTHON_BIN) {
97
- try {
98
- await new Promise<void>((resolve, reject) => {
99
- execFile(PYTHON_BIN!, ['--version'], { timeout: 5000 }, (err, stdout) => {
100
- if (err) return reject(err)
101
- checks.python = stdout.trim()
102
- resolve()
103
- })
104
- })
105
- } catch {
106
- checks.python = 'error'
107
- }
108
- } else {
109
- checks.python = 'standalone'
110
- }
111
-
112
- // Check claude CLI
113
- try {
114
- await new Promise<void>((resolve, reject) => {
115
- execFile('claude', ['--version'], { timeout: 5000 }, (err, stdout) => {
116
- if (err) return reject(err)
117
- checks.claude = stdout.trim()
118
- claudeAvailable = true
119
- resolve()
120
- })
121
- })
122
- } catch {
123
- checks.claude = 'error'
124
- }
125
-
126
- // Check Codex CLI. The desktop CLI can print benign PATH warnings to stderr,
127
- // so version extraction uses stdout + stderr and looks for the codex-cli line.
128
- try {
129
- await new Promise<void>((resolve, reject) => {
130
- execFile('codex', ['--version'], { timeout: 5000 }, (err, stdout, stderr) => {
131
- if (err) return reject(err)
132
- const combined = `${stdout}\n${stderr}`.trim()
133
- const versionLine = combined.split('\n').map(line => line.trim()).find(line => /^codex(?:-cli)?\s+/i.test(line))
134
- checks.codex = versionLine ?? combined.split('\n')[0] ?? 'available'
135
- codexAvailable = true
136
- resolve()
137
- })
138
- })
139
- } catch {
140
- checks.codex = 'error'
141
- }
142
-
143
- // Cursor Agent CLI — probe via `agent about` only (≤5s). Never use
144
- // `agent status` (can hang on login UX while logged out).
145
- try {
146
- const agentBinary = resolveAgentBinary()
147
- if (!agentBinary) {
148
- checks.cursor = 'error'
149
- } else {
150
- await new Promise<void>((resolveCheck, reject) => {
151
- execFile(agentBinary, ['about'], { timeout: 5000 }, (err, stdout, stderr) => {
152
- if (err) return reject(err)
153
- const combined = `${stdout}\n${stderr}`.trim()
154
- const versionLine = combined.split('\n').map(line => line.trim()).find(line =>
155
- /CLI Version|cursor|agent/i.test(line),
156
- )
157
- checks.cursor = versionLine ?? combined.split('\n')[0] ?? 'available'
158
- resolveCheck()
159
- })
160
- })
161
- await getCursorModelCatalog()
162
- cursorAvailable = isCursorProviderReady()
163
- if (!cursorAvailable) {
164
- checks.cursor = typeof checks.cursor === 'string' && checks.cursor !== 'error'
165
- ? `${checks.cursor} (models unresolved)`
166
- : 'error'
167
- }
168
- }
169
- } catch {
170
- checks.cursor = 'error'
171
- }
93
+ const claudeAvailable = staticProbes.claudeAvailable
94
+ const codexAvailable = staticProbes.codexAvailable
95
+ const cursorAvailable = staticProbes.cursorAvailable
172
96
 
173
97
  // Check session cache freshness (COS mode only)
174
98
  if (COS_SCRIPTS_DIR) {