@gotcos/glasses-server 6.78.0 → 6.78.1

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,3 +1,11 @@
1
+ ## 6.78.1 - Glasses messages reach open Claude sessions again, whatever the size of ~/.claude/projects
2
+
3
+ Hotfix on 6.78.0. No companion or COS Control change.
4
+
5
+ - **What broke.** To decide whether an open Claude session is idle, the server finds its transcript by walking `~/.claude/projects`, and it refused the walk past 2,048 folders. Every Claude transcript then read as missing, so occupancy answered `live_desktop_process` for every open session: queued glasses messages waited until they expired (the lens said "Waiting for the current turn to end"), and live delivery through the peer inbox could not confirm either. On Miles's Mac the count passed 2,048 on 2026-10-17 12:33, almost all of it empty folders left by COS's own stateless `claude -p` calls (fixed in the COS scripts separately). The ceiling dates from 6.36; 6.77.0 has the same refusal.
6
+ - **The fix.** The transcript path Claude Code reports in every hook event (`transcript_path`, already in the session signal store) is used first, registered at boot so every reader of Claude transcripts gets it: no walk, no folder limit. It is used only when it is `<projects>/<one folder>/<session id>.jsonl` and the file exists; anything else falls back to the walk, unchanged. The walk ceiling rises to 8,192, and a refusal now logs one `[native-head]` line at most once an hour instead of failing silently.
7
+ - **Checks.** Full suite 7,715 passed, 2 skipped. 9 of 9 guard mutants killed by named tests.
8
+
1
9
  ## 6.78.0 - Steer from the glasses, Retry and Send again, and crash safety for Asks and desk turns (Pi Durable parity)
2
10
 
3
11
  Pairs with COS Glasses companion 6.10.645. Includes 6.77.0. No COS Control change is required (0.5.322 and 0.5.323 work; see the client matrix). Miles, 2026-10-18: steer from the glasses and retry, for Messages and Sessions; no regressions; better offline to online. Plan: Pi Durable parity build v3.1.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.78.0",
3
+ "version": "6.78.1",
4
4
  "description": "COS Glasses: 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": {
package/server/index.ts CHANGED
@@ -70,7 +70,7 @@ import { nativeQueueControl, plainQueueText } from './lib/codex-queue-control.js
70
70
  import { makeCodexLiveDeliverer } from './lib/codex-live-queue-deps.js'
71
71
  import { forkThread, realForkDeps } from './lib/fork-thread.js'
72
72
  import { ForkJobLedger, fileForkJobPersistence } from './lib/fork-job-ledger.js'
73
- import { nativeHead, realNativeHeadDeps } from './lib/native-head.js'
73
+ import { nativeHead, realNativeHeadDeps, registerClaudeTranscriptHint } from './lib/native-head.js'
74
74
  import { threadOccupancy, holderActivity } from './lib/thread-occupancy.js'
75
75
  import { displayRouter } from './routes/display.js'
76
76
  import { transcribeStreamRouter, getActiveTranscriptionSessionCount } from './routes/transcribe-stream.js'
@@ -536,6 +536,9 @@ bindingReapTimer.unref()
536
536
  // re-deriving roots per request.
537
537
  const occupancyDirs = realOccupancyDirs()
538
538
  const nativeHeadDeps = realNativeHeadDeps()
539
+ // 6.78.1: Claude Code's own transcript path from the hooks, so finding a session's transcript
540
+ // needs no walk of ~/.claude/projects (which refused past its folder limit; see native-head.ts).
541
+ registerClaudeTranscriptHint(sessionId => signalFor(sessionId)?.transcriptPath ?? null)
539
542
  const attachedWorkspaceDeps = realAttachedWorkspaceDeps(nativeHeadDeps)
540
543
  // 6.62.0: one set of readers for the Continue plan, rows, detail and the attach verdict.
541
544
  const continueFactsDeps = realContinueFactsDeps()
@@ -119,7 +119,7 @@
119
119
 
120
120
  import { createHash } from 'node:crypto'
121
121
  import { closeSync, constants as fsConstants, fstatSync, openSync, readSync, readdirSync, statSync } from 'node:fs'
122
- import { join } from 'node:path'
122
+ import { basename, dirname, join, resolve } from 'node:path'
123
123
  import { agentSessionRoots, idFromCodexFilename } from './agent-session-store.js'
124
124
  import { cursorTranscriptPath } from './cursor-agent-store.js'
125
125
  import { isValidNativeThreadId } from './native-thread-id.js'
@@ -177,8 +177,16 @@ export const TAIL_WINDOW_BYTES_BY_PROVIDER: Record<NativeHeadProvider, number> =
177
177
  */
178
178
  export const HEAD_ROW_COUNT = 3
179
179
 
180
- /** Ceilings that keep a pathological tree from turning attach into a directory walk. */
181
- export const MAX_CLAUDE_PROJECT_DIRS = 2048
180
+ /**
181
+ * Ceilings that keep a pathological tree from turning attach into a directory walk.
182
+ *
183
+ * 6.78.1: Claude raised from 2048. COS's own stateless `claude -p` calls each left an empty
184
+ * project folder behind (about 100 to 270 a day on Miles's Mac); at 2,049 the walk refused,
185
+ * every Claude transcript read as missing, and every glasses message to an open Claude session
186
+ * waited from 2026-10-17 12:33 until it expired. A session whose hooks name its transcript
187
+ * no longer walks at all (`hintedClaudeTranscript`); this ceiling now bounds only the rest.
188
+ */
189
+ export const MAX_CLAUDE_PROJECT_DIRS = 8192
182
190
  export const MAX_CODEX_DIR_ENTRIES = 512
183
191
  export const MAX_CODEX_FILES_SCANNED = 20_000
184
192
 
@@ -224,6 +232,13 @@ export interface NativeHeadDeps {
224
232
  * as `agentSessionLines`' injectable head/tail sizes.
225
233
  */
226
234
  tailWindowBytes?: number
235
+ /**
236
+ * 6.78.1: where Claude Code itself says this session's transcript is (the `transcript_path`
237
+ * its hook events carry), or null. Optional: absent, or null, means walk the project folders
238
+ * as before. Validated before use (`hintedClaudeTranscript`), so a wrong hint can only fall
239
+ * back to the walk, never name another file.
240
+ */
241
+ claudeTranscriptHint?: (sessionId: string) => string | null
227
242
  }
228
243
 
229
244
  /**
@@ -406,6 +421,41 @@ export function transcriptPathFor(
406
421
  }
407
422
  }
408
423
 
424
+ /**
425
+ * The hook's transcript path, only if it is exactly `<root>/<one folder>/<threadId>.jsonl` and
426
+ * the file exists. The walk's two-copies refusal is not repeated here: this is the file Claude
427
+ * Code reported writing for this very session, which is stronger evidence than a name match.
428
+ */
429
+ function hintedClaudeTranscript(filename: string, root: string, threadId: string, deps: NativeHeadDeps): string | null {
430
+ if (typeof deps.claudeTranscriptHint !== 'function') return null
431
+ let hint: string | null
432
+ try {
433
+ hint = deps.claudeTranscriptHint(threadId)
434
+ } catch {
435
+ return null
436
+ }
437
+ if (typeof hint !== 'string') return null
438
+ const path = resolve(hint)
439
+ if (basename(path) !== filename) return null
440
+ if (dirname(dirname(path)) !== resolve(root)) return null
441
+ return deps.fileExists(path) ? path : null
442
+ }
443
+
444
+ /** Said at most once an hour: the walk refusing is otherwise silent, and it was for 26 hours. */
445
+ let lastOverflowWarnAt = 0
446
+ function warnClaudeProjectsOverflow(root: string, count: number): void {
447
+ const now = Date.now()
448
+ if (now - lastOverflowWarnAt < 60 * 60_000) return
449
+ lastOverflowWarnAt = now
450
+ console.warn(`[native-head] ${count} folders in ${root}, over the ${MAX_CLAUDE_PROJECT_DIRS} walk limit: Claude sessions without a hook-reported transcript read as missing`)
451
+ }
452
+
453
+ /** Where index.ts registers the hook lookup, so every `realNativeHeadDeps()` gets it. */
454
+ let claudeTranscriptHintSource: ((sessionId: string) => string | null) | null = null
455
+ export function registerClaudeTranscriptHint(read: ((sessionId: string) => string | null) | null): void {
456
+ claudeTranscriptHintSource = read
457
+ }
458
+
409
459
  function cursorAgentTranscriptPath(threadId: string, deps: NativeHeadDeps): string | null {
410
460
  return cursorTranscriptPath(threadId, deps.dirs.cursorProjectsDir)
411
461
  }
@@ -414,10 +464,16 @@ function claudeTranscriptPath(threadId: string, deps: NativeHeadDeps): string |
414
464
  const root = deps.dirs.claudeProjectsDir
415
465
  if (!deps.dirExists(root)) return null
416
466
 
467
+ const filename = `${threadId}.jsonl`
468
+ const hinted = hintedClaudeTranscript(filename, root, threadId, deps)
469
+ if (hinted !== null) return hinted
470
+
417
471
  const folders = deps.readDir(root)
418
- if (folders.length > MAX_CLAUDE_PROJECT_DIRS) return null
472
+ if (folders.length > MAX_CLAUDE_PROJECT_DIRS) {
473
+ warnClaudeProjectsOverflow(root, folders.length)
474
+ return null
475
+ }
419
476
 
420
- const filename = `${threadId}.jsonl`
421
477
  let found: string | null = null
422
478
  for (const folder of folders) {
423
479
  const candidate = join(root, folder, filename)
@@ -660,5 +716,8 @@ function readTail(path: string, maxBytes: number): TailRead | null {
660
716
  }
661
717
 
662
718
  export function realNativeHeadDeps(dirs: NativeHeadDirs = realNativeHeadDirs()): NativeHeadDeps {
663
- return { dirs, dirExists, readDir, fileExists, readTail }
719
+ return {
720
+ dirs, dirExists, readDir, fileExists, readTail,
721
+ claudeTranscriptHint: sessionId => (claudeTranscriptHintSource === null ? null : claudeTranscriptHintSource(sessionId)),
722
+ }
664
723
  }