@gotcos/glasses-server 6.21.29 → 6.21.30

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,37 @@
1
+ ## 6.21.30
2
+
3
+ - **A name you removed by hand is now stated, and the stale write-up is called
4
+ out.** De-attribution rewrites the sidecar, the attendee list and the transcript
5
+ labels, but deliberately leaves narrative prose alone, because substituting into
6
+ a written sentence mangles grammar and can hit the wrong person. The applied
7
+ correction row has recorded `proseStale: true` for exactly this since the feature
8
+ shipped — and NOTHING read it.
9
+
10
+ Real case, 2026-08-07: Miles removed "Clem Ukaoma" from a personal call that was
11
+ only him and Queen (his father's voice matched a similar profile). All 8 label
12
+ sites were rewritten correctly. The LLM summary still opened "Miles, Queen, and
13
+ Clem talk through..." and the payload said nothing at all. The allowlist covered
14
+ it only as "not confirmed", which is far too weak: he did not fail to confirm
15
+ that person, he explicitly said they were not in the room.
16
+
17
+ The payload now carries `removedNames` and states it above the write-up: *"You
18
+ removed "Clem Ukaoma" from this meeting. The write-up below was written before
19
+ that and still uses the name: treat every mention of it as a capture error, not
20
+ a participant."* The prose is left intact — the record stays, the correction
21
+ travels beside it.
22
+
23
+ - **The overlap note is gated on how long the meeting RAN, not on voiced time.**
24
+ Gating on voiced time tripped it on 16 of 23 real meetings, because any overlap
25
+ at all exceeds the union of voiced speech. At 70% it stopped being a signal and
26
+ became boilerplate on something pasted into Slack. Rows adding to more than the
27
+ meeting length is the genuinely confusing case — 71m of rows inside a 66-minute
28
+ meeting — and that is roughly a tenth of meetings.
29
+
30
+ Worth recording for the next person who tests this: only the WORD-TIMING path
31
+ can overflow. Measured, the chunk-estimate path credits a contested second to
32
+ one speaker (two speakers at identical timestamps gave MU 273s and Gina 0s), so
33
+ a chunk-sourced meeting can never trip the note.
34
+
1
35
  ## 6.21.29
2
36
 
3
37
  Everything below was found by two rounds of adversarial review of 6.21.28 against
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.21.29",
3
+ "version": "6.21.30",
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": {
@@ -314,10 +314,19 @@ export interface AttendeeRenderOptions {
314
314
  */
315
315
  unattributedMs: number
316
316
  /**
317
- * UNION of all voiced time. Used only to detect that the rows overlap — see
318
- * the crosstalk note in `renderAttendees`. 0 disables the check.
317
+ * UNION of all voiced time. Only used to phrase the crosstalk note.
319
318
  */
320
319
  voicedMs: number
320
+ /**
321
+ * How long the meeting RAN. This is what the overlap note is gated on.
322
+ *
323
+ * Gating on `voicedMs` instead tripped on 16 of 23 real meetings, because any
324
+ * overlap at all exceeds the union of voiced time — at 70% the line stops being
325
+ * a signal and becomes boilerplate on something Miles pastes into Slack. Rows
326
+ * adding to more than the meeting LENGTH is the genuinely confusing case (71m
327
+ * of rows inside a 66-minute meeting), and that is ~10% of meetings.
328
+ */
329
+ durationMs: number
321
330
  }
322
331
 
323
332
  /** The attendee block, floor-applied. */
@@ -348,9 +357,9 @@ export function renderAttendees(voices: AttendeeLine[], o: AttendeeRenderOptions
348
357
  // right; adding them is what misleads, so say so rather than shrink anyone.
349
358
  const rowTotal = named.reduce((t, v) => t + (Number.isFinite(v.speakingMs) ? v.speakingMs : 0), 0) +
350
359
  (rest.length > 0 && Number.isFinite(o.unattributedMs) ? o.unattributedMs : 0)
351
- if (o.voicedMs > 0 && rowTotal > o.voicedMs * 1.02) {
352
- out.push(`- (these overlap: people talked over each other, so the rows add to ` +
353
- `more than the ${mmss(o.voicedMs)} of talking)`)
360
+ if (o.durationMs > 0 && rowTotal > o.durationMs * 1.02) {
361
+ out.push(`- (these overlap: people talked over each other, so the rows add up to ` +
362
+ `more than the ${mmss(o.durationMs)} the meeting ran)`)
354
363
  }
355
364
  return out.length > 0 ? out.join('\n') : '- (no voices identified)'
356
365
  }
@@ -363,7 +372,32 @@ export function renderAttendees(voices: AttendeeLine[], o: AttendeeRenderOptions
363
372
  * into a model has no other way to tell which. Naming the confirmed set is short
364
373
  * and complete.
365
374
  */
366
- export function renderProvenance(voices: AttendeeLine[], coverage: number | null): string {
375
+ /**
376
+ * Names a human explicitly REMOVED from this meeting, still present in the prose.
377
+ *
378
+ * De-attribution rewrites the sidecar, the attendee list and the transcript
379
+ * labels, but deliberately leaves narrative prose alone — substituting into a
380
+ * written sentence mangles grammar and can hit the wrong person. So the applied
381
+ * correction row records `proseStale: true` and, until now, NOTHING read it.
382
+ *
383
+ * Real case, 2026-08-07: Miles removed "Clem Ukaoma" from a personal call that
384
+ * was only him and Queen (his father's voice matched a similar profile). All 8
385
+ * label sites were rewritten correctly; the LLM summary still opened "Miles,
386
+ * Queen, and Clem talk through..." and the payload said nothing. An allowlist
387
+ * ("this name was not confirmed") is far too weak here — he did not fail to
388
+ * confirm this person, he explicitly said they were not in the room.
389
+ */
390
+ export interface RemovedName {
391
+ label: string
392
+ /** True when narrative prose still carries the name. */
393
+ proseStale: boolean
394
+ }
395
+
396
+ export function renderProvenance(
397
+ voices: AttendeeLine[],
398
+ coverage: number | null,
399
+ removed: RemovedName[] = [],
400
+ ): string {
367
401
  const named = voices.filter(v => v.asserted)
368
402
  // Quoted because a label may legitimately contain a comma — `invalidLabelReason`
369
403
  // rejects brackets and newlines but not commas, and a typed "Smith, John" would
@@ -384,6 +418,20 @@ export function renderProvenance(voices: AttendeeLine[], coverage: number | null
384
418
  'comes from the raw capture or the write-up and was NOT confirmed. Some may be ' +
385
419
  'people mentioned rather than people present.',
386
420
  ]
421
+ // Before the coverage caveat: an explicit human removal outranks every other
422
+ // statement in this block, and it must not read as one more hedge.
423
+ const stale = removed.filter(r => r.proseStale).map(r => `"${r.label}"`)
424
+ const quiet = removed.filter(r => !r.proseStale).map(r => `"${r.label}"`)
425
+ if (stale.length > 0) {
426
+ lines.push(
427
+ `You removed ${stale.join(', ')} from this meeting. The write-up below was ` +
428
+ 'written before that and still uses the name: treat every mention of it as a ' +
429
+ 'capture error, not a participant.',
430
+ )
431
+ }
432
+ if (quiet.length > 0) {
433
+ lines.push(`You also removed ${quiet.join(', ')} from this meeting.`)
434
+ }
387
435
  if (coverage !== null && coverage < SHARE_COVERAGE_FLOOR) {
388
436
  lines.push(
389
437
  `Only ${Math.round(coverage * 100)}% of the voice in this meeting was ` +
@@ -411,6 +459,10 @@ export interface ClipboardInput {
411
459
  capturedChars: number
412
460
  /** UNION of voiced time, for the crosstalk-overflow note. */
413
461
  voicedMs: number
462
+ /** How long the meeting ran. Gates the crosstalk-overflow note. */
463
+ durationMs: number
464
+ /** Names a human explicitly de-attributed from this meeting. */
465
+ removed: RemovedName[]
414
466
  title: string
415
467
  date: string
416
468
  durationMin: number
@@ -445,9 +497,10 @@ function header(i: ClipboardInput, compact: boolean): string[] {
445
497
  parts.push(
446
498
  '', '## Who spoke',
447
499
  renderAttendees(i.attendees, {
448
- coverage: i.coverage, unattributedMs: i.unattributedMs, voicedMs: i.voicedMs,
500
+ coverage: i.coverage, unattributedMs: i.unattributedMs,
501
+ voicedMs: i.voicedMs, durationMs: i.durationMs,
449
502
  }),
450
- '', renderProvenance(i.attendees, i.coverage),
503
+ '', renderProvenance(i.attendees, i.coverage, i.removed),
451
504
  )
452
505
  for (const [heading, body] of [
453
506
  ['Summary', i.scribe.summary],
@@ -8,7 +8,7 @@ import { Router } from 'express'
8
8
  import { emitDisplay } from '../lib/display-bus.js'
9
9
  import { cleanTranscriptLines } from '../lib/hallucination-filter.js'
10
10
  import { durableAtomicWriteFileSync } from '../lib/atomic-fs.js'
11
- import { appendCorrection, pendingCorrections } from '../lib/meeting-corrections.js'
11
+ import { appendCorrection, appliedCorrections, pendingCorrections } from '../lib/meeting-corrections.js'
12
12
  import { isSampleFromSession, untraceableSampleCount } from '../lib/training-audio-provenance.js'
13
13
  import { sendAudioFile } from '../lib/send-audio.js'
14
14
  import { chunkDiagnostics } from '../lib/chunk-embedding-diagnostics.js'
@@ -880,10 +880,19 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
880
880
  coverage,
881
881
  // The UNION, never the sum of per-voice figures — crosstalk is counted once.
882
882
  unattributedMs: review.unattributedSpeakingMs,
883
- // Union of ALL voiced time. Only used to notice that the per-voice rows add
884
- // to more than the meeting, which happens whenever people talk over
885
- // each other — 34 of 323 real meetings.
886
883
  voicedMs: review.voicedMs,
884
+ // The overlap note is gated on how long the meeting RAN. Gating on voiced
885
+ // time tripped on 16 of 23 real meetings.
886
+ durationMs: review.durationMs || 0,
887
+ // Names this human explicitly de-attributed. The ledger has recorded
888
+ // `proseStale` on every applied de-attribution since the feature shipped and
889
+ // NOTHING has ever read it: on 2026-08-07 Miles removed "Clem Ukaoma" from a
890
+ // call that was only him and Queen, all 8 label sites were rewritten, and the
891
+ // summary still opened "Miles, Queen, and Clem talk through..." with no
892
+ // indication anywhere in the payload.
893
+ removed: appliedCorrections(sessionId)
894
+ .filter(r => isUnattributed(r.to))
895
+ .map(r => ({ label: r.from, proseStale: r.proseStale === true })),
887
896
  // Which business this is. The sibling /speakers route has always carried
888
897
  // this and this one dropped it, so a personal 1:1 about someone's
889
898
  // compensation was byte-identical to a marketing sync.
@@ -921,6 +930,8 @@ export function createMeetingRouter(deps: MeetingRouteDependencies = {}): Router
921
930
  // no write-up yet.
922
931
  capturedChars: clip.capturedChars,
923
932
  domain: clip.domain,
933
+ // So the panel can warn above the write-up, not just the clipboard.
934
+ removedNames: clip.removed,
924
935
  coverage,
925
936
  sharesReported: coverage !== null && coverage >= SHARE_COVERAGE_FLOOR,
926
937
  clipboardSummary: summaryText,