@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 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
- Managed-server updates pass their Claude readiness proof again.
4
-
5
- - **The proof no longer loads your MCP catalog.** COS Control verifies a
6
- candidate server by running one real `claude -p --model haiku` query with
7
- no tools. On Claude Code 2.1.251, `--tools ''` still loads every MCP
8
- server's tool definitions, and on a Mac with a large fleet that alone was
9
- about 244,000 tokens against Haiku's 200,000-token window: "Prompt is too
10
- long", exit 1, zero API time, and the update rolled back to the previous
11
- server every time (six attempts on 6.43.1). The proof now passes
12
- `--strict-mcp-config` with an empty MCP config, which keeps it under
13
- 20,000 tokens and answers in about two seconds. Nothing else changed.
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.2",
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. Claude Code 2.1.251 turns `--tools ''`
114
- * into "load the whole catalog, expose none of it", and on a Mac with a
115
- * large MCP fleet that catalog alone was ~244K tokens against Haiku's 200K
116
- * window: "Prompt is too long", exit 1, zero API time. Six 6.43.1 update
117
- * attempts rolled back on 2026-09-01/02 before a single request was made.
118
- * An explicit empty config with --strict-mcp-config keeps the proof under
119
- * 20K tokens and is what a readiness check should be anyway. */
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
- const result = provider === 'claude'
178
- ? await runBounded('claude', claudeProofArgs(), '', CLAUDE_PROOF_TIMEOUT_MS, signal)
179
- : await runBounded('codex', [
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
- ...(ok ? {} : { error: safeProofError(result) }),
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 !== 'claude' && provider !== 'codex') {
21
- return res.status(400).json({ error: 'provider must be claude or codex' })
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'