@gotcos/glasses-server 6.44.16 → 6.45.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 +77 -51
- package/README.md +9 -0
- package/bin/memory-setup.cjs +55 -0
- package/memory-runtime/cos-memory-runtime-0.1.0.tar.gz +0 -0
- package/memory-runtime/runtime.json +7 -0
- package/package.json +7 -4
- package/server/index.ts +4 -4
- package/server/lib/adaptive-playback-audio.ts +1 -1
- package/server/lib/agent-session-store.ts +139 -28
- package/server/lib/archive-search.ts +1 -1
- package/server/lib/attached-provider-adapter.ts +1 -1
- package/server/lib/attached-workspace.ts +3 -3
- package/server/lib/chunk-embedding-store.ts +1 -1
- package/server/lib/context-files.ts +1 -1
- package/server/lib/cos-context-browser.ts +27 -8
- package/server/lib/cos-operations-meetings.ts +1 -1
- package/server/lib/domains.ts +2 -2
- package/server/lib/maintenance-lifecycle.ts +1 -1
- package/server/lib/meeting-audio-archive.ts +3 -3
- package/server/lib/meeting-corrections.ts +3 -3
- package/server/lib/meeting-relabel-enrolment.ts +1 -1
- package/server/lib/meeting-relabel.ts +2 -2
- package/server/lib/meeting-scribe-content.ts +7 -7
- package/server/lib/meeting-speaker-review.ts +3 -3
- package/server/lib/meeting-summary.ts +1 -1
- package/server/lib/message-era-reset.ts +1 -1
- package/server/lib/message-era.ts +1 -1
- package/server/lib/morning-brief-config.ts +1 -1
- package/server/lib/morning-brief-coverage.ts +1 -1
- package/server/lib/occupancy-probes.ts +7 -4
- package/server/lib/ollama-tools.ts +1 -1
- package/server/lib/profile.ts +29 -5
- package/server/lib/python-bridge.ts +6 -4
- package/server/lib/query-job-types.ts +1 -1
- package/server/lib/session-stream-events.ts +3 -3
- package/server/lib/session-transcript-watcher.ts +1 -1
- package/server/lib/speaker-calibration-log.ts +1 -1
- package/server/lib/speaker-name.ts +1 -1
- package/server/lib/thread-occupancy.ts +1 -1
- package/server/lib/thread-turn-queue-deliver.ts +1 -1
- package/server/lib/thread-turn-queue.ts +3 -3
- package/server/lib/video-upload-v2.ts +1 -1
- package/server/lib/voice-profile-store.ts +2 -2
- package/server/lib/whisper-metal-gate.ts +1 -1
- package/server/routes/agent-session-bindings.ts +2 -2
- package/server/routes/agent-session-stream.ts +1 -1
- package/server/routes/agent-sessions.ts +2 -2
- package/server/routes/claude-sessions.ts +1 -1
- package/server/routes/health.ts +3 -1
- package/server/routes/meeting.ts +8 -8
- package/server/routes/memory.ts +66 -7
- package/server/routes/prompt-drafts.ts +1 -1
- package/server/routes/session-index.ts +1 -1
- package/server/routes/thread-turn-queue.ts +10 -4
- package/server/routes/transcribe-stream.ts +15 -4
- package/server/routes/voice.ts +4 -4
|
@@ -107,7 +107,7 @@ function routeAutoClean(req: { body?: any; query?: any }): AutoCleanRequest {
|
|
|
107
107
|
async function cleanOutboundDictation(text: string, opts: AutoCleanRequest & { signal?: AbortSignal }): Promise<string> {
|
|
108
108
|
let cleaned = applyNegativeRules(applyCorrections(text)).replace(/\s+/g, ' ').trim() || text
|
|
109
109
|
// applyCorrections above is an EXACT string map, so it only fixes misspellings someone
|
|
110
|
-
// already hand-authored. A novel miss ("
|
|
110
|
+
// already hand-authored. A novel miss ("Rileyy" for Riley, "Exampel" for Example) sails
|
|
111
111
|
// through it. The Levenshtein pass is what catches those, and until now it had exactly
|
|
112
112
|
// ONE call site — transcribe-audio.ts:252, the server-transcription route — so phone
|
|
113
113
|
// Moonshine dictation, which arrives here as text, never got it. Same construction and
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// process also streams live audio, so that blocked the event loop. Now async
|
|
30
30
|
// and cached against file identity.
|
|
31
31
|
// 4. The filename filter matched iCloud sync-conflict duplicates. On this machine
|
|
32
|
-
// `.
|
|
32
|
+
// `.session_index_cache_Example-Mac-Studio 3.json` shares 543 of its 602 rows
|
|
33
33
|
// with the canonical file, so the merged list served 543 duplicates. Filtering
|
|
34
34
|
// ` N` filenames out would be wrong in the other direction: ` 2.json` holds 245
|
|
35
35
|
// rows that appear nowhere else. Dedupe therefore happens by session_id, newest
|
|
@@ -102,13 +102,17 @@ export async function drainThread(
|
|
|
102
102
|
|
|
103
103
|
let delivered = 0, held = 0, retired = 0
|
|
104
104
|
let dirty = false
|
|
105
|
+
let refusedGate: ReturnType<ThreadTurnQueueDeps['occupancy']> | null = null
|
|
105
106
|
|
|
106
107
|
for (const turn of queue) {
|
|
107
108
|
if (turn.status !== 'waiting') continue
|
|
108
109
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
110
|
+
// Reuse only refusal within this synchronous pass. Rechecking a busy thread
|
|
111
|
+
// for every queued row repeats a whole-process descriptor scan. A refusal
|
|
112
|
+
// can delay delivery until the next sweep, but can never authorize a write.
|
|
113
|
+
// Positive verdicts and all checks after a delivery remain fresh.
|
|
114
|
+
const gate: ReturnType<ThreadTurnQueueDeps['occupancy']> = refusedGate ?? deps.occupancy(provider, threadId)
|
|
115
|
+
if (!gate.attachable) refusedGate = gate
|
|
112
116
|
const seen: DrainObservation = {
|
|
113
117
|
attachable: gate.attachable,
|
|
114
118
|
turnEnded: deps.turnEnded(provider, threadId),
|
|
@@ -135,11 +139,13 @@ export async function drainThread(
|
|
|
135
139
|
dirty = true
|
|
136
140
|
|
|
137
141
|
let outcome: { ok: boolean; reason?: string; serverRetryable?: boolean }
|
|
142
|
+
refusedGate = null
|
|
138
143
|
try {
|
|
139
144
|
outcome = await deps.deliver(turn)
|
|
140
145
|
} catch (error) {
|
|
141
146
|
outcome = { ok: false, reason: error instanceof Error ? error.message : 'deliver_threw' }
|
|
142
147
|
}
|
|
148
|
+
refusedGate = null
|
|
143
149
|
|
|
144
150
|
if (outcome.ok) {
|
|
145
151
|
turn.status = 'delivered'
|
|
@@ -147,7 +153,7 @@ export async function drainThread(
|
|
|
147
153
|
delivered += 1
|
|
148
154
|
} else if (isRetryableDelivery(outcome)) {
|
|
149
155
|
// A GATE REFUSAL IS NOT A FAILED DELIVERY, so it must not spend the ceiling --
|
|
150
|
-
// measured cost of getting this wrong: three of
|
|
156
|
+
// measured cost of getting this wrong: three of the user's turns retired in about
|
|
151
157
|
// two minutes each, never delivered, while the 6h TTL never got to matter.
|
|
152
158
|
//
|
|
153
159
|
// REFUNDED, not deferred. The increment and its fsync stay BEFORE the call
|
|
@@ -144,7 +144,7 @@ const THANKYOU_FILTER = process.env.COS_WHISPER_THANKYOU_FILTER !== '0'
|
|
|
144
144
|
const MEETING_PREVIEW_MAX_BYTES = 512 * 1024
|
|
145
145
|
let meetingPreviewBusy = false
|
|
146
146
|
|
|
147
|
-
// DEFAULT ON as of 6.21.22 (
|
|
147
|
+
// DEFAULT ON as of 6.21.22 (the user, 2026-08-06: "I want it to default to being
|
|
148
148
|
// on... it's stable and makes meetings so much better"). Opt-OUT now, matching
|
|
149
149
|
// STRIP_BRAND_URLS / THANKYOU_FILTER above: set COS_WHISPER_MEETING_PREVIEW=0
|
|
150
150
|
// to disable. The committed Large-v3 transcript remains authoritative and
|
|
@@ -463,6 +463,8 @@ export interface StrandedCapture {
|
|
|
463
463
|
/** Minutes of audio actually captured before it went quiet. */
|
|
464
464
|
capturedMinutes: number
|
|
465
465
|
chunks: number
|
|
466
|
+
transcriptState: 'ready' | 'processing' | 'no_speech'
|
|
467
|
+
canSave: boolean
|
|
466
468
|
/** When the sweeper will save this on the user's behalf. */
|
|
467
469
|
promotesAt: string
|
|
468
470
|
/** Readable draft on disk, once the capture has been stale long enough. */
|
|
@@ -485,21 +487,30 @@ export interface StrandedCapture {
|
|
|
485
487
|
*/
|
|
486
488
|
export function getStrandedCaptures(now = Date.now()): StrandedCapture[] {
|
|
487
489
|
const drafts = new Map(listStrandedDrafts(STRANDED_DRAFT_DIR).map(d => [d.sessionId, d]))
|
|
488
|
-
return getTranscriptionSessionLiveness(now).staleSessions.
|
|
490
|
+
return getTranscriptionSessionLiveness(now).staleSessions.flatMap(stale => {
|
|
489
491
|
const session = sessions.get(stale.sessionId)
|
|
490
492
|
const lastActivityAt = session?.lastActivityAt ?? now - stale.silentForMs
|
|
491
493
|
const draft = drafts.get(stale.sessionId) ?? null
|
|
492
|
-
|
|
494
|
+
const received = session?.receivedIndices ?? []
|
|
495
|
+
// An empty session shell is not evidence that audio was lost. Leave its
|
|
496
|
+
// ledger open so a backgrounded phone can still upload before retention.
|
|
497
|
+
if (!stale.chunks && !received.length && !hasSessionAudio(stale.sessionId)) return []
|
|
498
|
+
const completed = new Set(session?.asrCompletedIndices ?? [])
|
|
499
|
+
const noSpeech = !stale.chunks && received.length > 0 && received.every(index => completed.has(index))
|
|
500
|
+
const transcriptState: StrandedCapture['transcriptState'] = stale.chunks ? 'ready' : noSpeech ? 'no_speech' : 'processing'
|
|
501
|
+
return [{
|
|
493
502
|
sessionId: stale.sessionId,
|
|
494
503
|
idleMinutes: Math.round(stale.silentForMs / 60_000),
|
|
495
504
|
capturedMinutes: Math.round(
|
|
496
505
|
Math.max(0, lastActivityAt - (session?.startTime ?? lastActivityAt)) / 60_000,
|
|
497
506
|
),
|
|
498
507
|
chunks: stale.chunks,
|
|
508
|
+
transcriptState,
|
|
509
|
+
canSave: transcriptState === 'ready',
|
|
499
510
|
promotesAt: new Date(lastActivityAt + LOCAL_FIRST_MEETING_IDLE_RETENTION_MS).toISOString(),
|
|
500
511
|
draftPath: draft?.path ?? null,
|
|
501
512
|
draftBytes: draft?.bytes ?? null,
|
|
502
|
-
}
|
|
513
|
+
}]
|
|
503
514
|
})
|
|
504
515
|
}
|
|
505
516
|
|
package/server/routes/voice.ts
CHANGED
|
@@ -441,15 +441,15 @@ voiceRouter.post('/voice/enroll-ext', async (req, res) => {
|
|
|
441
441
|
|
|
442
442
|
// ── Voice sample playback (6.21.18) ───────────────────────────────────────
|
|
443
443
|
//
|
|
444
|
-
//
|
|
444
|
+
// the user: hearing three seconds of a voice settles an identity question that a
|
|
445
445
|
// similarity score cannot. These two paths need NO retention change — the audio
|
|
446
446
|
// already exists:
|
|
447
447
|
//
|
|
448
448
|
// training-audio what the system thinks a NAMED person sounds like
|
|
449
449
|
// ext-audio an UNIDENTIFIED voice, 72-hour window
|
|
450
450
|
//
|
|
451
|
-
// The first is the higher-value one for the review panel: "is this really
|
|
452
|
-
// is answered by playing
|
|
451
|
+
// The first is the higher-value one for the review panel: "is this really Jamie?"
|
|
452
|
+
// is answered by playing Jamie's own profile sample, not by playing the segment
|
|
453
453
|
// under review.
|
|
454
454
|
|
|
455
455
|
/** Newest WAV in a directory — the most representative recent sample. */
|
|
@@ -646,7 +646,7 @@ function fanOutTotals(runs: readonly SpeakerRenameFanOut[]): {
|
|
|
646
646
|
|
|
647
647
|
// Fails closed below the search-accept threshold. A wrong merge destroys BOTH
|
|
648
648
|
// identities at once and cannot be undone from the store alone, so the only
|
|
649
|
-
// acceptable evidence is acoustic. `force` exists for the case where
|
|
649
|
+
// acceptable evidence is acoustic. `force` exists for the case where the user
|
|
650
650
|
// knows something the audio does not, and it is logged.
|
|
651
651
|
voiceRouter.post('/voice/merge-profiles', (req, res) => {
|
|
652
652
|
try {
|