@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +77 -51
  2. package/README.md +9 -0
  3. package/bin/memory-setup.cjs +55 -0
  4. package/memory-runtime/cos-memory-runtime-0.1.0.tar.gz +0 -0
  5. package/memory-runtime/runtime.json +7 -0
  6. package/package.json +7 -4
  7. package/server/index.ts +4 -4
  8. package/server/lib/adaptive-playback-audio.ts +1 -1
  9. package/server/lib/agent-session-store.ts +139 -28
  10. package/server/lib/archive-search.ts +1 -1
  11. package/server/lib/attached-provider-adapter.ts +1 -1
  12. package/server/lib/attached-workspace.ts +3 -3
  13. package/server/lib/chunk-embedding-store.ts +1 -1
  14. package/server/lib/context-files.ts +1 -1
  15. package/server/lib/cos-context-browser.ts +27 -8
  16. package/server/lib/cos-operations-meetings.ts +1 -1
  17. package/server/lib/domains.ts +2 -2
  18. package/server/lib/maintenance-lifecycle.ts +1 -1
  19. package/server/lib/meeting-audio-archive.ts +3 -3
  20. package/server/lib/meeting-corrections.ts +3 -3
  21. package/server/lib/meeting-relabel-enrolment.ts +1 -1
  22. package/server/lib/meeting-relabel.ts +2 -2
  23. package/server/lib/meeting-scribe-content.ts +7 -7
  24. package/server/lib/meeting-speaker-review.ts +3 -3
  25. package/server/lib/meeting-summary.ts +1 -1
  26. package/server/lib/message-era-reset.ts +1 -1
  27. package/server/lib/message-era.ts +1 -1
  28. package/server/lib/morning-brief-config.ts +1 -1
  29. package/server/lib/morning-brief-coverage.ts +1 -1
  30. package/server/lib/occupancy-probes.ts +7 -4
  31. package/server/lib/ollama-tools.ts +1 -1
  32. package/server/lib/profile.ts +29 -5
  33. package/server/lib/python-bridge.ts +6 -4
  34. package/server/lib/query-job-types.ts +1 -1
  35. package/server/lib/session-stream-events.ts +3 -3
  36. package/server/lib/session-transcript-watcher.ts +1 -1
  37. package/server/lib/speaker-calibration-log.ts +1 -1
  38. package/server/lib/speaker-name.ts +1 -1
  39. package/server/lib/thread-occupancy.ts +1 -1
  40. package/server/lib/thread-turn-queue-deliver.ts +1 -1
  41. package/server/lib/thread-turn-queue.ts +3 -3
  42. package/server/lib/video-upload-v2.ts +1 -1
  43. package/server/lib/voice-profile-store.ts +2 -2
  44. package/server/lib/whisper-metal-gate.ts +1 -1
  45. package/server/routes/agent-session-bindings.ts +2 -2
  46. package/server/routes/agent-session-stream.ts +1 -1
  47. package/server/routes/agent-sessions.ts +2 -2
  48. package/server/routes/claude-sessions.ts +1 -1
  49. package/server/routes/health.ts +3 -1
  50. package/server/routes/meeting.ts +8 -8
  51. package/server/routes/memory.ts +66 -7
  52. package/server/routes/prompt-drafts.ts +1 -1
  53. package/server/routes/session-index.ts +1 -1
  54. package/server/routes/thread-turn-queue.ts +10 -4
  55. package/server/routes/transcribe-stream.ts +15 -4
  56. 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 ("Miyala" for Niala, "Yukoma" for Ukaoma) sails
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
- // `.session_index_cache_Ukaoma-Mac-Studio 3.json` shares 543 of its 602 rows
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
- // Observed FRESH for each turn: delivering one makes the thread busy again, so a
110
- // verdict from the top of the loop would be stale by the second item.
111
- const gate = deps.occupancy(provider, threadId)
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 Miles's turns retired in about
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 (Miles, 2026-08-06: "I want it to default to being
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.map(stale => {
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
- return {
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
 
@@ -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
- // Miles: hearing three seconds of a voice settles an identity question that a
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 Navaz?"
452
- // is answered by playing Navaz's own profile sample, not by playing the segment
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 Miles
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 {