@gotcos/glasses-server 6.79.2 → 6.81.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 (70) hide show
  1. package/.env.example +9 -0
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +10 -0
  4. package/managed-runtime-contract.json +1 -0
  5. package/package.json +10 -1
  6. package/server/index.ts +14 -1
  7. package/server/lib/agent-session-search.ts +8 -5
  8. package/server/lib/agent-session-store.ts +45 -4
  9. package/server/lib/archive.ts +2 -1
  10. package/server/lib/attached-provider-adapter.ts +11 -0
  11. package/server/lib/claude-bridge.ts +112 -8
  12. package/server/lib/claude-settings-guard.ts +94 -0
  13. package/server/lib/claude-steer.ts +27 -0
  14. package/server/lib/codex-bridge.ts +205 -32
  15. package/server/lib/codex-engine-sessions.ts +27 -6
  16. package/server/lib/codex-homes.ts +134 -0
  17. package/server/lib/codex-live-queue.ts +14 -0
  18. package/server/lib/codex-profile-home.ts +452 -0
  19. package/server/lib/codex-queue-control.ts +17 -1
  20. package/server/lib/codex-subagent-activity.ts +22 -0
  21. package/server/lib/conversation.ts +92 -15
  22. package/server/lib/direct-bridge.ts +853 -0
  23. package/server/lib/direct-sse.ts +262 -0
  24. package/server/lib/fork-thread.ts +11 -0
  25. package/server/lib/key-answer-fields.ts +49 -0
  26. package/server/lib/key-catalog.ts +301 -0
  27. package/server/lib/key-lanes-pricing.ts +12 -0
  28. package/server/lib/key-lanes-wiring.ts +346 -0
  29. package/server/lib/key-lanes.ts +145 -0
  30. package/server/lib/key-ledger.ts +123 -0
  31. package/server/lib/key-notices.ts +486 -0
  32. package/server/lib/key-profile-rejects.ts +25 -0
  33. package/server/lib/key-profile-routing.ts +173 -0
  34. package/server/lib/key-profiles.ts +182 -0
  35. package/server/lib/key-relay-contract.ts +262 -0
  36. package/server/lib/key-relay-errors.ts +211 -0
  37. package/server/lib/key-relay-meter.ts +303 -0
  38. package/server/lib/key-relay.ts +1018 -0
  39. package/server/lib/key-secrets-guard.ts +52 -0
  40. package/server/lib/key-store.ts +228 -0
  41. package/server/lib/keyed-sessions.ts +154 -0
  42. package/server/lib/lane-b-env.ts +169 -0
  43. package/server/lib/meeting-autonomous-finalize.ts +267 -0
  44. package/server/lib/meeting-stop-intents.ts +277 -0
  45. package/server/lib/model-router.ts +96 -7
  46. package/server/lib/morning-brief-config.ts +3 -0
  47. package/server/lib/native-head.ts +13 -1
  48. package/server/lib/occupancy-probes.ts +4 -0
  49. package/server/lib/ollama-run-ledger.ts +96 -13
  50. package/server/lib/prompt-edit.ts +2 -0
  51. package/server/lib/query-job-coordinator.ts +2 -1
  52. package/server/lib/query-job-runtime.ts +67 -15
  53. package/server/lib/query-job-store.ts +7 -1
  54. package/server/lib/query-job-types.ts +15 -1
  55. package/server/lib/session-hook-events.ts +28 -0
  56. package/server/lib/session-hook-ledger.ts +10 -0
  57. package/server/lib/thread-occupancy.ts +22 -1
  58. package/server/lib/token-audit.ts +18 -4
  59. package/server/lib/work-handoff-requests.ts +3 -0
  60. package/server/routes/agent-session-bindings.ts +15 -0
  61. package/server/routes/agent-sessions.ts +17 -5
  62. package/server/routes/handoffs.ts +22 -1
  63. package/server/routes/health.ts +5 -0
  64. package/server/routes/key-lanes.ts +442 -0
  65. package/server/routes/meeting.ts +326 -47
  66. package/server/routes/openai-compat.ts +28 -3
  67. package/server/routes/query.ts +20 -6
  68. package/server/routes/transcribe-stream.ts +48 -1
  69. package/shared/key-preference.ts +57 -0
  70. package/shared/model-preference.ts +20 -2
@@ -1,5 +1,6 @@
1
1
  import { MacParakeetUnavailableError, transcribeMacParakeet } from '../lib/mac-recording-asr.js'
2
2
  import { getMeetingStore } from '../lib/meeting-store.js'
3
+ import { appendAutonomousSaveLog, getStopIntentStore, savedChunkReplay, type StopIntent } from '../lib/meeting-stop-intents.js'
3
4
  import { MAC_AUDIO, MAC_CAPTURE_ID, persistMacChunkReceipt, macError, registerMacRecording, requireMacRecording, readMacRecording, validateMacWav } from '../lib/mac-recording.js'
4
5
  import { AUTO_ENROLL_THRESHOLD } from '../lib/voiceprint-floor.js'
5
6
  import { readSpeakerPolicy, policyLabel, type SpeakerMode } from '../lib/meeting-speaker-policy.js'
@@ -2164,6 +2165,47 @@ interface StreamCandidateInput {
2164
2165
  mode?: string
2165
2166
  }
2166
2167
 
2168
+ /**
2169
+ * 6.81.0: the phone re-sends a chunk after the server saved its meeting on its own.
2170
+ *
2171
+ * WHY. The server saves a stopped meeting itself only once it holds every chunk, so any chunk
2172
+ * the phone still lists as unconfirmed is one whose upload reached the Mac and whose answer
2173
+ * was lost while the phone was off screen. Before 6.81.0 such a replay was refused 410 and the
2174
+ * phone, holding a closed meeting with "unconfirmed" audio, called it a collision and kept the
2175
+ * audio forever. The saved meeting already holds that chunk, so it answers with the saved
2176
+ * text, exactly as a live session answers a replayed index, and the phone adopts the receipt.
2177
+ *
2178
+ * ONLY for meetings the SERVER saved after a Stop: every other closed session answers 410 as
2179
+ * before. A chunk the saved meeting never held is still refused, and is logged loudly, because
2180
+ * the finalizer's rule (never save an incomplete capture) says it cannot happen.
2181
+ */
2182
+ function replayAfterAutonomousSave(sessionId: string, chunkIndex: number): StreamChunkCompletionResponse | null {
2183
+ let intent: StopIntent | null
2184
+ try { intent = getStopIntentStore().get(sessionId) } catch { return null }
2185
+ if (!intent || intent.state !== 'saved' || intent.savedBy !== 'server' || !intent.sidecarPath) return null
2186
+ const closed = closedSessionRecords.get(sessionId)
2187
+ const replay = savedChunkReplay(intent.sidecarPath, chunkIndex, closed?.asrCompletedIndices ?? [])
2188
+ const store = getStopIntentStore()
2189
+ if (!replay) {
2190
+ store.update(sessionId, { lateRefused: (intent.lateRefused ?? 0) + 1 })
2191
+ const fields = { sessionId, chunkIndex, finalChunkIndex: intent.finalChunkIndex, lateRefused: (intent.lateRefused ?? 0) + 1 }
2192
+ console.warn(`[meeting/auto-save] late_chunk_refused ${sessionId} ${JSON.stringify(fields)}`)
2193
+ appendAutonomousSaveLog('late_chunk_refused', fields)
2194
+ return null
2195
+ }
2196
+ const at = Date.now()
2197
+ store.update(sessionId, {
2198
+ lateReplays: (intent.lateReplays ?? 0) + 1,
2199
+ ...(intent.phoneConfirmedAt ? {} : { phoneConfirmedAt: at, phoneConfirmedBy: 'chunk_replay' }),
2200
+ })
2201
+ const fields = { sessionId, chunkIndex, kind: replay.kind, lateReplays: (intent.lateReplays ?? 0) + 1, sinceSaveMs: at - (intent.savedAt ?? at) }
2202
+ console.log(`[meeting/auto-save] late_replay ${sessionId} ${JSON.stringify(fields)}`)
2203
+ appendAutonomousSaveLog('late_replay', fields)
2204
+ if (!intent.phoneConfirmedAt) appendAutonomousSaveLog('phone_confirmed', { sessionId, via: 'chunk_replay', sinceSaveMs: at - (intent.savedAt ?? at) })
2205
+ if (replay.kind === 'canonical') return canonicalChunkResponse(replay.chunk as unknown as TranscriptChunk, sessionId, chunkIndex)
2206
+ return emptyChunkResponse({ text: '', speaker: 'Ext', elapsed: 0, canonical: false }, sessionId, chunkIndex)
2207
+ }
2208
+
2167
2209
  async function processStreamChunk(opts: {
2168
2210
  sessionId: string
2169
2211
  chunkIndex: number
@@ -2182,7 +2224,12 @@ async function processStreamChunk(opts: {
2182
2224
  const tReq = performance.now()
2183
2225
  validateSessionId(sessionId)
2184
2226
  validateChunkIndex(chunkIndex)
2185
- if (isSessionDeleted(sessionId)) throw makeHttpError(410, 'session is closed', 'session_closed')
2227
+ if (isSessionDeleted(sessionId)) {
2228
+ // 6.81.0: a meeting the server saved on its own answers a chunk it already holds.
2229
+ const replay = replayAfterAutonomousSave(sessionId, chunkIndex)
2230
+ if (replay) return replay
2231
+ throw makeHttpError(410, 'session is closed', 'session_closed')
2232
+ }
2186
2233
  if (audioBuffer.length < 100) throw makeHttpError(400, 'audio too short', 'audio_too_short')
2187
2234
 
2188
2235
  const audioSha256 = sha256Hex(audioBuffer)
@@ -0,0 +1,57 @@
1
+ // Key lanes (K1, plan v4 section 5.1): the ONE parser that may read a key profile out of a model
2
+ // preference. `isModelPreference` and `normalizeModelPreference` (model-preference.ts) stay
3
+ // closed and unchanged, so every site that does not call parsePreference keeps refusing a
4
+ // profile exactly as 6.79.0 does. Call this only at the sites plan 5.2 marks Accept.
5
+ //
6
+ // Encoding: a profile-bound preference is `profile:<id>`. The prefix keeps a profile id from ever
7
+ // colliding with a model slot name (`opus` is a valid id shape). On `/v1` a profile is offered as
8
+ // `cos-key-<id>` (plan 9.4), handled before `resolveModel` and only for profiles that opt in.
9
+ import { normalizeModelPreference, type ModelPreference } from './model-preference.js'
10
+
11
+ export const KEY_PROFILE_ID_RE = /^[a-z0-9][a-z0-9-]{0,31}$/
12
+ export const KEY_PROFILE_PREFERENCE_PREFIX = 'profile:'
13
+ export const V1_KEY_MODEL_PREFIX = 'cos-key-'
14
+
15
+ export type ParsedPreference =
16
+ | { kind: 'model'; value: ModelPreference }
17
+ | { kind: 'profile'; id: string }
18
+
19
+ export function isKeyProfileId(value: unknown): value is string {
20
+ return typeof value === 'string' && KEY_PROFILE_ID_RE.test(value)
21
+ }
22
+
23
+ /** `profile:<id>`, or throws on a malformed id (a caller bug, never user input). */
24
+ export function profilePreference(id: string): string {
25
+ if (!isKeyProfileId(id)) throw new Error('invalid key profile id')
26
+ return KEY_PROFILE_PREFERENCE_PREFIX + id
27
+ }
28
+
29
+ /**
30
+ * A model slot (normalized exactly as normalizeModelPreference does, so `codex-high` still
31
+ * migrates), a `profile:<id>`, or undefined for anything else. Never throws.
32
+ */
33
+ export function parsePreference(value: unknown): ParsedPreference | undefined {
34
+ const model = normalizeModelPreference(value)
35
+ if (model) return { kind: 'model', value: model }
36
+ if (typeof value !== 'string' || !value.startsWith(KEY_PROFILE_PREFERENCE_PREFIX)) return undefined
37
+ const id = value.slice(KEY_PROFILE_PREFERENCE_PREFIX.length)
38
+ return isKeyProfileId(id) ? { kind: 'profile', id } : undefined
39
+ }
40
+
41
+ /** True only for a well-formed `profile:<id>`. A model slot is false. */
42
+ export function isProfilePreference(value: unknown): boolean {
43
+ return parsePreference(value)?.kind === 'profile'
44
+ }
45
+
46
+ /** `/v1` model name for a profile (plan 9.4). */
47
+ export function v1KeyModelName(id: string): string {
48
+ if (!isKeyProfileId(id)) throw new Error('invalid key profile id')
49
+ return V1_KEY_MODEL_PREFIX + id
50
+ }
51
+
52
+ /** The profile id in a `cos-key-<id>` `/v1` model name, or undefined. */
53
+ export function parseV1KeyModelName(value: unknown): string | undefined {
54
+ if (typeof value !== 'string' || !value.startsWith(V1_KEY_MODEL_PREFIX)) return undefined
55
+ const id = value.slice(V1_KEY_MODEL_PREFIX.length)
56
+ return isKeyProfileId(id) ? id : undefined
57
+ }
@@ -208,7 +208,23 @@ export function resetRuntimeCursorModelLabels(): void {
208
208
  delete runtimeCursorLabels[CURSOR_COMPOSER_MODEL]
209
209
  }
210
210
 
211
- export function modelLabel(model: ModelPreference): string {
211
+ /**
212
+ * Key lanes K1 (plan 5.2 labels): a caller that renders a key-profile turn passes the profile
213
+ * record, and its label wins. With no profile passed every label below is the 6.79.0 one,
214
+ * including the `'Opus'` default. `isModelPreference` stays closed: a profile is never a slot.
215
+ */
216
+ export interface ModelLabelProfile {
217
+ label: string
218
+ }
219
+
220
+ function profileLabel(profile: ModelLabelProfile | undefined, max: number): string | undefined {
221
+ const label = typeof profile?.label === 'string' ? profile.label.trim() : ''
222
+ return label ? Array.from(label).slice(0, max).join('') : undefined
223
+ }
224
+
225
+ export function modelLabel(model: ModelPreference, profile?: ModelLabelProfile): string {
226
+ const fromProfile = profileLabel(profile, 40)
227
+ if (fromProfile) return fromProfile
212
228
  switch (model) {
213
229
  case 'fable': return 'Fable'
214
230
  case 'sonnet': return 'Sonnet'
@@ -224,7 +240,9 @@ export function modelLabel(model: ModelPreference): string {
224
240
  }
225
241
  }
226
242
 
227
- export function modelShortLabel(model: ModelPreference): string {
243
+ export function modelShortLabel(model: ModelPreference, profile?: ModelLabelProfile): string {
244
+ const fromProfile = profileLabel(profile, 12)
245
+ if (fromProfile) return fromProfile
228
246
  switch (model) {
229
247
  case 'fable': return 'Fable'
230
248
  case 'sonnet': return 'Sonnet'