@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
@@ -543,11 +543,32 @@ export function codexOwners(
543
543
  return { owners, doubt }
544
544
  }
545
545
 
546
+ /**
547
+ * Key lanes K1 (plan v4 8.2): the server home's writer locks, then each keyed profile home's.
548
+ * A keyed home that is absent adds nothing (its detector was never the server's to lose); with
549
+ * no profile home this is exactly `codexOwners` on the server home.
550
+ */
551
+ export function codexOwnersAcrossHomes(threadId: string, probes: OccupancyProbes, dirs: OccupancyDirs): ScanResult {
552
+ const base = codexOwners(threadId, probes, dirs.codexLocksDir)
553
+ if (!dirs.codexProfileLocksDirs) return base
554
+ const owners = [...base.owners]
555
+ let doubt = base.doubt
556
+ for (const dir of dirs.codexProfileLocksDirs) {
557
+ if (!probes.dirExists(dir)) continue
558
+ const keyed = codexOwners(threadId, probes, dir)
559
+ owners.push(...keyed.owners)
560
+ if (keyed.doubt !== null && keyed.doubt !== 'detector_unavailable') doubt ??= keyed.doubt
561
+ }
562
+ return { ...base, owners, doubt }
563
+ }
564
+
546
565
  export interface OccupancyDirs {
547
566
  /** `<CLAUDE_CONFIG_DIR|~/.claude>/sessions` */
548
567
  claudeSessionsDir: string
549
568
  /** `<CODEX_HOME|~/.codex>/thread-writer-locks` */
550
569
  codexLocksDir: string
570
+ /** Key lanes K1 (plan v4 8.2): each keyed profile home's `thread-writer-locks`. Absent with no profile home. */
571
+ codexProfileLocksDirs?: string[]
551
572
  /** `<COS_AGENT_SESSIONS_HOME|~>/.cursor/chats` */
552
573
  cursorChatsDir: string
553
574
  }
@@ -698,7 +719,7 @@ export function threadOccupancy(
698
719
  try {
699
720
  result = provider === 'claude'
700
721
  ? claudeOwners(threadId, probes, dirs.claudeSessionsDir)
701
- : codexOwners(threadId, probes, dirs.codexLocksDir)
722
+ : codexOwnersAcrossHomes(threadId, probes, dirs)
702
723
  } catch {
703
724
  return { attachable: false, owners: [], reason: 'probe_failed' }
704
725
  }
@@ -7,6 +7,7 @@ import { resolve } from 'node:path'
7
7
  import { homedir } from 'node:os'
8
8
 
9
9
  import { COS_SCRIPTS_DIR } from './python-bridge.js'
10
+ import type { TokenAuditKeyFields } from './key-relay-contract.js'
10
11
 
11
12
  // Shared JSONL file — same path as Python's llm_client.py uses
12
13
  // Uses COS_SCRIPTS_DIR when available (server mode), falls back to user home
@@ -21,7 +22,7 @@ function estimateTokens(chars: number): number {
21
22
  return Math.max(1, Math.floor(chars / CHARS_PER_TOKEN))
22
23
  }
23
24
 
24
- export interface TokenAuditEntry {
25
+ export interface TokenAuditEntry extends TokenAuditKeyFields {
25
26
  source: string // "g2-voice", "g2-prewarm", "g2-archive", "g2-query"
26
27
  model: string // "opus", "sonnet", "haiku", "cursor-composer", ...
27
28
  inputChars: number
@@ -35,9 +36,14 @@ export interface TokenAuditEntry {
35
36
  usageKind?: 'provider_final' | 'estimated'
36
37
  }
37
38
 
38
- export function logTokenAudit(entry: TokenAuditEntry): void {
39
- const record = {
40
- ts: new Date().toISOString(),
39
+ /**
40
+ * The JSONL record. Key-lane fields (plan v4 3.1: `profile`, `lane`, `credentialKind`, `usd`) are
41
+ * conditional spreads AFTER every 6.79.0 key, written only when the caller sets them, so a Lane A
42
+ * (subscription) row is byte-identical to 6.79.0. Callers set them only on keyed turns.
43
+ */
44
+ export function buildTokenAuditRecord(entry: TokenAuditEntry, now: Date = new Date()): Record<string, unknown> {
45
+ return {
46
+ ts: now.toISOString(),
41
47
  source: entry.source,
42
48
  model: entry.model,
43
49
  input_chars: entry.inputChars,
@@ -51,7 +57,15 @@ export function logTokenAudit(entry: TokenAuditEntry): void {
51
57
  ...(entry.runId ? { run_id: entry.runId } : {}),
52
58
  ...(entry.sessionId ? { session_id: entry.sessionId } : {}),
53
59
  ...(entry.clientJobId ? { client_job_id: entry.clientJobId } : {}),
60
+ ...(entry.profile ? { profile: entry.profile } : {}),
61
+ ...(entry.lane ? { lane: entry.lane } : {}),
62
+ ...(entry.credentialKind ? { credentialKind: entry.credentialKind } : {}),
63
+ ...(typeof entry.usd === 'number' && Number.isFinite(entry.usd) ? { usd: entry.usd } : {}),
54
64
  }
65
+ }
66
+
67
+ export function logTokenAudit(entry: TokenAuditEntry): void {
68
+ const record = buildTokenAuditRecord(entry)
55
69
  try {
56
70
  appendFileSync(AUDIT_FILE, JSON.stringify(record) + '\n', { encoding: 'utf8', mode: 0o600 })
57
71
  chmodSync(AUDIT_FILE, 0o600)
@@ -23,6 +23,7 @@ import { isSafeDomainName } from './domains.js'
23
23
  import { acquireServerInstanceLock, type ServerInstanceLock } from './server-instance-lock.js'
24
24
  import { parseWorkSessionId, modelProvider, UUID_V4, FORK_PROVIDERS } from './work-activity.js'
25
25
  import { normalizeModelPreference } from '../../shared/model-preference.js'
26
+ import { KEY_PROFILE_REJECT_CODE, KEY_PROFILE_WORK_REJECT, keyProfileRejectApplies } from './key-profile-rejects.js'
26
27
 
27
28
  export class WorkHandoffRequestError extends Error {
28
29
  /** `extra` goes into the error body next to the code (the daily cap's `resetsAt`). */
@@ -136,6 +137,8 @@ export function parseHandoffRequest(body: unknown, defaultModel: string): { inpu
136
137
  let model: string | undefined
137
138
  if (given(body.model)) {
138
139
  model = normalizeModelPreference(body.model)
140
+ // Key lanes K1 (plan 5.2): Reject, and say why. Work hands off subscription sessions only.
141
+ if (!model && keyProfileRejectApplies(body.model)) throw new WorkHandoffRequestError(KEY_PROFILE_REJECT_CODE, 400, KEY_PROFILE_WORK_REJECT)
139
142
  if (!model) throw invalid('Choose a model slot this server knows.')
140
143
  }
141
144
  let note: string | undefined
@@ -131,6 +131,7 @@ import { isValidNativeThreadId } from '../lib/native-thread-id.js'
131
131
  import { FORK_PROVIDER_FAILURES, type ForkProviderFailure } from '../lib/fork-thread.js'
132
132
  import { CLIENT_FORK_ID_RE, ForkJobLedger, type ForkJobRow } from '../lib/fork-job-ledger.js'
133
133
  import { PEER_VERIFY_TIMEOUT_MS } from '../lib/session-peer-inbox.js'
134
+ import { KEYED_SESSION_REFUSAL } from '../lib/keyed-sessions.js'
134
135
  import {
135
136
  CANCEL_QUEUE_HOLD_MS,
136
137
  CLIENT_CANCEL_ID_RE,
@@ -652,6 +653,9 @@ export type WriteRefusal =
652
653
  // person said stop. Deliberately NOT a fence (plan validation B2): the next Continue
653
654
  // must work, and it re-reads the head.
654
655
  | 'turn_cancelled'
656
+ // Key lanes (P3, wired by P11): the thread ran on a key profile. Refused before any spawn, with
657
+ // the same 409 body every other keyed entry point answers (keyed-sessions.ts).
658
+ | 'keyed_session'
655
659
  // ------------------------------------------------------------------- fork
656
660
  //
657
661
  // Fork gets its OWN members rather than reusing the ones above, and the reason
@@ -739,6 +743,7 @@ export const WRITE_REASON_COPY: Record<Exclude<WriteRefusal, OccupancyReason>, s
739
743
  'COS could not run this turn. Nothing was sent. You can try again.',
740
744
  turn_cancelled:
741
745
  'Run cancelled.',
746
+ keyed_session: KEYED_SESSION_REFUSAL.message,
742
747
 
743
748
  // Fork copy. No sentence here may end with "Fork it instead" — this IS the fork,
744
749
  // and pointing a failed fork back at itself is a dead end rather than an action.
@@ -2698,6 +2703,10 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
2698
2703
  return refuseFork('fork_orphan_possible', { orphanPossible: true })
2699
2704
  }
2700
2705
 
2706
+ // Key lanes: fork-thread refused a keyed source before any spawn (P3). The exact keyed 409.
2707
+ if (raw && typeof raw === 'object' && (raw as { ok?: unknown }).ok === false && (raw as { reason?: unknown }).reason === 'keyed_session') {
2708
+ return refuseFork('keyed_session', { ...KEYED_SESSION_REFUSAL, retryable: false })
2709
+ }
2701
2710
  const outcome = classifyFork(raw, threadIdParam)
2702
2711
  {
2703
2712
  // 6.62.1: one line per attempt. On 2026-10-05 a fork's real cause existed only
@@ -3458,6 +3467,12 @@ export function createAgentSessionBindingsRouter(deps: AgentSessionBindingsDeps)
3458
3467
  return refuseTurn('turn_cancelled', { retryable: false, deliveryState: 'cancelled' })
3459
3468
  }
3460
3469
 
3470
+ // Key lanes: the adapter refused a keyed thread before any spawn (P3). The exact keyed 409.
3471
+ if (adapterEvidence.adapterReason === 'keyed_session') {
3472
+ spawnMay = false // refused before any spawn
3473
+ return refuseTurn('keyed_session', { ...KEYED_SESSION_REFUSAL, retryable: false })
3474
+ }
3475
+
3461
3476
  if (delivery.kind === 'aborted') {
3462
3477
  // The adapter's own proof that the child never opened the session.
3463
3478
  spawnMay = false
@@ -1,5 +1,14 @@
1
1
  import { workObservationFields } from '../lib/session-work-observation.js'
2
- import { readCodexSubagents } from '../lib/codex-subagent-activity.js'
2
+ import { readCodexSubagentsAcross } from '../lib/codex-subagent-activity.js'
3
+ import { listKeyedSessions } from '../lib/keyed-sessions.js'
4
+ import { codexProfileHome } from '../lib/codex-homes.js'
5
+ import { join as joinPath } from 'node:path'
6
+
7
+ /** Key lanes (P11): a keyed Codex thread's own sessions root (P3's keyed-session record). */
8
+ function keyedCodexRootOf(): (id: string) => string | undefined {
9
+ const byId = new Map(listKeyedSessions().filter(r => r.engine === 'codex').map(r => [r.id.toLowerCase(), joinPath(codexProfileHome(r.profileId), 'sessions')]))
10
+ return id => byId.get(id.toLowerCase())
11
+ }
3
12
  // GET /api/agent-sessions
4
13
  // GET /api/agent-sessions/search?q=
5
14
  // GET /api/agent-sessions/:provider/:sessionId
@@ -31,7 +40,8 @@ import {
31
40
  emptySessionListDropped,
32
41
  loadClaudeDesktopAliases,
33
42
  loadCursorComposerNames,
34
- loadCodexThreadNames,
43
+ codexSessionRootsOf,
44
+ loadCodexThreadNamesAll,
35
45
  idFromCodexFilename,
36
46
  parseAgentSession,
37
47
  type AgentProvider,
@@ -518,7 +528,8 @@ agentSessionsRouter.get('/agent-sessions', async (req, res) => {
518
528
  derivedById.set(row.session_id, deriveCodexRow(row.session_id, running.occupied.has(row.session_id), facts, now))
519
529
  }
520
530
  const queuedOf = await allQueuedWaitingLookup(now)
521
- const subagents = await readCodexSubagents(sessions.filter(row => row.provider === 'codex').map(row => row.session_id), roots.codexSessions, now)
531
+ const codexRoots = codexSessionRootsOf(roots)
532
+ const subagents = await readCodexSubagentsAcross(sessions.filter(row => row.provider === 'codex').map(row => row.session_id), codexRoots, now, codexRoots.length > 1 ? keyedCodexRootOf() : undefined)
522
533
  res.json({
523
534
  sessions: sessions.map((row, index) => {
524
535
  const derived = derivedById.get(row.session_id)
@@ -624,7 +635,7 @@ agentSessionsRouter.get('/agent-sessions/:provider/:sessionId', async (req, res)
624
635
  // parent's id (see `listCodexSessions`), and the parent's name would be the wrong one.
625
636
  let codexThreadName: string | undefined
626
637
  if (provider === 'codex') {
627
- const names = await loadCodexThreadNames(agentSessionRoots().codexSessions)
638
+ const names = await loadCodexThreadNamesAll(agentSessionRoots())
628
639
  const fileId = idFromCodexFilename(found.split('/').pop() || '')
629
640
  codexThreadName = (fileId ? names.get(fileId) : undefined) || names.get(sessionId) || undefined
630
641
  }
@@ -649,7 +660,8 @@ agentSessionsRouter.get('/agent-sessions/:provider/:sessionId', async (req, res)
649
660
  const now = Date.now()
650
661
  const rollout = provider === 'codex' ? codexRolloutFacts(found, now) : null
651
662
  const observed = provider === 'codex' ? codexCompactionFields(workObservationFor(parsed.session_id), rollout, now) : workObservationFields(workObservationFor(parsed.session_id))
652
- const subagents = provider === 'codex' ? (await readCodexSubagents([sessionId], agentSessionRoots().codexSessions)).get(sessionId.toLowerCase()) : observed.subagent_activity
663
+ const detailRoots = provider === 'codex' ? codexSessionRootsOf(agentSessionRoots()) : []
664
+ const subagents = provider === 'codex' ? (await readCodexSubagentsAcross([sessionId], detailRoots, undefined, detailRoots.length > 1 ? keyedCodexRootOf() : undefined)).get(sessionId.toLowerCase()) : observed.subagent_activity
653
665
  const modified = st.mtime.toISOString()
654
666
  const activity = await readSessionActivity(provider, found)
655
667
  const running = runningForThread(provider, parsed.session_id, activityClockMs(st.mtimeMs, activity))
@@ -10,6 +10,7 @@ import {
10
10
  import { getAvailableCliSessionId } from '../lib/claude-bridge.js'
11
11
  import { getCodexExecutionCwd, getCodexTrustMode } from '../lib/codex-run-ledger.js'
12
12
  import { normalizeHandoffCode } from '../../shared/handoff-intent.js'
13
+ import { anyKeyedSessions, isKeyedCosSession, isKeyedSession, KEYED_SESSION_REFUSAL } from '../lib/keyed-sessions.js'
13
14
 
14
15
  export const handoffsRouter = Router()
15
16
 
@@ -17,6 +18,20 @@ function runtimeExpiresAt(): string {
17
18
  return new Date(Date.now() + 2 * 60 * 60_000).toISOString()
18
19
  }
19
20
 
21
+ /**
22
+ * Key lanes K1 (plan v4 7.1, 7.3): a handoff carries a CLI session id that another surface
23
+ * resumes, on the subscription. One that ran on a key is refused in K1. With no keyed session
24
+ * ever recorded this is one failed stat and the route is the b7c620e one.
25
+ */
26
+ function keyedHandoff(body: Record<string, any> | null | undefined, resolvedCliSessionId?: string): boolean {
27
+ if (!anyKeyedSessions()) return false
28
+ const runtime = body?.runtime && typeof body.runtime === 'object' ? body.runtime : {}
29
+ return isKeyedCosSession(body?.sessionId)
30
+ || isKeyedSession(runtime.claude?.cliSessionId)
31
+ || isKeyedSession(runtime.codex?.codexThreadId)
32
+ || isKeyedSession(resolvedCliSessionId)
33
+ }
34
+
20
35
  function enrichRuntime(body: Record<string, any>): HandoffCreateInput {
21
36
  const input: HandoffCreateInput = { ...body }
22
37
  const runtime = body.runtime && typeof body.runtime === 'object' ? { ...body.runtime } : {}
@@ -53,7 +68,9 @@ function enrichRuntime(body: Record<string, any>): HandoffCreateInput {
53
68
  handoffsRouter.post('/handoffs', async (req, res) => {
54
69
  try {
55
70
  const body = req.body && typeof req.body === 'object' ? req.body : {}
56
- const handoff = await createHandoff(enrichRuntime(body))
71
+ const input = enrichRuntime(body)
72
+ if (keyedHandoff(body, (input.runtime as any)?.claude?.cliSessionId)) return res.status(409).json(KEYED_SESSION_REFUSAL)
73
+ const handoff = await createHandoff(input)
57
74
  res.status(201).json({ handoff, context: buildHandoffPromptContext(handoff) })
58
75
  } catch (err: any) {
59
76
  res.status(500).json({ error: err?.message ?? 'handoff create failed' })
@@ -92,6 +109,10 @@ handoffsRouter.post('/handoffs/:code/claim', async (req, res) => {
92
109
  if (!code) return res.status(404).json({ error: 'handoff not found' })
93
110
  try {
94
111
  const claimedBy = typeof req.body?.claimedBy === 'string' ? req.body.claimedBy : 'unknown'
112
+ // Key lanes: a handoff saved before its session was known to be keyed is not claimed either.
113
+ if (anyKeyedSessions() && keyedHandoff((await getHandoff(code)) as Record<string, any> | null)) {
114
+ return res.status(409).json(KEYED_SESSION_REFUSAL)
115
+ }
95
116
  const handoff = await claimHandoff(code, claimedBy)
96
117
  if (!handoff) return res.status(404).json({ error: 'handoff not found' })
97
118
  res.json({ handoff, context: buildHandoffPromptContext(handoff) })
@@ -32,6 +32,7 @@ import { meetingAskCapabilities } from '../lib/meeting-ask-runtime.js'
32
32
  import { lensGistPublicHealth } from '../lib/lens-gist.js'
33
33
  import { meetingSummaryLLMEnabled } from '../lib/meeting-summary.js'
34
34
  import { getKeyStatus } from '../lib/openai-key.js'
35
+ import { keyLanesHealthFields, keyProfilesModelsField } from '../lib/key-lanes.js'
35
36
  import {
36
37
  getCodexModelCatalog,
37
38
  getCodexModelCatalogSnapshot,
@@ -550,6 +551,8 @@ healthRouter.get('/health', async (_req, res) => {
550
551
  // count only (health is public); the items and their Retry are behind the token at
551
552
  // GET /api/maintenance/status.
552
553
  crash_loop_parking: crashLoopParkingHealth(),
554
+ // Key lanes (plan v4 3.1): `details.keyLanes` only while enabled. Absent, never null, when off.
555
+ ...keyLanesHealthFields(),
553
556
  })
554
557
  })
555
558
 
@@ -616,6 +619,8 @@ healthRouter.get('/models', async (req, res) => {
616
619
  refreshedAt: ollamaCatalog.refreshedAt,
617
620
  },
618
621
  ollamaReady: isOllamaProviderReady(),
622
+ // Key lanes (plan v4 3.1): `keyProfiles` only while a profile is active. Absent when off.
623
+ ...keyProfilesModelsField(),
619
624
  serverInstanceId: getServerInstanceId(),
620
625
  // Minted here rather than on a route of its own: this response is already
621
626
  // authenticated, and the mint is stateless, so it costs nothing and stores