@gotcos/glasses-server 6.79.1 → 6.80.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 (74) hide show
  1. package/.env.example +9 -0
  2. package/CHANGELOG.md +98 -0
  3. package/README.md +20 -1
  4. package/managed-runtime-contract.json +6 -1
  5. package/package.json +9 -1
  6. package/server/index.ts +11 -0
  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/dictation-clean.ts +13 -6
  23. package/server/lib/dictation-fillers.ts +17 -10
  24. package/server/lib/dictation-polish-need.ts +48 -0
  25. package/server/lib/dictation-prewarm.ts +209 -0
  26. package/server/lib/direct-bridge.ts +853 -0
  27. package/server/lib/direct-sse.ts +262 -0
  28. package/server/lib/fork-thread.ts +11 -0
  29. package/server/lib/key-answer-fields.ts +49 -0
  30. package/server/lib/key-catalog.ts +301 -0
  31. package/server/lib/key-lanes-pricing.ts +12 -0
  32. package/server/lib/key-lanes-wiring.ts +346 -0
  33. package/server/lib/key-lanes.ts +145 -0
  34. package/server/lib/key-ledger.ts +123 -0
  35. package/server/lib/key-notices.ts +486 -0
  36. package/server/lib/key-profile-rejects.ts +25 -0
  37. package/server/lib/key-profile-routing.ts +173 -0
  38. package/server/lib/key-profiles.ts +182 -0
  39. package/server/lib/key-relay-contract.ts +262 -0
  40. package/server/lib/key-relay-errors.ts +211 -0
  41. package/server/lib/key-relay-meter.ts +303 -0
  42. package/server/lib/key-relay.ts +1018 -0
  43. package/server/lib/key-secrets-guard.ts +52 -0
  44. package/server/lib/key-store.ts +228 -0
  45. package/server/lib/keyed-sessions.ts +154 -0
  46. package/server/lib/lane-b-env.ts +169 -0
  47. package/server/lib/live-cues-v2-settings.ts +51 -13
  48. package/server/lib/live-cues-v2.ts +147 -19
  49. package/server/lib/model-router.ts +96 -7
  50. package/server/lib/morning-brief-config.ts +3 -0
  51. package/server/lib/native-head.ts +13 -1
  52. package/server/lib/occupancy-probes.ts +4 -0
  53. package/server/lib/ollama-run-ledger.ts +96 -13
  54. package/server/lib/prompt-edit.ts +2 -0
  55. package/server/lib/query-job-coordinator.ts +2 -1
  56. package/server/lib/query-job-runtime.ts +67 -15
  57. package/server/lib/query-job-store.ts +7 -1
  58. package/server/lib/query-job-types.ts +15 -1
  59. package/server/lib/session-hook-events.ts +28 -0
  60. package/server/lib/session-hook-ledger.ts +10 -0
  61. package/server/lib/thread-occupancy.ts +22 -1
  62. package/server/lib/token-audit.ts +18 -4
  63. package/server/lib/work-handoff-requests.ts +3 -0
  64. package/server/routes/agent-session-bindings.ts +15 -0
  65. package/server/routes/agent-sessions.ts +17 -5
  66. package/server/routes/handoffs.ts +22 -1
  67. package/server/routes/health.ts +5 -0
  68. package/server/routes/key-lanes.ts +442 -0
  69. package/server/routes/live-cues.ts +39 -3
  70. package/server/routes/openai-compat.ts +28 -3
  71. package/server/routes/prompt-drafts.ts +75 -8
  72. package/server/routes/query.ts +20 -6
  73. package/shared/key-preference.ts +57 -0
  74. package/shared/model-preference.ts +20 -2
@@ -44,6 +44,11 @@ import { dictationSlotForMode, legacyDictationKey, type DictationPlan } from '..
44
44
  import { autoCleanDictation, AUTOCLEAN_MAX_CHARS } from '../lib/dictation-clean.js'
45
45
  import { fillerPassEnabled, stripDisfluencies } from '../lib/dictation-fillers.js'
46
46
  import { polishBudgetExhausted, recordPolishCall, type PolishBucket } from '../lib/dictation-polish-budget.js'
47
+ import { polishNeed, skipCleanEnabled } from '../lib/dictation-polish-need.js'
48
+ import {
49
+ discardPolishPrewarm, isPrewarmModel, prewarmEnabled, PrewarmUnavailableError, runWarmPolish,
50
+ startPolishPrewarm, takePolishPrewarm,
51
+ } from '../lib/dictation-prewarm.js'
47
52
  import { getVocabulary } from '../lib/profile.js'
48
53
  import { createBreaker } from '../lib/claude-circuit.js'
49
54
  import { logTokenAudit } from '../lib/token-audit.js'
@@ -85,7 +90,9 @@ const autoCleanBreakers = {
85
90
  const autoCleanDefaultEnabled = () => ['1', 'true', 'on'].includes((process.env.COS_DICTATION_AUTOCLEAN ?? '').toLowerCase())
86
91
 
87
92
  /** 6.79.1, additive on finalize answers: why the polish model did not run (or did not finish). */
88
- export type PolishSkipped = 'disabled' | 'too_long' | 'breaker_open' | 'daily_cap' | 'failed'
93
+ export type PolishSkipped = 'disabled' | 'too_long' | 'breaker_open' | 'daily_cap' | 'failed' | 'clean'
94
+ /** 6.79.2, additive when finalize was given a prewarmToken: what became of that warm process. */
95
+ export type PrewarmOutcome = 'used' | 'missed' | 'discarded'
89
96
 
90
97
  interface AutoCleanRequest { enabled?: boolean; model?: DictationCleanModel }
91
98
  function routeAutoClean(req: { body?: any; query?: any }): AutoCleanRequest {
@@ -99,8 +106,20 @@ function routeAutoClean(req: { body?: any; query?: any }): AutoCleanRequest {
99
106
 
100
107
  async function cleanOutboundDictation(
101
108
  text: string,
102
- opts: AutoCleanRequest & { bucket: PolishBucket; signal?: AbortSignal },
103
- ): Promise<{ text: string; polishSkipped?: PolishSkipped }> {
109
+ opts: AutoCleanRequest & { bucket: PolishBucket; signal?: AbortSignal; prewarmToken?: string },
110
+ ): Promise<{ text: string; polishSkipped?: PolishSkipped; prewarm?: PrewarmOutcome }> {
111
+ const result = await polishOutboundDictation(text, opts)
112
+ // A warm process the model call did not take must not sit until its TTL.
113
+ if (opts.prewarmToken && !result.prewarm) {
114
+ return { ...result, prewarm: discardPolishPrewarm(opts.prewarmToken) ? 'discarded' : 'missed' }
115
+ }
116
+ return result
117
+ }
118
+
119
+ async function polishOutboundDictation(
120
+ text: string,
121
+ opts: AutoCleanRequest & { bucket: PolishBucket; signal?: AbortSignal; prewarmToken?: string },
122
+ ): Promise<{ text: string; polishSkipped?: PolishSkipped; prewarm?: PrewarmOutcome }> {
104
123
  let cleaned = applyNegativeRules(applyCorrections(text)).replace(/\s+/g, ' ').trim() || text
105
124
  // applyCorrections above is an EXACT string map, so it only fixes misspellings someone
106
125
  // already hand-authored. A novel miss ("Rileyy" for Riley, "Exampel" for Example) sails
@@ -129,22 +148,37 @@ async function cleanOutboundDictation(
129
148
  const model = opts.model ?? 'haiku'
130
149
  const autoCleanBreaker = autoCleanBreakers[model]
131
150
  if (cleaned.length > AUTOCLEAN_MAX_CHARS) return { text: cleaned, polishSkipped: 'too_long' }
151
+ // 6.79.2, off by default: nothing for the model to fix (see dictation-polish-need.ts).
152
+ if (skipCleanEnabled() && polishNeed(cleaned) === null) return { text: cleaned, polishSkipped: 'clean' }
132
153
  if (autoCleanBreaker.isOpen()) return { text: cleaned, polishSkipped: 'breaker_open' }
133
154
  if (polishBudgetExhausted(opts.bucket)) return { text: cleaned, polishSkipped: 'daily_cap' }
134
155
  const startedAt = Date.now()
135
156
  recordPolishCall(opts.bucket)
157
+ // 6.79.2: a warm process started at key press answers without the CLI's start-up cost.
158
+ const warmEntry = opts.prewarmToken ? takePolishPrewarm(opts.prewarmToken, model) : null
159
+ let prewarm: PrewarmOutcome | undefined = opts.prewarmToken ? 'missed' : undefined
136
160
  try {
137
- const polished = (await autoCleanDictation(cleaned, getVocabulary(), { model, signal: opts.signal })).trim()
161
+ let polished: string | null = null
162
+ if (warmEntry) {
163
+ try {
164
+ polished = (await runWarmPolish(warmEntry, cleaned, getVocabulary(), { signal: opts.signal })).trim()
165
+ prewarm = 'used'
166
+ } catch (err) {
167
+ // Nothing was sent: the cold path below is the same call, not a second one.
168
+ if (!(err instanceof PrewarmUnavailableError)) throw err
169
+ }
170
+ }
171
+ if (polished === null) polished = (await autoCleanDictation(cleaned, getVocabulary(), { model, signal: opts.signal })).trim()
138
172
  autoCleanBreaker.recordSuccess()
139
173
  logTokenAudit({
140
174
  source: 'g2-dictation-autoclean', model, inputChars: cleaned.length, outputChars: polished.length,
141
175
  durationMs: Date.now() - startedAt, caller: `dictation_autoclean_${opts.bucket}`,
142
176
  })
143
- return { text: polished || cleaned }
177
+ return { text: polished || cleaned, ...(prewarm ? { prewarm } : {}) }
144
178
  } catch (err: any) {
145
179
  autoCleanBreaker.recordFailure()
146
180
  console.warn(`[prompt-draft] auto-clean failed (glossary-only): ${err?.message ?? err}`)
147
- return { text: cleaned, polishSkipped: 'failed' }
181
+ return { text: cleaned, polishSkipped: 'failed', ...(prewarm ? { prewarm } : {}) }
148
182
  }
149
183
  }
150
184
 
@@ -165,17 +199,50 @@ promptDraftsRouter.post('/dictation/finalize', async (req, res) => {
165
199
  if (model !== undefined && !isDictationCleanModel(model)) {
166
200
  return res.status(400).json({ error: 'invalid_autoclean_model' })
167
201
  }
202
+ const rawToken = req.body?.prewarmToken
203
+ if (rawToken !== undefined && (typeof rawToken !== 'string' || !PREWARM_TOKEN.test(rawToken))) {
204
+ return res.status(400).json({ error: 'invalid_prewarm_token' })
205
+ }
168
206
 
169
207
  const abort = new AbortController()
170
208
  res.on('close', () => { if (!res.writableEnded) abort.abort() })
171
- const { text: finalText, polishSkipped } = await cleanOutboundDictation(text, {
209
+ const { text: finalText, polishSkipped, prewarm } = await cleanOutboundDictation(text, {
172
210
  enabled: true,
173
211
  model,
174
212
  bucket: surface,
175
213
  signal: abort.signal,
214
+ prewarmToken: rawToken,
176
215
  })
177
216
  // 6.79.1: polishSkipped only when the model did not polish, so a polished answer is unchanged.
178
- res.json({ text: finalText, surface, polished: finalText !== text, ...(polishSkipped ? { polishSkipped } : {}) })
217
+ // 6.79.2: prewarm only when the request carried a prewarmToken.
218
+ res.json({
219
+ text: finalText, surface, polished: finalText !== text,
220
+ ...(polishSkipped ? { polishSkipped } : {}), ...(prewarm ? { prewarm } : {}),
221
+ })
222
+ })
223
+
224
+ const PREWARM_TOKEN = /^[0-9a-f-]{36}$/
225
+
226
+ /** 6.79.2: start the polish process at key press. Fire and forget: every refusal is a normal
227
+ * answer, and finalize without a token (or with a dead one) spawns as before. */
228
+ promptDraftsRouter.post('/dictation/polish/prewarm', (req, res) => {
229
+ const model = req.body?.model ?? 'haiku'
230
+ const surface = req.body?.surface ?? 'message'
231
+ if (!isDictationCleanModel(model)) return res.status(400).json({ error: 'invalid_autoclean_model' })
232
+ if (surface !== 'message' && surface !== 'meeting') return res.status(400).json({ error: 'invalid_surface' })
233
+ if (!prewarmEnabled()) return res.status(409).json({ error: 'prewarm_unavailable', reason: 'disabled' })
234
+ if (!isPrewarmModel(model)) return res.status(409).json({ error: 'prewarm_unavailable', reason: 'model' })
235
+ if (autoCleanBreakers[model].isOpen()) return res.status(409).json({ error: 'prewarm_unavailable', reason: 'breaker_open' })
236
+ if (polishBudgetExhausted(surface)) return res.status(409).json({ error: 'prewarm_unavailable', reason: 'daily_cap' })
237
+ try {
238
+ res.json(startPolishPrewarm(model))
239
+ } catch (err: any) {
240
+ res.status(409).json({ error: 'prewarm_unavailable', reason: 'cli' })
241
+ }
242
+ })
243
+
244
+ promptDraftsRouter.delete('/dictation/polish/prewarm/:token', (req, res) => {
245
+ res.json({ discarded: discardPolishPrewarm(req.params.token) })
179
246
  })
180
247
 
181
248
  async function readRawBody(req: AsyncIterable<Buffer | Uint8Array | string>): Promise<Buffer> {
@@ -8,8 +8,9 @@ import { errMsg } from '../lib/utils.js'
8
8
  import {
9
9
  isCursorModel,
10
10
  normalizeEffortPreference,
11
- normalizeModelPreference,
12
11
  } from '../../shared/model-preference.js'
12
+ import { parsePreference } from '../../shared/key-preference.js'
13
+ import type { KeyProfileTurn } from '../lib/key-profile-routing.js'
13
14
  import { QueryAttachmentError, resolveQueryAttachments } from '../lib/query-attachments.js'
14
15
  import { getMediaStore } from '../lib/media-store.js'
15
16
  import { mergeMediaAttachmentRefs } from '../../shared/media-attachment.js'
@@ -19,6 +20,8 @@ import {
19
20
  maintenanceErrorPayload,
20
21
  } from '../lib/maintenance-lifecycle.js'
21
22
  import { currentMessageEra, LEGACY_MESSAGE_ERA } from '../lib/message-era.js'
23
+ import { keyAnswerDoneFields } from '../lib/key-answer-fields.js'
24
+ import { isProfilePreference } from '../../shared/key-preference.js'
22
25
 
23
26
  const TOOL_STATUS_MESSAGES: Record<string, string> = {
24
27
  WebSearch: 'Searching web...',
@@ -92,8 +95,11 @@ queryRouter.post('/query', async (req, res) => {
92
95
  return res.status(400).json({ error: 'query string or attachment required' })
93
96
  }
94
97
 
95
- // Validate model if provided
96
- const validModel = normalizeModelPreference(model)
98
+ // Validate model if provided. Key lanes K1 (plan 5.2): Accept. A `profile:<id>` is passed to
99
+ // model-router as is (it runs on the profile or is refused there); every family guard below
100
+ // reads the closed slot, so a model slot behaves exactly as before.
101
+ const requestedBinding = parsePreference(model)
102
+ const validModel = requestedBinding?.kind === 'model' ? requestedBinding.value : undefined
97
103
  const validEffort = normalizeEffortPreference(effort)
98
104
 
99
105
  // Validate globalMsgNum if provided
@@ -136,12 +142,17 @@ queryRouter.post('/query', async (req, res) => {
136
142
  done = true
137
143
  })
138
144
 
145
+ // Set only by model-router's onKeyProfileTurn: the binding is the turn's model on the stream.
146
+ let keyTurn: KeyProfileTurn | undefined
139
147
  try {
140
148
  const sid = await callModelStreaming(resolvedQuery || '', sessionId, {
149
+ ...(requestedBinding?.kind === 'model' ? {} : {
150
+ onKeyProfileTurn: (turn: KeyProfileTurn) => { keyTurn = turn },
151
+ }),
141
152
  onStart: (model, sid, cliSessionId, metadata) => {
142
153
  streamSessionId = sid
143
154
  if (!done) {
144
- const payload = { model, sessionId: sid, cliSessionId, ...metadata }
155
+ const payload = { model: keyTurn ? keyTurn.binding : model, sessionId: sid, cliSessionId, ...metadata }
145
156
  res.write(`event: start\ndata: ${JSON.stringify(payload)}\n\n`)
146
157
  emitDisplay({ type: 'start', data: payload })
147
158
  }
@@ -192,7 +203,8 @@ queryRouter.post('/query', async (req, res) => {
192
203
  const attachments = mergeMediaAttachmentRefs(attachmentRefs, metadata?.outputAttachments)
193
204
  const { outputAttachments: _outputAttachments, ...runMetadata } = metadata ?? {}
194
205
  const payload = {
195
- text: fullText, sessionId: streamSessionId, model, cliSessionId, ...runMetadata,
206
+ text: fullText, sessionId: streamSessionId, model: keyTurn ? keyTurn.binding : model, cliSessionId, ...runMetadata,
207
+ ...keyAnswerDoneFields(runMetadata),
196
208
  ...(attachments.length > 0 ? { attachments } : {}),
197
209
  }
198
210
  res.write(`event: done\ndata: ${JSON.stringify(payload)}\n\n`)
@@ -215,7 +227,7 @@ queryRouter.post('/query', async (req, res) => {
215
227
  maintenanceLease.release()
216
228
  }
217
229
  },
218
- }, validModel, imageInputs,
230
+ }, requestedBinding?.kind === 'profile' ? model as KeyProfileTurn['binding'] : validModel, imageInputs,
219
231
  // Pass reference if provided (for "recall message N" feature)
220
232
  reference && typeof reference === 'object' && reference.query && reference.response
221
233
  ? { query: String(reference.query), response: String(reference.response) }
@@ -228,6 +240,8 @@ queryRouter.post('/query', async (req, res) => {
228
240
  requestAttachments: attachmentRefs,
229
241
  attachmentPromptBlock: resolvedAttachments.promptBlock,
230
242
  ...(validModel && isCursorModel(validModel) ? { cursorExecutionMode } : {}),
243
+ // QA U9: the one-turn plan-limit offer runs on the key without binding the session.
244
+ ...(req.body.keyTurnOnly === true && isProfilePreference(model) ? { keyTurnOnly: true } : {}),
231
245
  },
232
246
  )
233
247
  } catch (err: unknown) {
@@ -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'