@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
@@ -81,6 +81,28 @@ async function childState(file: string, root: string, now: number, budget: { byt
81
81
  }
82
82
 
83
83
  /** One bounded read-only SQLite snapshot per list/detail read. */
84
+ /**
85
+ * Key lanes (P11): readCodexSubagents over every Codex home. Each parent is read in the home that
86
+ * holds its thread (`rootOf`, a keyed thread's profile home), every other one in the server home.
87
+ * With one root (key lanes off, or no profile home) it is exactly the one server-home call.
88
+ */
89
+ export async function readCodexSubagentsAcross(
90
+ parents: readonly string[], sessionsRoots: readonly string[], now?: number, rootOf?: (id: string) => string | undefined,
91
+ ): Promise<Map<string, SubagentActivity>> {
92
+ const server = sessionsRoots[0]!
93
+ const read = (ids: readonly string[], root: string) => now === undefined ? readCodexSubagents(ids, root) : readCodexSubagents(ids, root, now)
94
+ if (sessionsRoots.length <= 1 || !rootOf) return read(parents, server)
95
+ const groups = new Map<string, string[]>()
96
+ for (const id of parents) {
97
+ const own = rootOf(id)
98
+ const root = own && sessionsRoots.includes(own) ? own : server
99
+ groups.set(root, [...(groups.get(root) ?? []), id])
100
+ }
101
+ const out = new Map<string, SubagentActivity>()
102
+ for (const [root, ids] of groups) for (const [id, activity] of await read(ids, root)) out.set(id, activity)
103
+ return out
104
+ }
105
+
84
106
  export async function readCodexSubagents(
85
107
  parents: readonly string[], sessionsRoot: string, now = Date.now(),
86
108
  ): Promise<Map<string, SubagentActivity>> {
@@ -13,6 +13,8 @@ import { logSessionEnd, buildSessionLogEntry, writeSessionLog } from './session-
13
13
  import { atomicWriteFileSync, durableAtomicWriteFileSync, loadJsonOrQuarantine } from './atomic-fs.js'
14
14
  import { localDay } from './local-day.js'
15
15
  import { normalizeModelPreference, type ModelPreference } from '../../shared/model-preference.js'
16
+ import { parsePreference, profilePreference } from '../../shared/key-preference.js'
17
+ import { onKeyLanesInactive } from './key-lanes.js'
16
18
  import { parseMediaAttachmentRefs, type MediaAttachmentRef } from '../../shared/media-attachment.js'
17
19
  import { secureExistingPrivateFile } from './secure-user-config.js'
18
20
  import { currentMessageEra } from './message-era.js'
@@ -20,6 +22,38 @@ import { formatReferencedSourceData } from './prompt-reference-boundary.js'
20
22
 
21
23
  export type { ModelPreference }
22
24
 
25
+ /**
26
+ * Key lanes K1 (plan v4 5.2, W2): what a session or an exchange may be bound to. A model slot,
27
+ * exactly as before, or a key profile as `profile:<id>`. Persistence accepts a profile by SYNTAX
28
+ * only: whether that profile still exists, is enabled and has a key is decided at resolve time
29
+ * (model-router), which refuses with a lens line. Dropping the binding here would make the next
30
+ * turn run on the subscription without a word, which is the bug this type exists to prevent.
31
+ */
32
+ export type KeyProfileBinding = `profile:${string}`
33
+ export type SessionModelBinding = ModelPreference | KeyProfileBinding
34
+
35
+ /**
36
+ * The persistence normalizer (plan 5.2 Accept by syntax). For every input that is not a well
37
+ * formed `profile:<id>` this returns exactly what normalizeModelPreference returns, so a store
38
+ * with no profile in it reads and writes byte for byte as 6.79.0 did.
39
+ */
40
+ export function normalizeSessionBinding(value: unknown): SessionModelBinding | undefined {
41
+ const parsed = parsePreference(value)
42
+ if (!parsed) return undefined
43
+ return parsed.kind === 'model' ? parsed.value : profilePreference(parsed.id) as KeyProfileBinding
44
+ }
45
+
46
+ /** The profile id of a `profile:<id>` binding, else undefined. */
47
+ export function bindingProfileId(value: unknown): string | undefined {
48
+ const parsed = parsePreference(value)
49
+ return parsed?.kind === 'profile' ? parsed.id : undefined
50
+ }
51
+
52
+ /** The closed view of a binding: the model slot, or null for a profile (or nothing). */
53
+ function closedSessionModel(value: SessionModelBinding | null | undefined): ModelPreference | null {
54
+ return normalizeModelPreference(value) ?? null
55
+ }
56
+
23
57
  export interface Exchange {
24
58
  role: 'user' | 'assistant'
25
59
  content: string
@@ -31,8 +65,9 @@ export interface Exchange {
31
65
  /** Short-number namespace. Missing means the original legacy era. */
32
66
  messageEra?: string
33
67
  /** Per-exchange model badge. Prefer this over session.modelPreference when
34
- * rendering history so Cursor/Codex turns do not inherit an Opus default. */
35
- modelPreference?: ModelPreference
68
+ * rendering history so Cursor/Codex turns do not inherit an Opus default.
69
+ * K1: a keyed turn is stamped `profile:<id>` (accepted by syntax). */
70
+ modelPreference?: SessionModelBinding
36
71
  /** Public attachment refs may live on either half of a Q&A pair: request
37
72
  * photos on the user exchange, model-published images on the assistant
38
73
  * exchange. Bytes, filesystem paths, and capabilities never persist here. */
@@ -75,7 +110,7 @@ interface Session {
75
110
  exchanges: Exchange[]
76
111
  lastActivity: number
77
112
  createdAt: number
78
- modelPreference: ModelPreference | null
113
+ modelPreference: SessionModelBinding | null
79
114
  contextBreaks: number[] // timestamps where user said "new chat" — prompt history only sees exchanges after last break
80
115
  }
81
116
 
@@ -160,12 +195,12 @@ function loadFromDisk(): void {
160
195
  let loaded = 0
161
196
  for (const [id, session] of Object.entries(result.data.sessions)) {
162
197
  if (!session.contextBreaks) session.contextBreaks = []
163
- session.modelPreference = normalizeModelPreference(session.modelPreference) ?? null
198
+ session.modelPreference = normalizeSessionBinding(session.modelPreference) ?? null
164
199
  // Persistence boundary: malformed refs are dropped without sacrificing
165
200
  // the surrounding exchange or the rest of the recovered session.
166
201
  for (const exchange of session.exchanges) {
167
202
  validateLoadedExchangeProvenance(exchange)
168
- const exchangeModel = normalizeModelPreference(exchange.modelPreference)
203
+ const exchangeModel = normalizeSessionBinding(exchange.modelPreference)
169
204
  if (exchangeModel) exchange.modelPreference = exchangeModel
170
205
  else delete exchange.modelPreference
171
206
  if ('attachments' in exchange && exchange.attachments !== undefined) {
@@ -479,7 +514,7 @@ export function addExchange(
479
514
  globalMsgNum?: number,
480
515
  attachments?: MediaAttachmentRef[],
481
516
  provenance?: ExchangeJobProvenance,
482
- modelPreference?: ModelPreference | null,
517
+ modelPreference?: SessionModelBinding | null,
483
518
  ): Exchange {
484
519
  let session = sessions.get(sessionId)
485
520
  if (!session) {
@@ -487,7 +522,7 @@ export function addExchange(
487
522
  sessions.set(sessionId, session)
488
523
  }
489
524
 
490
- const stampedModel = normalizeModelPreference(modelPreference)
525
+ const stampedModel = normalizeSessionBinding(modelPreference)
491
526
  const exchange: Exchange = {
492
527
  role,
493
528
  content,
@@ -593,7 +628,7 @@ export function reconcileExchangeByJobIdentity(
593
628
  globalMsgNum?: number,
594
629
  attachments?: MediaAttachmentRef[],
595
630
  messageEra?: string,
596
- modelPreference?: ModelPreference | null,
631
+ modelPreference?: SessionModelBinding | null,
597
632
  origin?: ExchangeOriginStamp,
598
633
  ): ReconciledExchange {
599
634
  if (!validClientJobId(identity.clientJobId) || !validGeneration(identity.generation)) {
@@ -625,7 +660,7 @@ export function reconcileExchangeByJobIdentity(
625
660
  && /^[a-z0-9][a-z0-9._-]{0,79}$/i.test(messageEra)
626
661
  ? messageEra
627
662
  : currentMessageEra()
628
- const stampedModel = normalizeModelPreference(modelPreference)
663
+ const stampedModel = normalizeSessionBinding(modelPreference)
629
664
  let exchange: Exchange
630
665
  const created = matchingIndexes.length === 0
631
666
  if (created) {
@@ -709,17 +744,57 @@ export async function clearSession(sessionId: string): Promise<void> {
709
744
  scheduleCacheUpdate()
710
745
  }
711
746
 
747
+ /**
748
+ * The session's model slot. CLOSED (plan 5): a session bound to a key profile reads null here,
749
+ * so no caller that predates key lanes can ever mistake `profile:<id>` for a model and fall
750
+ * through to Claude. Routing reads getSessionBinding instead.
751
+ */
712
752
  export function getSessionModel(sessionId: string): ModelPreference | null {
753
+ return closedSessionModel(sessions.get(sessionId)?.modelPreference)
754
+ }
755
+
756
+ /** The session's binding, a model slot or `profile:<id>`. Only model-router and the job runtime read it. */
757
+ export function getSessionBinding(sessionId: string): SessionModelBinding | null {
713
758
  return sessions.get(sessionId)?.modelPreference ?? null
714
759
  }
715
760
 
761
+ /**
762
+ * Plan 12 (Remove all keys): every session bound to a profile (or only to `profileIds`) loses the
763
+ * binding and gets a context break, so a later subscription turn starts a fresh CLI session and
764
+ * never resumes a keyed transcript. Exchange stamps stay: history still says which answers ran
765
+ * on a key. Returns the session ids it changed.
766
+ */
767
+ export function clearProfileBindings(profileIds?: readonly string[]): string[] {
768
+ const only = profileIds ? new Set(profileIds) : null
769
+ const changed: string[] = []
770
+ const now = Date.now()
771
+ for (const session of sessions.values()) {
772
+ const id = bindingProfileId(session.modelPreference)
773
+ if (id === undefined || (only && !only.has(id))) continue
774
+ session.modelPreference = null
775
+ session.contextBreaks.push(now)
776
+ changed.push(session.id)
777
+ }
778
+ if (changed.length > 0) {
779
+ scheduleSave()
780
+ scheduleCacheUpdate()
781
+ }
782
+ return changed
783
+ }
784
+
785
+ // Plan 12: Remove all keys clears every profile binding. A named Map entry; nothing runs until
786
+ // the remove-all route fires it.
787
+ onKeyLanesInactive('p6-session-bindings', reason => {
788
+ if (reason === 'remove_all') clearProfileBindings()
789
+ })
790
+
716
791
  /** Prefer per-exchange stamps; fall back to session model for pre-stamp rows. */
717
792
  export function resolveExchangePairModel(
718
793
  user: Pick<Exchange, 'modelPreference'> | undefined,
719
794
  assistant: Pick<Exchange, 'modelPreference'> | undefined,
720
- sessionModel: ModelPreference | null | undefined,
721
- ): ModelPreference | undefined {
722
- return normalizeModelPreference(
795
+ sessionModel: SessionModelBinding | null | undefined,
796
+ ): SessionModelBinding | undefined {
797
+ return normalizeSessionBinding(
723
798
  assistant?.modelPreference
724
799
  ?? user?.modelPreference
725
800
  ?? sessionModel
@@ -727,7 +802,7 @@ export function resolveExchangePairModel(
727
802
  )
728
803
  }
729
804
 
730
- export function setSessionModel(sessionId: string, model: ModelPreference | null): void {
805
+ export function setSessionModel(sessionId: string, model: SessionModelBinding | null): void {
731
806
  const session = sessions.get(sessionId)
732
807
  if (session) {
733
808
  session.modelPreference = model
@@ -809,7 +884,7 @@ export interface SessionSummary {
809
884
  exchangeCount: number
810
885
  lastActivity: number
811
886
  createdAt: number
812
- modelPreference: ModelPreference | null
887
+ modelPreference: SessionModelBinding | null
813
888
  lastQuery: string | null
814
889
  }
815
890
 
@@ -948,7 +1023,9 @@ export function getActiveSessions(): SessionToArchive[] {
948
1023
  contextBreaks: session.contextBreaks,
949
1024
  createdAt: session.createdAt,
950
1025
  lastActivity: session.lastActivity,
951
- modelPreference: session.modelPreference,
1026
+ // Archive rows carry their own stamps (a keyed answer is stamped profile:<id>); the
1027
+ // session-level fallback stays a closed model slot, as archive.ts types it.
1028
+ modelPreference: closedSessionModel(session.modelPreference),
952
1029
  })
953
1030
  }
954
1031
  return result