@gotcos/glasses-server 6.79.2 → 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.
- package/.env.example +9 -0
- package/CHANGELOG.md +19 -0
- package/README.md +10 -0
- package/managed-runtime-contract.json +1 -0
- package/package.json +9 -1
- package/server/index.ts +11 -0
- 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/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/openai-compat.ts +28 -3
- package/server/routes/query.ts +20 -6
- package/shared/key-preference.ts +57 -0
- 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
|
-
|
|
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:
|
|
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 =
|
|
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 =
|
|
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?:
|
|
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 =
|
|
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?:
|
|
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 =
|
|
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:
|
|
721
|
-
):
|
|
722
|
-
return
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|