@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.
- package/.env.example +9 -0
- package/CHANGELOG.md +31 -0
- package/README.md +10 -0
- package/managed-runtime-contract.json +1 -0
- package/package.json +10 -1
- package/server/index.ts +14 -1
- package/server/lib/agent-session-search.ts +8 -5
- package/server/lib/agent-session-store.ts +45 -4
- package/server/lib/archive.ts +2 -1
- package/server/lib/attached-provider-adapter.ts +11 -0
- package/server/lib/claude-bridge.ts +112 -8
- package/server/lib/claude-settings-guard.ts +94 -0
- package/server/lib/claude-steer.ts +27 -0
- package/server/lib/codex-bridge.ts +205 -32
- package/server/lib/codex-engine-sessions.ts +27 -6
- package/server/lib/codex-homes.ts +134 -0
- package/server/lib/codex-live-queue.ts +14 -0
- package/server/lib/codex-profile-home.ts +452 -0
- package/server/lib/codex-queue-control.ts +17 -1
- package/server/lib/codex-subagent-activity.ts +22 -0
- package/server/lib/conversation.ts +92 -15
- package/server/lib/direct-bridge.ts +853 -0
- package/server/lib/direct-sse.ts +262 -0
- package/server/lib/fork-thread.ts +11 -0
- package/server/lib/key-answer-fields.ts +49 -0
- package/server/lib/key-catalog.ts +301 -0
- package/server/lib/key-lanes-pricing.ts +12 -0
- package/server/lib/key-lanes-wiring.ts +346 -0
- package/server/lib/key-lanes.ts +145 -0
- package/server/lib/key-ledger.ts +123 -0
- package/server/lib/key-notices.ts +486 -0
- package/server/lib/key-profile-rejects.ts +25 -0
- package/server/lib/key-profile-routing.ts +173 -0
- package/server/lib/key-profiles.ts +182 -0
- package/server/lib/key-relay-contract.ts +262 -0
- package/server/lib/key-relay-errors.ts +211 -0
- package/server/lib/key-relay-meter.ts +303 -0
- package/server/lib/key-relay.ts +1018 -0
- package/server/lib/key-secrets-guard.ts +52 -0
- package/server/lib/key-store.ts +228 -0
- package/server/lib/keyed-sessions.ts +154 -0
- package/server/lib/lane-b-env.ts +169 -0
- package/server/lib/meeting-autonomous-finalize.ts +267 -0
- package/server/lib/meeting-stop-intents.ts +277 -0
- package/server/lib/model-router.ts +96 -7
- package/server/lib/morning-brief-config.ts +3 -0
- package/server/lib/native-head.ts +13 -1
- package/server/lib/occupancy-probes.ts +4 -0
- package/server/lib/ollama-run-ledger.ts +96 -13
- package/server/lib/prompt-edit.ts +2 -0
- package/server/lib/query-job-coordinator.ts +2 -1
- package/server/lib/query-job-runtime.ts +67 -15
- package/server/lib/query-job-store.ts +7 -1
- package/server/lib/query-job-types.ts +15 -1
- package/server/lib/session-hook-events.ts +28 -0
- package/server/lib/session-hook-ledger.ts +10 -0
- package/server/lib/thread-occupancy.ts +22 -1
- package/server/lib/token-audit.ts +18 -4
- package/server/lib/work-handoff-requests.ts +3 -0
- package/server/routes/agent-session-bindings.ts +15 -0
- package/server/routes/agent-sessions.ts +17 -5
- package/server/routes/handoffs.ts +22 -1
- package/server/routes/health.ts +5 -0
- package/server/routes/key-lanes.ts +442 -0
- package/server/routes/meeting.ts +326 -47
- package/server/routes/openai-compat.ts +28 -3
- package/server/routes/query.ts +20 -6
- package/server/routes/transcribe-stream.ts +48 -1
- package/shared/key-preference.ts +57 -0
- 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))
|
|
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
|
-
|
|
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'
|