@gotcos/glasses-server 6.43.2 → 6.43.3
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/CHANGELOG.md +32 -11
- package/package.json +1 -1
- package/server/lib/provider-proof.ts +119 -14
- package/server/routes/provider-proof.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
+
## 6.43.3
|
|
2
|
+
|
|
3
|
+
Managed-server updates no longer hinge on one vendor's meter.
|
|
4
|
+
|
|
5
|
+
- **The readiness proof reports why it failed, as a code.** COS Control
|
|
6
|
+
verifies a candidate server with one real no-tool query per installed
|
|
7
|
+
provider. On 2026-09-01 Claude had hit its session limit, Codex proved in
|
|
8
|
+
seven seconds, and six 6.43.1 updates still rolled back because the only
|
|
9
|
+
answer was "provider process exited 1". `POST /api/diagnostics/provider-proof`
|
|
10
|
+
now returns `code`: `provider_quota` (vendor session or usage limit),
|
|
11
|
+
`provider_auth` (not signed in), `provider_context_overflow`,
|
|
12
|
+
`provider_missing`, `provider_timeout`, `provider_canceled`,
|
|
13
|
+
`provider_bad_answer`, or `provider_failed`. The `error` sentence is one
|
|
14
|
+
fixed phrase per code; vendor text never leaves the server. COS Control
|
|
15
|
+
0.5.184 uses the code to treat a quota as a skip when another provider
|
|
16
|
+
proves (on 6.43.2 and older it classifies the error sentence instead).
|
|
17
|
+
- **Cursor is a proof provider.** `{"provider":"cursor"}` runs the Cursor
|
|
18
|
+
`agent` in documented read-only ask mode with the prompt on stdin and reads
|
|
19
|
+
the result event, so a Mac with Cursor signed in can prove an update while
|
|
20
|
+
Claude and Codex are both at their limits.
|
|
21
|
+
|
|
1
22
|
## 6.43.2
|
|
2
23
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
24
|
+
What actually reached npm under this number (published 2026-09-02 07:27 CDT
|
|
25
|
+
from the commit before the proof codes landed):
|
|
26
|
+
|
|
27
|
+
- **The Claude proof states its empty MCP config explicitly**
|
|
28
|
+
(`--strict-mcp-config --mcp-config '{"mcpServers":{}}'`) instead of relying
|
|
29
|
+
on `CLAUDE_CODE_SAFE_MODE` alone. Without safe mode, `--tools ''` on Claude
|
|
30
|
+
Code 2.1.251 still loads the whole catalog, which on a large fleet is a
|
|
31
|
+
244K-token request Haiku refuses before any API call. Belt and braces; the
|
|
32
|
+
2026-09-01 rollbacks were the session limit, not this.
|
|
33
|
+
- The message-reservation rationale in code and changelog states the
|
|
34
|
+
observed double #74 accurately (one exchange shown twice by the companion).
|
|
14
35
|
|
|
15
36
|
## 6.43.1
|
|
16
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.43.
|
|
3
|
+
"version": "6.43.3",
|
|
4
4
|
"description": "COS Glasses \u2014 self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, Cursor Agent CLI, or local Ollama",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
import { cosBrainDir } from './launch-dir.js'
|
|
3
|
+
import { resolveAgentBinary } from './cursor-model-catalog.js'
|
|
3
4
|
import { terminateProviderProcess } from './provider-process-lifecycle.js'
|
|
4
5
|
|
|
5
|
-
export type ProofProvider = 'claude' | 'codex'
|
|
6
|
+
export type ProofProvider = 'claude' | 'codex' | 'cursor'
|
|
7
|
+
export const PROOF_PROVIDERS: readonly ProofProvider[] = ['claude', 'codex', 'cursor']
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Why a proof did not pass, as a stable code COS Control can branch on
|
|
11
|
+
* without reading logs. `provider_quota` is the one that matters: a vendor
|
|
12
|
+
* session or usage limit is a fact about the vendor's meter, not about the
|
|
13
|
+
* candidate server, and an update must not roll back on it when another
|
|
14
|
+
* provider proves. (2026-09-01: six 6.43.1 updates rolled back on Claude's
|
|
15
|
+
* session limit while Codex proved in 7 s.)
|
|
16
|
+
*/
|
|
17
|
+
export type ProviderProofCode =
|
|
18
|
+
| 'provider_quota'
|
|
19
|
+
| 'provider_auth'
|
|
20
|
+
| 'provider_context_overflow'
|
|
21
|
+
| 'provider_missing'
|
|
22
|
+
| 'provider_timeout'
|
|
23
|
+
| 'provider_canceled'
|
|
24
|
+
| 'provider_bad_answer'
|
|
25
|
+
| 'provider_failed'
|
|
6
26
|
|
|
7
27
|
export interface ProviderProofResult {
|
|
8
28
|
provider: ProofProvider
|
|
@@ -10,6 +30,7 @@ export interface ProviderProofResult {
|
|
|
10
30
|
durationMs: number
|
|
11
31
|
cached: boolean
|
|
12
32
|
error?: string
|
|
33
|
+
code?: ProviderProofCode
|
|
13
34
|
}
|
|
14
35
|
|
|
15
36
|
const PROOF_TOKEN = 'COS_CONTROL_OK'
|
|
@@ -110,13 +131,14 @@ export function runBounded(
|
|
|
110
131
|
export const CLAUDE_PROOF_MODEL = 'haiku'
|
|
111
132
|
export const CLAUDE_PROOF_TIMEOUT_MS = 45_000
|
|
112
133
|
|
|
113
|
-
/** No MCP servers for the proof
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
134
|
+
/** No MCP servers for the proof, stated explicitly rather than relied on
|
|
135
|
+
* from CLAUDE_CODE_SAFE_MODE alone. Without safe mode, Claude Code 2.1.251
|
|
136
|
+
* turns `--tools ''` into "load the whole catalog, expose none of it", and
|
|
137
|
+
* on a Mac with a large MCP fleet that catalog alone is ~244K tokens against
|
|
138
|
+
* Haiku's 200K window ("Prompt is too long", exit 1, zero API time). Safe
|
|
139
|
+
* mode currently prevents that; an explicit empty config with
|
|
140
|
+
* --strict-mcp-config keeps the proof under 20K tokens whatever a future
|
|
141
|
+
* CLI does with the safe-mode flag. */
|
|
120
142
|
export const CLAUDE_PROOF_MCP_CONFIG = '{"mcpServers":{}}'
|
|
121
143
|
|
|
122
144
|
export function claudeProofArgs(): string[] {
|
|
@@ -172,11 +194,85 @@ function safeProofError(result: ProcessResult): string {
|
|
|
172
194
|
return 'provider returned no valid proof response'
|
|
173
195
|
}
|
|
174
196
|
|
|
197
|
+
const QUOTA_RE = /usage limit|hit your (?:usage |session )?limit|session limit|rate.?limit|too many requests|\b429\b|\bquota\b|resets? (?:at|in) |over capacity|overloaded|\b529\b|plan limit/i
|
|
198
|
+
const AUTH_RE = /not logged in|please (?:log ?in|sign in)|invalid api key|authentication|unauthori[sz]ed|\b401\b|\b403\b|login required|token (?:has )?expired|no credentials/i
|
|
199
|
+
const OVERFLOW_RE = /prompt is too long|context window|too many tokens|exceeds the (?:model|context)/i
|
|
200
|
+
const MISSING_RE = /ENOENT|command not found|no such file/i
|
|
201
|
+
|
|
202
|
+
/** Vendor text the provider printed, so the code can be derived without ever
|
|
203
|
+
* returning that text to a caller. Claude's JSON `result` carries the error
|
|
204
|
+
* sentence on `is_error`; Codex and Cursor print it on stderr or as a
|
|
205
|
+
* result/error event. */
|
|
206
|
+
export function classifyProofFailure(result: ProcessResult, answer: string, expected = PROOF_TOKEN): ProviderProofCode {
|
|
207
|
+
if (result.aborted) return 'provider_canceled'
|
|
208
|
+
if (result.timedOut) return 'provider_timeout'
|
|
209
|
+
const haystack = `${result.stderr}\n${result.stdout}`.slice(0, 40_000)
|
|
210
|
+
if (result.code === null && MISSING_RE.test(haystack)) return 'provider_missing'
|
|
211
|
+
if (OVERFLOW_RE.test(haystack)) return 'provider_context_overflow'
|
|
212
|
+
if (QUOTA_RE.test(haystack)) return 'provider_quota'
|
|
213
|
+
if (AUTH_RE.test(haystack)) return 'provider_auth'
|
|
214
|
+
if (result.code === 0) return answer === expected ? 'provider_failed' : 'provider_bad_answer'
|
|
215
|
+
return 'provider_failed'
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** One safe sentence per code; never the vendor's text. */
|
|
219
|
+
export function describeProofCode(code: ProviderProofCode, result: ProcessResult): string {
|
|
220
|
+
switch (code) {
|
|
221
|
+
case 'provider_quota': return 'provider session or usage limit reached'
|
|
222
|
+
case 'provider_auth': return 'provider is not signed in'
|
|
223
|
+
case 'provider_context_overflow': return 'provider refused the proof prompt as too long'
|
|
224
|
+
case 'provider_missing': return 'provider binary is not installed or not on PATH'
|
|
225
|
+
case 'provider_timeout': return 'provider proof timed out'
|
|
226
|
+
case 'provider_canceled': return 'provider proof canceled'
|
|
227
|
+
case 'provider_bad_answer': return 'provider answered but not with the proof token'
|
|
228
|
+
default: return safeProofError(result)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const CURSOR_PROOF_TIMEOUT_MS = 120_000
|
|
233
|
+
|
|
234
|
+
/** Cursor `agent` in documented read-only ask mode, one stream-json turn, the
|
|
235
|
+
* prompt on stdin like the bridge. No `--model`: the account default is the
|
|
236
|
+
* readiness question, not any particular model. */
|
|
237
|
+
export function cursorProofArgs(workspace = cosBrainDir() ?? process.cwd()): string[] {
|
|
238
|
+
return ['-p', '--mode', 'ask', '--output-format', 'stream-json', '--trust', '--workspace', workspace]
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The final `result` event wins; assistant deltas with a timestamp are the
|
|
242
|
+
* fallback, mirroring cursor-bridge's extractCursorResponseText. */
|
|
243
|
+
export function cursorProofText(stdout: string): string {
|
|
244
|
+
let deltas = ''
|
|
245
|
+
for (const line of stdout.split('\n')) {
|
|
246
|
+
if (!line.trim()) continue
|
|
247
|
+
let event: any
|
|
248
|
+
try { event = JSON.parse(line) } catch { continue }
|
|
249
|
+
const type = String(event?.type ?? '').toLowerCase()
|
|
250
|
+
if (type === 'result') {
|
|
251
|
+
if (event?.subtype === 'success' && event?.is_error !== true && typeof event?.result === 'string') return event.result.trim()
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
if (type !== 'assistant' || typeof event?.timestamp_ms !== 'number') continue
|
|
255
|
+
if (event?.model_call_id != null && event.model_call_id !== '') continue
|
|
256
|
+
const content = event?.message?.content
|
|
257
|
+
if (Array.isArray(content)) {
|
|
258
|
+
for (const block of content) {
|
|
259
|
+
if (typeof block === 'string') deltas += block
|
|
260
|
+
else if (typeof block?.text === 'string') deltas += block.text
|
|
261
|
+
}
|
|
262
|
+
} else if (typeof event?.text === 'string') {
|
|
263
|
+
deltas += event.text
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return deltas.trim()
|
|
267
|
+
}
|
|
268
|
+
|
|
175
269
|
async function executeProof(provider: ProofProvider, signal?: AbortSignal): Promise<ProviderProofResult> {
|
|
176
270
|
const started = Date.now()
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
271
|
+
let result: ProcessResult
|
|
272
|
+
if (provider === 'claude') {
|
|
273
|
+
result = await runBounded('claude', claudeProofArgs(), '', CLAUDE_PROOF_TIMEOUT_MS, signal)
|
|
274
|
+
} else if (provider === 'codex') {
|
|
275
|
+
result = await runBounded('codex', [
|
|
180
276
|
'exec',
|
|
181
277
|
'--sandbox', 'read-only',
|
|
182
278
|
'--skip-git-repo-check',
|
|
@@ -185,16 +281,25 @@ async function executeProof(provider: ProofProvider, signal?: AbortSignal): Prom
|
|
|
185
281
|
'--ephemeral',
|
|
186
282
|
'-',
|
|
187
283
|
], PROOF_PROMPT, 120_000, signal)
|
|
284
|
+
} else {
|
|
285
|
+
const binary = resolveAgentBinary()
|
|
286
|
+
result = binary
|
|
287
|
+
? await runBounded(binary, cursorProofArgs(), PROOF_PROMPT, CURSOR_PROOF_TIMEOUT_MS, signal)
|
|
288
|
+
: { code: null, stdout: '', stderr: 'ENOENT: cursor agent binary not found', timedOut: false, aborted: false }
|
|
289
|
+
}
|
|
188
290
|
const text = provider === 'claude'
|
|
189
291
|
? claudeProofText(result.stdout)
|
|
190
|
-
: codexProofText(result.stdout)
|
|
292
|
+
: provider === 'codex' ? codexProofText(result.stdout) : cursorProofText(result.stdout)
|
|
191
293
|
const ok = result.code === 0 && text === PROOF_TOKEN
|
|
294
|
+
if (ok) return { provider, ok, durationMs: Date.now() - started, cached: false }
|
|
295
|
+
const code = classifyProofFailure(result, text)
|
|
192
296
|
return {
|
|
193
297
|
provider,
|
|
194
|
-
ok,
|
|
298
|
+
ok: false,
|
|
195
299
|
durationMs: Date.now() - started,
|
|
196
300
|
cached: false,
|
|
197
|
-
|
|
301
|
+
error: describeProofCode(code, result),
|
|
302
|
+
code,
|
|
198
303
|
}
|
|
199
304
|
}
|
|
200
305
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Router } from 'express'
|
|
2
|
-
import { runProviderProof, type ProofProvider } from '../lib/provider-proof.js'
|
|
2
|
+
import { PROOF_PROVIDERS, runProviderProof, type ProofProvider } from '../lib/provider-proof.js'
|
|
3
3
|
import {
|
|
4
4
|
acquireMaintenanceWork,
|
|
5
5
|
maintenanceErrorPayload,
|
|
@@ -17,8 +17,8 @@ providerProofRouter.post('/diagnostics/provider-proof', async (req, res) => {
|
|
|
17
17
|
|| address.startsWith('127.') || address.startsWith('::ffff:127.')
|
|
18
18
|
if (!loopback) return res.status(403).json({ error: 'loopback_required' })
|
|
19
19
|
const provider = req.body?.provider
|
|
20
|
-
if (provider
|
|
21
|
-
return res.status(400).json({ error: 'provider must be claude or
|
|
20
|
+
if (!PROOF_PROVIDERS.includes(provider)) {
|
|
21
|
+
return res.status(400).json({ error: 'provider must be claude, codex, or cursor', code: 'provider_unknown' })
|
|
22
22
|
}
|
|
23
23
|
const controllerProof = maintenanceOperationCredentialsValid({
|
|
24
24
|
leaseId: typeof req.headers['x-cos-maintenance-lease'] === 'string'
|