@gotcos/glasses-server 6.28.0 → 6.29.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.
@@ -0,0 +1,200 @@
1
+ // Does this server offer Continue at all, and for what?
2
+ //
3
+ // The Continue/Fork affordance is a UI decision that has to be made BEFORE any
4
+ // thread is named, so COS Control and the phone cannot discover it the way they
5
+ // discover everything else about a thread — by asking
6
+ // /api/agent-sessions/:provider/:threadId/attachability. When the feature is off
7
+ // the two write routes are not registered at all, so a client that reaches for
8
+ // them gets a bare 404 with no reason and no copy, which is indistinguishable
9
+ // from a typo, a proxy, or a dead server. This module is what a client reads
10
+ // instead, once, up front.
11
+ //
12
+ // ---------------------------------------------------------------- the rule
13
+ //
14
+ // ABSENT MEANS DISABLED. NEVER ENABLED.
15
+ //
16
+ // These fields did not exist before this build. Every client older than it will
17
+ // read `undefined` from all three, and `undefined` MUST resolve to off — the same
18
+ // fail-closed posture as the rest of Continue Original Agent Thread, for the same
19
+ // reason: a client that guesses "the field is missing, so probably fine" offers a
20
+ // Continue button that writes into a real human's conversation, and the cost of
21
+ // guessing the other way is one Fork.
22
+ //
23
+ // That rule cannot be enforced by the wire format, because absence is exactly what
24
+ // an old client sees and an old client cannot be changed. So it is enforced by
25
+ // `readThreadAttachCapability`, which is the ONLY sanctioned way to interpret this
26
+ // surface, and by the test that feeds it a genuine older-shaped /api/health body
27
+ // and requires all three answers to come back off.
28
+ //
29
+ // `supported` exists solely to make the two OFFs distinguishable. Without it,
30
+ // "this server has never heard of Continue" and "this server can Continue but the
31
+ // user has not switched it on" are the same empty answer, and a client cannot tell
32
+ // a user to flip a setting that may not exist. It is a compile-time constant: this
33
+ // build has the code. Whether the routes are reachable is `enabled`.
34
+ //
35
+ // ------------------------------------------------------------ anti-drift
36
+ //
37
+ // `enabled` reads `threadAttachEnabled()` — the SAME function
38
+ // routes/agent-session-bindings.ts calls to decide whether to register the two
39
+ // POST routes. Not a second copy of `process.env.COS_THREAD_ATTACH_ENABLED === '1'`.
40
+ // A copy is how this surface comes to advertise a write path that 404s, and the
41
+ // repo already carries that lesson one import above the call site in
42
+ // routes/health.ts: MEDIA_CHUNKED_UPLOAD_ENABLED is imported from the route that
43
+ // owns the endpoints "so it cannot drift from whether they are actually
44
+ // registered."
45
+ //
46
+ // `providers` is sourced from BINDABLE_PROVIDERS for the same reason. Cursor must
47
+ // not appear (plan 2.5 makes it Fork-only), and the way to guarantee that is to
48
+ // read the list that `isBindableProvider` — the check the attach route itself
49
+ // applies — is built from, rather than restating two names here and hoping.
50
+ //
51
+ // ------------------------------------------------- why providers empties out
52
+ //
53
+ // When the gate is off, `providers` is `[]` rather than the list this build could
54
+ // drive if it were on. A client that reads only `threadAttachProviders` and never
55
+ // looks at `threadAttachEnabled` is then still correct. Every field independently
56
+ // resolves to "no" when the answer is no; there is no combination of fields a
57
+ // careless reader can pick that yields a Continue button pointing at an
58
+ // unregistered route.
59
+ //
60
+ // The cost is that a disabled server cannot tell an operator WHICH providers it
61
+ // would support. That is a settings-copy problem, and `supported: true` is enough
62
+ // to say "this build can do it, turn it on."
63
+
64
+ import {
65
+ BINDABLE_PROVIDERS,
66
+ isBindableProvider,
67
+ type BindableProvider,
68
+ } from './agent-session-binding-store.js'
69
+ import { threadAttachEnabled } from '../routes/agent-session-bindings.js'
70
+
71
+ /**
72
+ * This build has the feature compiled in.
73
+ *
74
+ * A constant, not a probe. There is no configuration under which the code is
75
+ * absent from a build that contains this file; the reachable/unreachable question
76
+ * is `enabled`. It is published so that `undefined` (an older server) and `false`
77
+ * (this build, switched off) are different answers on the wire.
78
+ */
79
+ export const THREAD_ATTACH_SUPPORTED = true
80
+
81
+ export interface ThreadAttachCapability {
82
+ /** The build knows what Continue is. False only when read off an older payload. */
83
+ supported: boolean
84
+ /** The write routes are registered and a Continue may be attempted. */
85
+ enabled: boolean
86
+ /**
87
+ * Providers that can be CONTINUED, not merely browsed or forked.
88
+ *
89
+ * Empty whenever `enabled` is false. Cursor is never a member: it is Fork-only.
90
+ */
91
+ providers: BindableProvider[]
92
+ /**
93
+ * Fork is available. NOT gated by `enabled`.
94
+ *
95
+ * Fork creates a new thread and leaves the source byte-identical, so the flag
96
+ * that protects an existing conversation does not apply. Gating it produced an
97
+ * incoherent default: every refusal recommends Fork while the route 404s.
98
+ */
99
+ forkSupported: boolean
100
+ }
101
+
102
+ /**
103
+ * The capability as this server should publish it.
104
+ *
105
+ * @param gate Tests only. Production takes the default, which is the same
106
+ * function that decides whether the write routes exist. Passing anything else
107
+ * from production code re-opens the drift this module was written to close.
108
+ */
109
+ export function threadAttachCapability(
110
+ gate: () => boolean = threadAttachEnabled,
111
+ ): ThreadAttachCapability {
112
+ let enabled = false
113
+ try {
114
+ // Anything other than an exact `true` is off. A gate that answers "maybe" is
115
+ // answering no, and a gate that throws has not answered at all.
116
+ enabled = gate() === true
117
+ } catch (error) {
118
+ console.error(
119
+ `[thread-attach-capability] gate threw: ${error instanceof Error ? error.message : error}`,
120
+ )
121
+ enabled = false
122
+ }
123
+ return {
124
+ supported: THREAD_ATTACH_SUPPORTED,
125
+ enabled,
126
+ providers: enabled ? [...BINDABLE_PROVIDERS] : [],
127
+ // NOT gated by `enabled`: fork creates a new thread and leaves the source
128
+ // byte-identical, so the flag protecting an existing conversation does not
129
+ // apply. Gating it made every refusal recommend a route that 404s.
130
+ forkSupported: THREAD_ATTACH_SUPPORTED,
131
+ }
132
+ }
133
+
134
+ /** The three keys as they appear on /api/health and /api/models. */
135
+ export interface ThreadAttachHealthFields {
136
+ threadAttachSupported: boolean
137
+ threadAttachEnabled: boolean
138
+ /** Fork is available whenever the build supports it, gate or no gate. */
139
+ threadForkSupported: boolean
140
+ threadAttachProviders: BindableProvider[]
141
+ }
142
+
143
+ /** Flatten the capability onto the health payload's key names. */
144
+ export function threadAttachHealthFields(
145
+ capability: ThreadAttachCapability,
146
+ ): ThreadAttachHealthFields {
147
+ return {
148
+ threadAttachSupported: capability.supported,
149
+ threadAttachEnabled: capability.enabled,
150
+ threadAttachProviders: capability.providers,
151
+ threadForkSupported: capability.forkSupported,
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Interpret a health payload from ANY server, including one that predates these
157
+ * fields entirely.
158
+ *
159
+ * The whole point of this function is the defaults, so read them as the contract:
160
+ *
161
+ * - a payload that is not an object at all -> everything off
162
+ * - `threadAttachSupported` anything but exactly `true` -> not supported, and
163
+ * therefore not enabled, whatever the other two fields say
164
+ * - `threadAttachEnabled` anything but exactly `true` -> off, so `'true'`, `1`
165
+ * and `'yes'` from a hand-rolled or proxied payload all fail closed
166
+ * - `providers` dropped entirely unless enabled, and filtered to names THIS
167
+ * build can actually drive, so a newer server naming a fourth provider does
168
+ * not make an older client offer a Continue it cannot perform
169
+ *
170
+ * A payload that contradicts itself — enabled without supported, or providers
171
+ * while disabled — is a defect somewhere, and a defect resolves to refuse.
172
+ */
173
+ export function readThreadAttachCapability(payload: unknown): ThreadAttachCapability {
174
+ const row =
175
+ payload && typeof payload === 'object' && !Array.isArray(payload)
176
+ ? (payload as Record<string, unknown>)
177
+ : null
178
+
179
+ const supported = row?.threadAttachSupported === true
180
+ const enabled = supported && row?.threadAttachEnabled === true
181
+
182
+ const providers: BindableProvider[] = []
183
+ const raw = row?.threadAttachProviders
184
+ if (enabled && Array.isArray(raw)) {
185
+ for (const entry of raw) {
186
+ // `isBindableProvider` is the attach route's own check. Anything it rejects
187
+ // is a provider this build has no code path for, so offering it would
188
+ // produce a Continue that cannot be honoured.
189
+ if (!isBindableProvider(entry)) continue
190
+ if (providers.includes(entry)) continue
191
+ providers.push(entry)
192
+ }
193
+ }
194
+
195
+ // Read off a payload from another server. An older build has no fork route, and
196
+ // its payload carries no such field, so absent must read as NOT supported — the
197
+ // same fail-closed rule the rest of this module runs on.
198
+ const forkSupported = row?.threadForkSupported === true
199
+ return { supported, enabled, providers, forkSupported }
200
+ }