@gotcos/glasses-server 6.44.15 → 6.45.0

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 (55) hide show
  1. package/CHANGELOG.md +83 -49
  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 +6 -3
  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 +17 -6
  16. package/server/lib/cos-operations-meetings.ts +15 -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/python-bridge.ts +6 -4
  33. package/server/lib/query-job-types.ts +1 -1
  34. package/server/lib/session-stream-events.ts +3 -3
  35. package/server/lib/session-transcript-watcher.ts +1 -1
  36. package/server/lib/speaker-calibration-log.ts +1 -1
  37. package/server/lib/speaker-name.ts +1 -1
  38. package/server/lib/thread-occupancy.ts +1 -1
  39. package/server/lib/thread-turn-queue-deliver.ts +1 -1
  40. package/server/lib/thread-turn-queue.ts +3 -3
  41. package/server/lib/video-upload-v2.ts +1 -1
  42. package/server/lib/voice-profile-store.ts +2 -2
  43. package/server/lib/whisper-metal-gate.ts +1 -1
  44. package/server/routes/agent-session-bindings.ts +2 -2
  45. package/server/routes/agent-session-stream.ts +1 -1
  46. package/server/routes/agent-sessions.ts +2 -2
  47. package/server/routes/claude-sessions.ts +1 -1
  48. package/server/routes/health.ts +1 -1
  49. package/server/routes/meeting.ts +8 -8
  50. package/server/routes/memory.ts +45 -7
  51. package/server/routes/prompt-drafts.ts +1 -1
  52. package/server/routes/session-index.ts +1 -1
  53. package/server/routes/thread-turn-queue.ts +10 -4
  54. package/server/routes/transcribe-stream.ts +1 -1
  55. package/server/routes/voice.ts +4 -4
@@ -111,7 +111,7 @@ export function isScratchCursorProject(folder: string): boolean {
111
111
  export const isSkippedCursorFolder = isScratchCursorProject
112
112
 
113
113
  /**
114
- * Titles that belong to a MACHINE, not to a conversation Miles had.
114
+ * Titles that belong to a MACHINE, not to a conversation the user had.
115
115
  *
116
116
  * Measured 2026-08-18 by driving the real collector over the 1,296 Claude transcripts on
117
117
  * this machine: of the 41 Claude docs it indexed, 28 were machine prompts -- 22 Slack
@@ -141,7 +141,7 @@ export function isKeepWarmSessionTitle(title: string): boolean {
141
141
  /**
142
142
  * The compaction preamble, which the harness writes as a USER turn.
143
143
  *
144
- * Miles's 2026-08-17 screenshot had this sitting in the DISCUSSION list as though he
144
+ * the user's 2026-08-17 screenshot had this sitting in the DISCUSSION list as though he
145
145
  * had asked it: "This session is being continued from a previous conversation… Summary:
146
146
  * ## 1. Primary Request and Intent". It is scaffolding, and it lands in the digest at
147
147
  * full length because it IS recent -- compaction happens mid-session -- so recency
@@ -251,7 +251,7 @@ export function proseSnippet(text: string, max = 160): string {
251
251
  * swipes of deliberate reading; 21,757 would be 109. Bounded on purpose: this
252
252
  * crosses a phone radio and renders on a 576x288 HUD.
253
253
  *
254
- * WHY NOT LOWER. 2,000 would still cut 7.5% of replies, and Miles's 1,821-char reply
254
+ * WHY NOT LOWER. 2,000 would still cut 7.5% of replies, and the user's 1,821-char reply
255
255
  * — the one that started this — sits inside the band that a 2,000 cap leaves with no
256
256
  * headroom at all.
257
257
  */
@@ -311,7 +311,7 @@ const DIGEST_TURN_MAX = 220
311
311
  * "frames everything after it" -- see the head-first block in composeDiscussionDigest,
312
312
  * which was itself a fix for a digest that spent everything on the tail.
313
313
  *
314
- * Miles overruled it from hardware, 2026-08-17, looking at a digest whose first two
314
+ * the user overruled it from hardware, 2026-08-17, looking at a digest whose first two
315
315
  * bullets were a question from the previous day and a compaction preamble: "It needs to
316
316
  * refresh to the most recent response inside of the thread. The discussion should show
317
317
  * the questions that we're asking and a summary of those most recent things, not
@@ -322,7 +322,7 @@ const DIGEST_TURN_MAX = 220
322
322
  * occupying the two most valuable slots in a list about what is happening NOW.
323
323
  *
324
324
  * Left as a constant rather than deleting the head path: this is a judgement call about
325
- * emphasis, and one number is the whole reversal if Miles wants some framing back.
325
+ * emphasis, and one number is the whole reversal if the user wants some framing back.
326
326
  */
327
327
  const DIGEST_HEAD_TURNS = 0
328
328
 
@@ -641,14 +641,96 @@ export function normalizeClaudeSessionId(raw: string): string {
641
641
  return id
642
642
  }
643
643
 
644
- export function peekClaudeDesktopHead(text: string): { title: string; cwd: string } {
645
- const unescape = (value: string) => value.replace(/\\"/g, '"').replace(/\\\\/g, '\\')
646
- const title = /"title"\s*:\s*"((?:\\.|[^"\\])*)"/.exec(text)
647
- const cwd = /"cwd"\s*:\s*"((?:\\.|[^"\\])*)"/.exec(text)
644
+ /** Read completed top-level fields from a bounded JSON prefix. Nested transcript
645
+ * strings are not metadata. Repeated keys and non-string values fail closed. */
646
+ export function desktopMetadataPrefix(text: string): { fields: Map<string, string | null>; complete: boolean } {
647
+ const fields = new Map<string, string | null>()
648
+ let i = 0
649
+ const space = () => { while (/\s/.test(text[i] || '') && i < text.length) i++ }
650
+ const string = (): string | undefined => {
651
+ const start = i
652
+ if (text[i++] !== '"') return undefined
653
+ let escaped = false
654
+ while (i < text.length) {
655
+ const ch = text[i++]
656
+ if (!escaped && ch === '"') {
657
+ try { return JSON.parse(text.slice(start, i)) as string } catch { return undefined }
658
+ }
659
+ if (!escaped && ch === '\\') escaped = true
660
+ else escaped = false
661
+ }
662
+ return undefined
663
+ }
664
+ space()
665
+ if (text[i++] !== '{') return { fields, complete: false }
666
+ while (i < text.length) {
667
+ space()
668
+ if (text[i] === '}') return { fields, complete: true }
669
+ const key = string(); if (key === undefined) break
670
+ space(); if (text[i++] !== ':') break
671
+ space()
672
+ let value: string | null = null
673
+ if (text[i] === '"') { const parsed = string(); if (parsed === undefined) break; value = parsed }
674
+ else {
675
+ let depth = 0, inString = false, escaped = false
676
+ while (i < text.length) {
677
+ const ch = text[i]
678
+ if (!inString && depth === 0 && (ch === ',' || ch === '}')) break
679
+ i++
680
+ if (inString) {
681
+ if (!escaped && ch === '"') inString = false
682
+ if (!escaped && ch === '\\') escaped = true; else escaped = false
683
+ } else if (ch === '"') inString = true
684
+ else if (ch === '{' || ch === '[') depth++
685
+ else if (ch === '}' || ch === ']') depth--
686
+ }
687
+ if (i === text.length) break
688
+ }
689
+ fields.set(key, fields.has(key) ? null : value)
690
+ space()
691
+ if (text[i] === '}') return { fields, complete: true }
692
+ if (text[i++] !== ',') break
693
+ }
694
+ return { fields, complete: false }
695
+ }
696
+
697
+ export function peekClaudeDesktopHead(text: string): { title: string; cwd: string; cliSessionId: string } {
698
+ const { fields, complete } = desktopMetadataPrefix(text)
699
+ const cli = fields.get('cliSessionId')
700
+ const normalized = typeof cli === 'string' ? normalizeClaudeSessionId(cli) : ''
648
701
  return {
649
- title: title ? unescape(title[1]).slice(0, 120) : '',
650
- cwd: cwd ? unescape(cwd[1]) : '',
702
+ title: (fields.get('title') || '').slice(0, 120),
703
+ cwd: fields.get('cwd') || '',
704
+ cliSessionId: fields.has('cliSessionId')
705
+ ? (CLAUDE_UUID_JSONL.test(normalized + '.jsonl') ? normalized : '!invalid')
706
+ : (complete ? '' : '!unreadable'),
707
+ }
708
+ }
709
+
710
+ type ClaudeDesktopHead = ReturnType<typeof peekClaudeDesktopHead> & { file: string; mtimeMs: number }
711
+
712
+ /** Explicit Desktop→CLI aliases only. A conflicting alias cannot select a transcript. */
713
+ export async function loadClaudeDesktopAliases(root: string): Promise<Map<string, ClaudeDesktopHead | null>> {
714
+ const aliases = new Map<string, ClaudeDesktopHead | null>()
715
+ if (!root) return aliases
716
+ for (const account of await dirents(root)) {
717
+ for (const workspace of await dirents(join(root, account))) {
718
+ const dir = join(root, account, workspace)
719
+ for (const name of await dirents(dir)) {
720
+ if (!name.startsWith('local_') || !name.endsWith('.json')) continue
721
+ const id = normalizeClaudeSessionId(name.slice(0, -5))
722
+ if (!CLAUDE_UUID_JSONL.test(id + '.jsonl')) continue
723
+ const file = join(dir, name), st = await fileStat(file)
724
+ if (!st?.isFile) continue
725
+ const head = { ...peekClaudeDesktopHead(await readWindow(file, false)), file, mtimeMs: st.mtimeMs }
726
+ const previous = aliases.get(id)
727
+ if (previous === null) continue
728
+ if (previous && previous.cliSessionId !== head.cliSessionId) aliases.set(id, null)
729
+ else if (!previous || head.mtimeMs >= previous.mtimeMs) aliases.set(id, head)
730
+ }
731
+ }
651
732
  }
733
+ return aliases
652
734
  }
653
735
 
654
736
  export async function loadClaudeStarredIds(configPath: string): Promise<Set<string>> {
@@ -851,6 +933,16 @@ export async function listClaudeSessions(
851
933
  desktopSessionsRoot = '',
852
934
  dropped: AgentSessionListDropped = emptySessionListDropped(),
853
935
  ): Promise<AgentSessionRow[]> {
936
+ const aliases = await loadClaudeDesktopAliases(desktopSessionsRoot)
937
+ const canonical = (id: string) => { const cli = aliases.get(id)?.cliSessionId; return cli && CLAUDE_UUID_JSONL.test(cli + '.jsonl') ? cli : id }
938
+ const canonicalStars = new Set([...starredIds].map(canonical))
939
+ const canonicalLive = new Set([...liveIds].map(canonical))
940
+ const desktopByCli = new Map<string, ClaudeDesktopHead>()
941
+ for (const [id, head] of aliases) {
942
+ if (!head) continue
943
+ const key = canonical(id), prev = desktopByCli.get(key)
944
+ if (!prev || head.mtimeMs >= prev.mtimeMs) desktopByCli.set(key, head)
945
+ }
854
946
  const seen = new Set<string>()
855
947
  const pinnedCandidates: Array<{ file: string; native: string; project: string; mtimeMs: number; birthtimeMs: number; desktop?: string }> = []
856
948
  const recentCandidates: Array<{ file: string; native: string; project: string; mtimeMs: number; birthtimeMs: number; desktop?: string }> = []
@@ -862,11 +954,11 @@ export async function listClaudeSessions(
862
954
  if (!CLAUDE_UUID_JSONL.test(name)) continue
863
955
  const native = name.slice(0, -6)
864
956
  const shortId = native.slice(0, 8)
865
- if (liveIds.has(shortId) || liveIds.has(native)) continue
957
+ if (canonicalLive.has(shortId) || canonicalLive.has(native)) continue
866
958
  const file = join(dir, name)
867
959
  const st = await fileStat(file)
868
960
  if (!st?.isFile) continue
869
- const pinned = starredIds.has(native.toLowerCase())
961
+ const pinned = canonicalStars.has(native.toLowerCase())
870
962
  const fresh = now.getTime() - st.mtimeMs <= AGENT_SESSION_MAX_AGE_MS
871
963
  if (!pinned && !fresh) {
872
964
  dropped.age += 1
@@ -878,15 +970,16 @@ export async function listClaudeSessions(
878
970
  project: workspaceLabel(folder),
879
971
  mtimeMs: st.mtimeMs,
880
972
  birthtimeMs: st.birthtimeMs,
973
+ desktop: desktopByCli.get(native)?.file,
881
974
  }
882
975
  seen.add(native.toLowerCase())
883
976
  if (pinned) pinnedCandidates.push(candidate)
884
977
  else recentCandidates.push(candidate)
885
978
  }
886
979
  }
887
- for (const starred of starredIds) {
888
- if (seen.has(starred) || liveIds.has(starred) || liveIds.has(starred.slice(0, 8))) continue
889
- const desktop = await findClaudeDesktopFile(desktopSessionsRoot, starred)
980
+ for (const starred of canonicalStars) {
981
+ if (seen.has(starred) || canonicalLive.has(starred) || canonicalLive.has(starred.slice(0, 8))) continue
982
+ const desktop = desktopByCli.get(starred)?.file
890
983
  if (!desktop) continue
891
984
  const st = await fileStat(desktop)
892
985
  if (!st?.isFile) continue
@@ -921,11 +1014,11 @@ export async function listClaudeSessions(
921
1014
  title = peek.customTitle ?? firstPrompt
922
1015
  latestAssistant = peek.latestAssistant
923
1016
  }
924
- if ((!title || !project) && (candidate.desktop || desktopSessionsRoot)) {
1017
+ if (candidate.desktop || !title || !project) {
925
1018
  const desktop = candidate.desktop || await findClaudeDesktopFile(desktopSessionsRoot, candidate.native)
926
1019
  if (desktop) {
927
1020
  const head = peekClaudeDesktopHead(await readWindow(desktop, false))
928
- if (!title && head.title) title = head.title
1021
+ if (head.title) title = head.title
929
1022
  if (!project && head.cwd) project = workspaceLabel(head.cwd)
930
1023
  }
931
1024
  }
@@ -939,7 +1032,7 @@ export async function listClaudeSessions(
939
1032
  modified: isoFromMtime(candidate.mtimeMs),
940
1033
  created: isoFromMtime(candidate.birthtimeMs),
941
1034
  alive: false,
942
- pinned: starredIds.has(candidate.native.toLowerCase()),
1035
+ pinned: canonicalStars.has(candidate.native.toLowerCase()),
943
1036
  ...discussionFields(title, firstPrompt, latestAssistant),
944
1037
  }))
945
1038
  }
@@ -997,9 +1090,9 @@ export async function listCodexSessions(
997
1090
  // `session_meta` record wholesale, so the new rollout carries the PARENT's id
998
1091
  // while its filename carries its own.
999
1092
  //
1000
- // Miles forked "Markt POS 2.0 build" into "POS Nation 3.0 build" and the fork
1093
+ // the user forked "Example project 2.0 build" into "Example project 3.0 build" and the fork
1001
1094
  // never appeared in the session list. It had not failed to index -- it was indexed
1002
- // AS its parent, so the list showed one `Markt POS 2.0 build` row whose modified
1095
+ // AS its parent, so the list showed one `Example project 2.0 build` row whose modified
1003
1096
  // time was the fork's activity. Two conversations, one identity, and the newer one
1004
1097
  // invisible.
1005
1098
  //
@@ -1183,7 +1276,11 @@ export async function listAgentSessions(
1183
1276
  sort: AgentSessionSort = 'updated',
1184
1277
  dropped: AgentSessionListDropped = emptySessionListDropped(),
1185
1278
  ): Promise<AgentSessionRow[]> {
1186
- const starredIds = await loadClaudeStarredIds(roots.claudeDesktopConfig)
1279
+ const originalStars = await loadClaudeStarredIds(roots.claudeDesktopConfig)
1280
+ const aliases = await loadClaudeDesktopAliases(roots.claudeCodeSessions)
1281
+ const canonical = (id: string) => { const cli = aliases.get(id)?.cliSessionId; return cli && CLAUDE_UUID_JSONL.test(cli + '.jsonl') ? cli : id }
1282
+ const starredIds = new Set([...originalStars].map(canonical))
1283
+ live = live.map(row => row.provider === 'claude' ? { ...row, session_id: canonical(normalizeClaudeSessionId(row.session_id)) } : row)
1187
1284
  const cursorPinned = await loadCursorPinnedIds(roots.cursorWorkspaceStorage)
1188
1285
  const enrichedLive = (await Promise.all(live.map(row => enrichLiveClaude(row, roots))))
1189
1286
  .filter(entry => !isKeepWarmSessionTitle(entry.display_label))
@@ -1233,15 +1330,29 @@ export async function findAgentSessionFile(
1233
1330
  if (!isSafeSessionId(sessionId)) return null
1234
1331
  const needle = sessionId.trim().toLowerCase()
1235
1332
  if (provider === 'claude') {
1333
+ const aliases = await loadClaudeDesktopAliases(roots.claudeCodeSessions)
1334
+ const files = new Map<string, string[]>()
1236
1335
  for (const folder of await dirents(roots.claudeProjects)) {
1237
1336
  const dir = join(roots.claudeProjects, folder)
1238
1337
  for (const name of await dirents(dir)) {
1239
- if (!name.endsWith('.jsonl')) continue
1338
+ if (!CLAUDE_UUID_JSONL.test(name)) continue
1339
+ const file = join(dir, name)
1340
+ if (!(await fileStat(file))?.isFile) continue
1240
1341
  const id = name.slice(0, -6).toLowerCase()
1241
- if (id === needle || id.startsWith(needle)) return join(dir, name)
1342
+ files.set(id, [...(files.get(id) || []), file])
1242
1343
  }
1243
1344
  }
1244
- return null
1345
+ const exact = aliases.has(needle) || files.has(needle)
1346
+ const ids = new Set<string>()
1347
+ for (const [id, head] of aliases) {
1348
+ if (!(exact ? id === needle : id.startsWith(needle))) continue
1349
+ if (!head) return null
1350
+ ids.add(head.cliSessionId || id)
1351
+ }
1352
+ for (const id of files.keys()) if (exact ? id === needle : id.startsWith(needle)) ids.add(id)
1353
+ if (ids.size !== 1) return null
1354
+ const matches = files.get([...ids][0]) || []
1355
+ return matches.length === 1 ? matches[0] : null
1245
1356
  }
1246
1357
  if (provider === 'codex') {
1247
1358
  for (const file of await listCodexJsonlFiles(roots.codexSessions)) {
@@ -1469,18 +1580,18 @@ export async function parseAgentSession(
1469
1580
  /**
1470
1581
  * Add a user turn to the recency digest -- or decline to.
1471
1582
  *
1472
- * TWO DECLINES, both found by parsing Miles's own 94 MiB session rather than by
1583
+ * TWO DECLINES, both found by parsing the user's own 94 MiB session rather than by
1473
1584
  * reasoning about the code.
1474
1585
  *
1475
1586
  * `injected`: STRUCTURAL, not a heuristic. Claude marks a slash-command body with
1476
1587
  * `isMeta: true` and a compaction preamble with `isCompactSummary: true`. Both are
1477
- * written as USER rows, so both rendered in the digest as things Miles had asked --
1588
+ * written as USER rows, so both rendered in the digest as things the user had asked --
1478
1589
  * his screenshot led with "This session is being continued from a previous
1479
1590
  * conversation… Summary: ## 1. Primary Request and Intent". Neither is an ask, and
1480
1591
  * both are recent, so recency ordering alone leaves them exactly where they were.
1481
1592
  *
1482
1593
  * `fromTail` on a TRUNCATED read: the head window IS the session opening. Feeding it
1483
- * into a recency list is the very thing Miles objected to -- "not something that's
1594
+ * into a recency list is the very thing the user objected to -- "not something that's
1484
1595
  * the 'first' message" -- and on a large session the 60-turn window never fills, so
1485
1596
  * those opening turns survive to the top of the digest forever. Measured on his
1486
1597
  * session: the two leading bullets were both head-window turns from the previous day.
@@ -72,7 +72,7 @@ function cleanSnippet(raw: string): string {
72
72
  * Count matches in `hayLower` but cut snippets from `hayOriginal`.
73
73
  *
74
74
  * Matching has to happen on the lowercased text for case-insensitivity, but a
75
- * snippet cut from that text renders as all-lowercase prose -- "chelsie owes the
75
+ * snippet cut from that text renders as all-lowercase prose -- "casey owes the
76
76
  * sales-vs-education split" -- which reads like broken data in a UI. The offsets
77
77
  * line up because toLowerCase() is length-preserving for effectively all real
78
78
  * archive text; where it is NOT (a handful of exotic codepoints expand), the
@@ -405,7 +405,7 @@ const STRIPPED_ENV_KEYS: readonly string[] = ['CLAUDECODE', 'COS_API_TOKEN']
405
405
  * the prompt is the user's private text; stdin also matches what both ordinary
406
406
  * bridges already do.
407
407
  *
408
- * THE TURN INHERITS THE SESSION'S OWN PERMISSIONS (Miles, 2026-08-16, explicit).
408
+ * THE TURN INHERITS THE SESSION'S OWN PERMISSIONS (the user, 2026-08-16, explicit).
409
409
  *
410
410
  * This used to force two independent read-only layers: `--permission-mode plan`
411
411
  * plus an empty `--tools`/`--allowedTools` pair. The result was a Continue that
@@ -8,9 +8,9 @@
8
8
  // WHY NOT DECODE THE PROJECT SLUG. Claude files transcripts under
9
9
  // `~/.claude/projects/<slug>/<id>.jsonl` where the slug is the cwd with separators
10
10
  // replaced. Decoding it back is LOSSY and wrong on this very machine:
11
- // slug -Users-ukaoma-Documents-GitHub-Ukaoma-Chief-Of-Staff-MU-Chief-Staff
12
- // naive /Users/ukaoma/Documents/GitHub/Ukaoma/Chief/Of/Staff/MU/Chief/Staff
13
- // real /Users/ukaoma/Documents/GitHub/Ukaoma Chief Of Staff/MU-Chief-Staff
11
+ // slug -Users-example-Documents-GitHub-Example-Chief-Of-Staff-MU-Chief-Staff
12
+ // naive /Users/example/Documents/GitHub/Example/Chief/Of/Staff/MU/Chief/Staff
13
+ // real /Users/example/Documents/GitHub/Example Chief Of Staff/MU-Chief-Staff
14
14
  // The real path contains spaces AND hyphens, so the mapping is not invertible.
15
15
  // Resuming in the wrong cwd is not a harmless error: Claude associates a session
16
16
  // with its project, so a mismatched cwd risks writing a NEW session instead of
@@ -38,7 +38,7 @@ export const EXPECTED_EMBEDDING_DIM = 192
38
38
  * non-invertible timbre vectors, so the privacy argument that motivates a short
39
39
  * audio window does not carry across unchanged.
40
40
  *
41
- * Flagged for Miles rather than assumed: if he wants these on the 8-hour clock
41
+ * Flagged for the user rather than assumed: if he wants these on the 8-hour clock
42
42
  * too, set the env var to a fraction and the correction loop becomes same-day
43
43
  * only.
44
44
  */
@@ -97,7 +97,7 @@ const NOTE_EXTENSIONS = ['.md', '.markdown', '.txt']
97
97
  *
98
98
  * SYMLINKS ARE FIRST-CLASS, and that is a correction. A `Dirent` reports a symlink
99
99
  * as `isSymbolicLink()`, NOT as `isDirectory()` or `isFile()`, so the first version
100
- * of this walk silently skipped every linked file and linked subfolder. Queen hit
100
+ * of this walk silently skipped every linked file and linked subfolder. Sam hit
101
101
  * exactly that on 2026-08-09: she wired `operations/memory` at her real note store
102
102
  * with a symlink, which worked only because `findDir` uses `statSync` and follows
103
103
  * links — anything linked one level deeper would have vanished with no error.
@@ -2,7 +2,7 @@
2
2
  // one id space, disjoint prefixes — so a reference is never ambiguous about which
3
3
  // one it means. `.` is permitted because a file id carries its extension; it
4
4
  // cannot traverse, since `/` is excluded and a bare `..` cannot match the prefix.
5
- export const MEMORY_ID_PATTERN = /^(?:mem|file)_[A-Za-z0-9._:-]{1,120}$/
5
+ export const MEMORY_ID_PATTERN = /^(?:mem|file|point)_[A-Za-z0-9._:-]{1,160}$/
6
6
  export const THREAD_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
7
7
 
8
8
  const CONTROL_CHARS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g
@@ -33,7 +33,7 @@ const UNC_PATH = /\\\\[A-Za-z0-9._-]+\\(?:[^\\\r\n]+\\?)*/g
33
33
  /**
34
34
  * Trim a greedy path match back to its last plausible component.
35
35
  *
36
- * Keeps `…/Ukaoma Chief Of Staff/MU/ops/comp.md` whole (every space is followed by
36
+ * Keeps `…/Example Chief Of Staff/MU/ops/comp.md` whole (every space is followed by
37
37
  * more path) while not swallowing the trailing prose in `/Users/me/x.md here`.
38
38
  * A component qualifies if it is followed by a `/` or contains a dot.
39
39
  */
@@ -208,7 +208,7 @@ export function normalizeMemoryList(value: unknown, limit: number): MemoryListIt
208
208
  for (const row of value.slice(0, Math.max(0, Math.min(limit, 50)))) {
209
209
  if (!row || typeof row !== 'object' || Array.isArray(row)) continue
210
210
  const source = row as Record<string, unknown>
211
- const id = cleanContextText(source.id, 128)
211
+ const id = cleanContextText(source.id, 170)
212
212
  if (!MEMORY_ID_PATTERN.test(id)) continue
213
213
  result.push({
214
214
  id,
@@ -242,7 +242,7 @@ export function normalizeMemoryDetail(value: unknown): (MemoryListItem & {
242
242
  function normalizeThread(value: unknown, detail: boolean): ThreadListItem | null {
243
243
  if (!value || typeof value !== 'object' || Array.isArray(value)) return null
244
244
  const source = value as Record<string, unknown>
245
- const id = cleanContextText(source.id, 128)
245
+ const id = cleanContextText(source.id, 170)
246
246
  if (!THREAD_ID_PATTERN.test(id)) return null
247
247
  const rawMeetings = Array.isArray(source.meetings) && source.meetings.length
248
248
  ? source.meetings
@@ -663,6 +663,7 @@ export function normalizeGraphStatus(value: unknown): Record<string, unknown> {
663
663
  const processor = asRecord(source.processor) ?? {}
664
664
  const indexState = stringOrAbsent(source.index_state, 32)
665
665
  return {
666
+ engine: stringOrAbsent(source.engine, 48) ?? 'lightrag',
666
667
  entities: integerOrAbsent(source.entities) ?? null,
667
668
  relationships: integerOrAbsent(source.relationships) ?? null,
668
669
  source_updated_at: isoOrAbsent(source.source_updated_at) ?? null,
@@ -913,7 +914,16 @@ export function normalizeExtractionBlock(value: unknown): Record<string, unknown
913
914
  const r = asRecord(row); const id = r ? stringOrAbsent(r.id, 16) : undefined
914
915
  return id ? [{ id, label: stringOrAbsent(r!.label, 40) ?? id, detail: stringOrAbsent(r!.detail, 200) ?? '', selected: r!.selected === true }] : []
915
916
  })
916
- return { tier: stringOrAbsent(x.tier, 16) ?? null, label: stringOrAbsent(x.label, 40) ?? null, detail: stringOrAbsent(x.detail, 200) ?? null, tiers }
917
+ const run = asRecord(x.execution)
918
+ const execution = run ? {
919
+ provider: stringOrAbsent(run.provider, 40) ?? null,
920
+ requested_model: stringOrAbsent(run.requested_model, 120) ?? null,
921
+ reasoning_effort: stringOrAbsent(run.reasoning_effort, 20) ?? null,
922
+ actual_identity: stringOrAbsent(run.actual_identity, 120) ?? 'unverified',
923
+ state: stringOrAbsent(run.state, 40) ?? 'unknown',
924
+ error: stringOrAbsent(run.error, 160) ?? null,
925
+ } : null
926
+ return { tier: stringOrAbsent(x.tier, 16) ?? null, label: stringOrAbsent(x.label, 40) ?? null, detail: stringOrAbsent(x.detail, 200) ?? null, tiers, execution }
917
927
  }
918
928
  export interface KnowledgeSetupSource { path: string; enabled: boolean; exists: boolean; files: number | null; added_at: string | null }
919
929
 
@@ -1032,7 +1042,8 @@ export function normalizeGuardrails(value: unknown): Record<string, unknown> | n
1032
1042
  max_repeat_ratio: typeof g.max_repeat_ratio === 'number' && Number.isFinite(g.max_repeat_ratio) ? g.max_repeat_ratio : 0.5,
1033
1043
  banned_patterns: (Array.isArray(g.banned_patterns) ? g.banned_patterns : []).filter((p): p is string => typeof p === 'string').slice(0, 50).map(p => p.slice(0, 200)),
1034
1044
  llm_review: {
1035
- enabled: llm.enabled === true,
1045
+ ...(typeof llm.supported === 'boolean' ? { supported: llm.supported } : {}),
1046
+ enabled: llm.supported !== false && llm.enabled === true,
1036
1047
  model: (GUARDRAIL_LLM_TIERS as readonly string[]).includes(String(llm.model)) ? String(llm.model) : 'haiku',
1037
1048
  max_per_run: integerOrAbsent(llm.max_per_run) ?? 20,
1038
1049
  },
@@ -19,6 +19,7 @@ import { discoveredDomains, domainAbbreviation as deriveAbbr, isSafeDomainName a
19
19
  import type { MeetingDetail, MeetingMeta } from './meeting-store.js'
20
20
  import { MEETING_SOURCE_MAX_BYTES, meetingDayCountsFromNames, meetingListLimit } from './meeting-store.js'
21
21
  import { meetingVoiceReview, parseSidecarListHead } from './meeting-voice-review.js'
22
+ import { dataPath } from './data-dir.js'
22
23
 
23
24
  /**
24
25
  * The four domains of ONE user's COS. Retained as the documented example layout
@@ -528,9 +529,12 @@ export function listCosOperationsMeetings(options: {
528
529
  domain?: string
529
530
  month?: string
530
531
  day?: string
532
+ /** The server's own recordings root; tests point it at a temp dir. */
533
+ recordingsRoot?: string
531
534
  } = {}): CosOperationsMeetingMeta[] {
532
535
  const operationsDir = resolveCosOperationsDir()
533
536
  if (!operationsDir) return []
537
+ const recordingsRoot = options.recordingsRoot ?? dataPath('recordings')
534
538
 
535
539
  const scoped = Boolean(options.month || options.day)
536
540
  const limit = meetingListLimit(options.limit, scoped)
@@ -569,7 +573,17 @@ export function listCosOperationsMeetings(options: {
569
573
  meta.recordId = `ops:${domain}:${month}:${file}`
570
574
  meta.mutable = true
571
575
  meta.canonicalRecord = `operations/${domain}/meetings/${month}/${file}`
572
- const hints = sidecarListHints(monthDir, file)
576
+ // The sidecar is gitignored in the operations tree, so a G2 recording
577
+ // whose markdown arrived through git or iCloud (three of the user's KC-week
578
+ // recordings, 2026-09-08) sits there without one and the row loses its
579
+ // session id, which reads as "cannot be reviewed". The server that saved
580
+ // it still holds the sidecar under its own recordings root by the same
581
+ // stem; read that one when the copy beside the markdown is missing.
582
+ let hints = sidecarListHints(monthDir, file)
583
+ if (!hints.sessionId && recordingsRoot) {
584
+ const fallbackDir = join(recordingsRoot, month)
585
+ if (existsSync(fallbackDir)) hints = sidecarListHints(fallbackDir, file)
586
+ }
573
587
  if (hints.sessionId) meta.sessionId = hints.sessionId
574
588
  if (hints.speakers.length > 0) {
575
589
  meta.voiceReview = meetingVoiceReview(hints.speakers, hints.sessionId)
@@ -160,7 +160,7 @@ export function discoveredTaskDomains(operationsDir: string | null): string[] {
160
160
  /**
161
161
  * Task domains: configured first, then any directory with a `tasks.md`.
162
162
  *
163
- * Same union rule as `resolveDomains` and for the same reason — Miles has no
163
+ * Same union rule as `resolveDomains` and for the same reason — the user has no
164
164
  * `domains` in his profile, discovery finds his four, and the union is exactly
165
165
  * his four, so switching the task store onto this changes nothing about his
166
166
  * install while removing one user's business units from everyone else's build.
@@ -182,7 +182,7 @@ export function taskDomainNames(operationsDir: string | null): string[] {
182
182
  * ROUTED before any folder exists.
183
183
  * - On disk but not configured: still listed, so a folder someone made by hand
184
184
  * never becomes invisible. This is also what protects an existing install —
185
- * Miles has no `domains` in his profile, discovery finds his four, and the
185
+ * the user has no `domains` in his profile, discovery finds his four, and the
186
186
  * union is exactly his four. Nothing is written to his profile and nothing
187
187
  * about his setup changes.
188
188
  * - Neither: the DEFAULTS. Only a genuinely fresh COS ever sees them, which is
@@ -60,7 +60,7 @@ export type MaintenanceWorkKind =
60
60
  // Two `task_dispatch` leases (dispatch + reconcile) are expected —
61
61
  // acquire() is a concurrent Map, not a per-kind mutex.
62
62
  | 'task_dispatch'
63
- // Miles-triggered recovery of a quarantined unsaved capture (6.19.0):
63
+ // the user-triggered recovery of a quarantined unsaved capture (6.19.0):
64
64
  // batch-transcribes retained WAVs into a durable scribe. Held for the whole
65
65
  // background run so an Update Server drain waits for it like any batch.
66
66
  // Long runs are made visible: the active-recovery registry renders a
@@ -6,7 +6,7 @@
6
6
  // review panel could show a phrase but never let anyone HEAR it, and its own
7
7
  // copy said "naming this needs its audio, which is no longer held."
8
8
  //
9
- // Miles's decision: keep a week, so review can happen on a weekend rather than
9
+ // the user's decision: keep a week, so review can happen on a weekend rather than
10
10
  // only within hours of the meeting, and stay under 8 GB.
11
11
  //
12
12
  // SIZING IS MEASURED, NOT GUESSED. Real recording volume over the 14 days to
@@ -30,14 +30,14 @@ import { dataPath } from './data-dir.js'
30
30
 
31
31
  export const MEETING_AUDIO_DIR = 'meeting-audio'
32
32
 
33
- /** One week, per Miles: review should survive until a weekend. */
33
+ /** One week, per the user: review should survive until a weekend. */
34
34
  export function meetingAudioTtlMs(): number {
35
35
  const raw = Number(process.env.COS_MEETING_AUDIO_RETENTION_DAYS)
36
36
  const days = Number.isFinite(raw) && raw > 0 ? raw : 7
37
37
  return days * 24 * 60 * 60 * 1000
38
38
  }
39
39
 
40
- /** Total budget for retained meeting audio. Miles: stay under 8 GB. */
40
+ /** Total budget for retained meeting audio. the user: stay under 8 GB. */
41
41
  export function meetingAudioMaxBytes(): number {
42
42
  const raw = Number(process.env.COS_MEETING_AUDIO_MAX_BYTES)
43
43
  return Number.isFinite(raw) && raw > 0 ? raw : 8 * 1024 * 1024 * 1024
@@ -19,8 +19,8 @@
19
19
  // WHAT IS DELIBERATELY NOT CORRECTED. The meeting markdown's Summary, Topics,
20
20
  // Decisions and Action Items are LLM prose that refers to people by BARE FIRST
21
21
  // NAME ("Jeremy pushed back", "Chris raised Beamer sentiment"). Verified on a
22
- // real scribe: 6 of 12 speakers appear that way. This org has two Kyles, two
23
- // Jacobuses and two Chrises, so a find/replace on a first name in narrative text
22
+ // real scribe: 6 of 12 speakers appear that way. This org has two Alexes, two
23
+ // Jordans and two Chrises, so a find/replace on a first name in narrative text
24
24
  // would silently rewrite a sentence about a different person. Prose is left
25
25
  // alone and flagged stale instead — `proseStale` on the applied row.
26
26
 
@@ -41,7 +41,7 @@ export type CorrectionPhase = 'intent' | 'applied' | 'failed' | 'confirmed'
41
41
  * demoted.
42
42
  *
43
43
  * Distinct from a rename, and it has to be: `relabelSidecarJson` rejects
44
- * `from === to`, so "yes, this really is Queen Ukaoma" cannot be expressed as a
44
+ * `from === to`, so "yes, this really is Sam Example" cannot be expressed as a
45
45
  * correction at all. The floor exists because a 0.56 match is not evidence — but
46
46
  * a person who was in the room IS evidence, and there was no way to record it.
47
47
  * The panel demoted the row, told the reviewer to name it, and then offered a
@@ -29,7 +29,7 @@
29
29
  //
30
30
  // Consequence, measured on `meeting_1786628481833_eagkaz`: naming one voice
31
31
  // enrolled 73 of 103 rows belonging to OTHER PEOPLE — 22 chunks of MU (the device
32
- // owner), 16 Vikas, 10 Vishnu, 8 Niranjan, 7 Anil, 6 Chris, 3 Manoj, 1 Navaz, and
32
+ // owner), 16 Riley, 10 Taylor, 8 Casey, 7 Morgan, 6 Chris, 3 Jordan, 1 Jamie, and
33
33
  // only 30 that were actually the named voice. It reported SUCCESS, because rows do
34
34
  // come back from the store; they were simply the wrong rows.
35
35
  //
@@ -8,7 +8,7 @@
8
8
  // (2026-08-06 Health Score V2): 135 sidecar chunks collapse to 46 speaker runs,
9
9
  // while the markdown carries 70 turns — and they disagree on who spoke, with the
10
10
  // markdown's 4th turn attributed to Joe Karbowski where the sidecar's 4th run
11
- // says Richard Jenkins. The transcript was rendered from a different
11
+ // says Riley Example. The transcript was rendered from a different
12
12
  // segmentation pass. So:
13
13
  //
14
14
  // * There is NO mapping from a chunk index to a transcript turn.
@@ -22,7 +22,7 @@
22
22
  //
23
23
  // AND WHAT IS NEVER REWRITTEN: the Summary / Topics / Decisions / Action Items
24
24
  // prose. It refers to people by bare first name — 6 of 12 speakers did so on the
25
- // meeting above — and this org has two Kyles, two Jacobuses and two Chrises, so
25
+ // meeting above — and this org has two Alexes, two Jordans and two Chrises, so
26
26
  // a first-name substitution in narrative text would rewrite sentences about
27
27
  // somebody else. Reported as `proseStale` instead.
28
28
 
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // v1 floored the attendee block and shipped every other section verbatim. On the
6
6
  // 2026-08-06 IJO Post-Mortem — where the review asserts exactly ONE voice — all
7
- // 14 unasserted names still reached the clipboard, including one Miles had
7
+ // 14 unasserted names still reached the clipboard, including one the user had
8
8
  // already confirmed was never in the room, as `[Name]:` transcript labels. The
9
9
  // stated contract, "only asserted voices are named", was false for the largest
10
10
  // output.
@@ -69,7 +69,7 @@ export interface ParsedScribe {
69
69
  * Sections that are none of the above, in document order.
70
70
  *
71
71
  * v1 discarded these, losing 116,820 characters across the corpus including
72
- * `Granola Structured Notes (canonical)` — Miles's own write-up — and
72
+ * `Granola Structured Notes (canonical)` — the user's own write-up — and
73
73
  * `Fathom Action Items (with exact timestamps)`. "Copy the meeting" has to
74
74
  * copy the meeting.
75
75
  */
@@ -322,7 +322,7 @@ export interface AttendeeRenderOptions {
322
322
  *
323
323
  * Gating on `voicedMs` instead tripped on 16 of 23 real meetings, because any
324
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
325
+ * a signal and becomes boilerplate on something the user pastes into Slack. Rows
326
326
  * adding to more than the meeting LENGTH is the genuinely confusing case (71m
327
327
  * of rows inside a 66-minute meeting), and that is ~10% of meetings.
328
328
  */
@@ -380,10 +380,10 @@ export function renderAttendees(voices: AttendeeLine[], o: AttendeeRenderOptions
380
380
  * written sentence mangles grammar and can hit the wrong person. So the applied
381
381
  * correction row records `proseStale: true` and, until now, NOTHING read it.
382
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
383
+ * Regression example (synthetic names): the user removed "Morgan Example" from a personal call that
384
+ * had only two participants (a third participant's voice matched a similar profile). All 8
385
+ * label sites were rewritten correctly; the LLM summary still opened "the user,
386
+ * Sam, and Morgan talk through..." and the payload said nothing. An allowlist
387
387
  * ("this name was not confirmed") is far too weak here — he did not fail to
388
388
  * confirm this person, he explicitly said they were not in the room.
389
389
  */
@@ -54,7 +54,7 @@ export const UNATTRIBUTED = new Set(['Unknown', 'Ext', '', 'Speaker 1', 'Speaker
54
54
  * distinct.
55
55
  *
56
56
  * De-attributing to a single shared `Ext` folded every corrected voice into one
57
- * row: on the 2026-08-06 Ditto meeting Miles named five wrong attributions, and
57
+ * row: on the 2026-08-06 Ditto meeting the user named five wrong attributions, and
58
58
  * collapsing them would have destroyed his ability to tell those five voices
59
59
  * apart afterwards — which is exactly what he then needs playback for. Numbering
60
60
  * keeps them separable while asserting no identity.
@@ -80,9 +80,9 @@ export const CONFIDENT_SIMILARITY = 0.65
80
80
  *
81
81
  * The identifier accepts a match at SEARCH_THRESHOLD = 0.55, so a single segment
82
82
  * scoring 0.55 currently arrives in the panel wearing somebody's full name. On
83
- * Miles's 2026-08-06 Ditto meeting that produced Richard Jenkins (1 segment,
83
+ * the user's 2026-08-06 Ditto meeting that produced Riley Example (1 segment,
84
84
  * 0.60), Luke Henry (1 segment, 0.55), Dylan Jackson (2 segments, 0.58) and
85
- * Navaz Sharif (3 segments, 0.58) — and he confirmed none of them were in the
85
+ * Jamie Sharif (3 segments, 0.58) — and he confirmed none of them were in the
86
86
  * room. Presenting those as names is the defect; the reviewer then has to undo
87
87
  * an assertion the system should never have made.
88
88
  *