@gotcos/glasses-server 6.21.23 → 6.21.24
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 +27 -0
- package/package.json +1 -1
- package/server/routes/maintenance.ts +16 -3
- package/server/routes/transcribe-stream.ts +70 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
## 6.21.24
|
|
2
|
+
|
|
3
|
+
- A leaked recording session can no longer block every restart. The maintenance
|
|
4
|
+
drain gate counted `sessions.size`, so a session whose phone dropped
|
|
5
|
+
mid-recording without sending a close pinned that count at 1 indefinitely and
|
|
6
|
+
Install, Repair, Restart and Update Server each drained it, timed out after
|
|
7
|
+
60s, and failed — with no user-visible reason. Hit twice on 2026-08-06
|
|
8
|
+
(6.21.20 and 6.21.22); the second phantom had been silent 54 minutes and the
|
|
9
|
+
only way through was to finalize it as a real meeting, which it was not.
|
|
10
|
+
|
|
11
|
+
The gate now counts only sessions active within the last 30 minutes. A stale
|
|
12
|
+
one is reported in status as `staleTranscriptionSessions` with its session id,
|
|
13
|
+
silent duration and chunk count, so a blocked operator can see the cause —
|
|
14
|
+
and `/api/meeting/orphans` reporting 0 can no longer coexist silently with a
|
|
15
|
+
held lock, since the two read different stores.
|
|
16
|
+
|
|
17
|
+
**Nothing is reaped.** The session, its chunks and its recoverability are
|
|
18
|
+
untouched; only its claim on the restart gate expires. The 30-minute window
|
|
19
|
+
sits far above the reasons a live recording legitimately goes quiet (a
|
|
20
|
+
backgrounded phone buffering to IndexedDB, a network drop) and far below the
|
|
21
|
+
4h durable-chunk retention.
|
|
22
|
+
|
|
23
|
+
Note for anyone reading the health payload: `oldestWorkStartedAt: null` does
|
|
24
|
+
NOT indicate a leak. `recording_session` reaches the gate through
|
|
25
|
+
`extraActiveByKind`, which never contributes a timestamp, so every recording
|
|
26
|
+
session reports null — healthy or not. Only `lastActivityAt` separates them.
|
|
27
|
+
|
|
1
28
|
## 6.21.23
|
|
2
29
|
|
|
3
30
|
- `GET /meeting/:sessionId/embeddings` — why each chunk was labelled the way it
|
package/package.json
CHANGED
|
@@ -15,7 +15,7 @@ import { getQueryJobRuntimeHealth } from '../lib/query-job-runtime.js'
|
|
|
15
15
|
import { serverMetrics } from '../lib/server-metrics.js'
|
|
16
16
|
import { getServerInstanceId } from '../lib/server-instance-id.js'
|
|
17
17
|
import { getWhisperHealth } from '../lib/whisper-local.js'
|
|
18
|
-
import { getActiveTranscriptionSessionCount } from './transcribe-stream.js'
|
|
18
|
+
import { getActiveTranscriptionSessionCount, getTranscriptionSessionLiveness } from './transcribe-stream.js'
|
|
19
19
|
|
|
20
20
|
export const maintenanceRouter = Router()
|
|
21
21
|
|
|
@@ -71,15 +71,21 @@ function operationCredentials(req: Request): MaintenanceOperationCredentials {
|
|
|
71
71
|
function statusSnapshot(credentials: MaintenanceOperationCredentials = {}) {
|
|
72
72
|
const jobs = getQueryJobRuntimeHealth()
|
|
73
73
|
const activeTranscriptionSessions = getActiveTranscriptionSessionCount()
|
|
74
|
+
// Only sessions still plausibly recording may hold the drain gate. A leaked
|
|
75
|
+
// session — phone dropped mid-recording, no close ever sent — otherwise pins
|
|
76
|
+
// this at 1 forever and every Install/Repair/Restart/Update drains, times out
|
|
77
|
+
// and fails with nothing to show the user. The stale ones are reported below
|
|
78
|
+
// rather than counted, and are never deleted here.
|
|
79
|
+
const sessionLiveness = getTranscriptionSessionLiveness()
|
|
74
80
|
const managed = managedRuntimeCapability()
|
|
75
81
|
const tracked = maintenanceLifecycle.snapshot(credentials, {
|
|
76
|
-
recording_session:
|
|
82
|
+
recording_session: sessionLiveness.live,
|
|
77
83
|
})
|
|
78
84
|
const untrackedDurableRuns = Math.max(0, jobs.activeRuns - (tracked.activeByKind.durable_query ?? 0))
|
|
79
85
|
const lifecycle = untrackedDurableRuns > 0
|
|
80
86
|
? maintenanceLifecycle.snapshot(credentials, {
|
|
81
87
|
durable_query_runtime: untrackedDurableRuns,
|
|
82
|
-
recording_session:
|
|
88
|
+
recording_session: sessionLiveness.live,
|
|
83
89
|
})
|
|
84
90
|
: tracked
|
|
85
91
|
return {
|
|
@@ -91,6 +97,13 @@ function statusSnapshot(credentials: MaintenanceOperationCredentials = {}) {
|
|
|
91
97
|
bootId: serverMetrics.bootId,
|
|
92
98
|
activeJobs: jobs.activeRuns,
|
|
93
99
|
activeTranscriptionSessions,
|
|
100
|
+
// Split out so a blocked operator can SEE why, and so "0 orphans" can never
|
|
101
|
+
// again coexist with a held lock: the orphans endpoint reads the quarantine
|
|
102
|
+
// directory, this reads the in-memory session map, and they are different
|
|
103
|
+
// stores. A stale session is surfaced here and blocks nothing.
|
|
104
|
+
liveTranscriptionSessions: sessionLiveness.live,
|
|
105
|
+
staleTranscriptionSessions: sessionLiveness.stale,
|
|
106
|
+
staleTranscriptionSessionDetail: sessionLiveness.staleSessions,
|
|
94
107
|
shuttingDown: jobs.shuttingDown,
|
|
95
108
|
durableStoreState: jobs.store.state,
|
|
96
109
|
lifecycle,
|
|
@@ -342,6 +342,76 @@ export function getActiveTranscriptionSessionCount(): number {
|
|
|
342
342
|
return sessions.size
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
/**
|
|
346
|
+
* How long a session may go silent before it stops BLOCKING a restart.
|
|
347
|
+
*
|
|
348
|
+
* Not a retention policy and not a reap: the session, its chunks and its
|
|
349
|
+
* recoverability are untouched. This decides one thing — whether it still
|
|
350
|
+
* counts toward the maintenance drain gate.
|
|
351
|
+
*
|
|
352
|
+
* Sized against the reasons a LIVE recording legitimately goes quiet: the phone
|
|
353
|
+
* backgrounded and buffering to IndexedDB, a network drop, a long pause. Those
|
|
354
|
+
* run to minutes. It sits deliberately far below
|
|
355
|
+
* LOCAL_FIRST_MEETING_IDLE_RETENTION_MS (4h), which governs how long chunks stay
|
|
356
|
+
* recoverable and is far too long to hold a restart on.
|
|
357
|
+
*/
|
|
358
|
+
export const RECORDING_SESSION_STALE_MS = 30 * 60 * 1000
|
|
359
|
+
|
|
360
|
+
export interface TranscriptionSessionLiveness {
|
|
361
|
+
/** Sessions still plausibly recording. These block a restart. */
|
|
362
|
+
live: number
|
|
363
|
+
/** Sessions silent past the threshold. Surfaced, NOT counted, NOT deleted. */
|
|
364
|
+
stale: number
|
|
365
|
+
/** Which ones, least-silent first, so an operator can act on a name. */
|
|
366
|
+
staleSessions: Array<{ sessionId: string; silentForMs: number; chunks: number }>
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Split active sessions into live and stale.
|
|
371
|
+
*
|
|
372
|
+
* WHY THIS EXISTS. The maintenance gate counts `sessions.size` and blocks every
|
|
373
|
+
* restart while it is non-zero. A session that leaks — the phone drops
|
|
374
|
+
* mid-recording and never sends a close — pins that count at 1 indefinitely, so
|
|
375
|
+
* Install, Repair, Restart and Update Server all drain, time out and fail with
|
|
376
|
+
* no user-visible reason. Observed twice on 2026-08-06 (servers 6.21.20 and
|
|
377
|
+
* 6.21.22); the second phantom had been silent 54 minutes and the only way
|
|
378
|
+
* through was to finalize it as a real meeting, which it was not.
|
|
379
|
+
*
|
|
380
|
+
* THE DISCRIMINATOR IS `lastActivityAt`, NOT `oldestWorkStartedAt`. The obvious
|
|
381
|
+
* reading — "activeTotal >= 1 with oldestWorkStartedAt null means leaked" — is
|
|
382
|
+
* WRONG and would strand healthy recordings. `recording_session` reaches the
|
|
383
|
+
* gate through `extraActiveByKind` (routes/maintenance.ts), and the snapshot's
|
|
384
|
+
* `oldestStartedAtMs` loop walks only tracked `work` entries, so an extra count
|
|
385
|
+
* never contributes a timestamp. EVERY recording session therefore reports
|
|
386
|
+
* `oldestWorkStartedAt: null`, healthy or leaked. Only the session's own
|
|
387
|
+
* `lastActivityAt` tells them apart.
|
|
388
|
+
*/
|
|
389
|
+
/**
|
|
390
|
+
* The live session map, for tests only.
|
|
391
|
+
*
|
|
392
|
+
* Exposed because the liveness split is pure logic over `lastActivityAt` but
|
|
393
|
+
* the map is module-private, and a test that cannot seed it can only assert the
|
|
394
|
+
* empty case. An earlier draft of the liveness test guarded on this symbol
|
|
395
|
+
* existing and silently no-opped four of its six cases — passing green while
|
|
396
|
+
* exercising nothing. Never guard a test on its own seam; make the seam real.
|
|
397
|
+
*/
|
|
398
|
+
export const __sessionsForTests = sessions
|
|
399
|
+
|
|
400
|
+
export function getTranscriptionSessionLiveness(now = Date.now()): TranscriptionSessionLiveness {
|
|
401
|
+
let live = 0
|
|
402
|
+
const staleSessions: TranscriptionSessionLiveness['staleSessions'] = []
|
|
403
|
+
for (const [sessionId, session] of sessions) {
|
|
404
|
+
const silentForMs = now - session.lastActivityAt
|
|
405
|
+
if (silentForMs >= RECORDING_SESSION_STALE_MS) {
|
|
406
|
+
staleSessions.push({ sessionId, silentForMs, chunks: session.chunks.length })
|
|
407
|
+
} else {
|
|
408
|
+
live++
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
staleSessions.sort((a, b) => a.silentForMs - b.silentForMs)
|
|
412
|
+
return { live, stale: staleSessions.length, staleSessions }
|
|
413
|
+
}
|
|
414
|
+
|
|
345
415
|
// Incremental chunk persistence — survive server restarts
|
|
346
416
|
const CHUNK_PERSIST_DIR = dataPath('active-sessions')
|
|
347
417
|
ensurePrivateDirectory(CHUNK_PERSIST_DIR)
|