@gotcos/glasses-server 6.36.13 → 6.36.14

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,5 +1,17 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 6.36.14
4
+ - **The session LIST now reports what its caps hid.** The 7-day age gate, the
5
+ 20-per-provider cap, and the Cursor 32 MB skip used to drop rows with no
6
+ signal, so a 60-row list looked complete. `GET /api/agent-sessions` now carries
7
+ `dropped: { age, limit, oversized }`. Additive: an older client ignores the
8
+ key. Zero means the walk found nothing to hide, not that the caps are off.
9
+ Keep-warm titles and Codex files over 32 MB are not counted — Codex is listed
10
+ oversize on purpose; Cursor is the one that skips. Measured on this machine
11
+ before publish: 62 listed, **2,144** older than 7 days, **91** over the
12
+ per-provider cap, **0** oversized. The visible first/last ids match running
13
+ 6.36.13, so the list membership did not change — only the silence did.
14
+
3
15
  ## 6.36.13
4
16
  - **Codex was spawned by bare name in four places, and it only worked here by accident.**
5
17
  `codex` on PATH is a shell alias to `/Applications/Codex.app`, which does not exist; the real
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.36.13",
3
+ "version": "6.36.14",
4
4
  "description": "COS Glasses — self-hosted AI heads-up-display server for Even G2 smart glasses, powered by Claude Code, Codex, or Cursor Agent CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,17 @@ const CODEX_ROLLOUT_STAMP = /^rollout-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2}
27
27
  export type AgentProvider = 'claude' | 'codex' | 'cursor'
28
28
  export type AgentSessionSort = 'updated' | 'opened'
29
29
 
30
+ /** Caps that drop LIST rows with no other signal. Search has its own budget. */
31
+ export interface AgentSessionListDropped {
32
+ age: number
33
+ limit: number
34
+ oversized: number
35
+ }
36
+
37
+ export function emptySessionListDropped(): AgentSessionListDropped {
38
+ return { age: 0, limit: 0, oversized: 0 }
39
+ }
40
+
30
41
  export interface AgentSessionRow {
31
42
  session_id: string
32
43
  provider: AgentProvider
@@ -836,6 +847,7 @@ export async function listClaudeSessions(
836
847
  cap = AGENT_SESSION_PER_PROVIDER_LIMIT,
837
848
  starredIds: ReadonlySet<string> = new Set(),
838
849
  desktopSessionsRoot = '',
850
+ dropped: AgentSessionListDropped = emptySessionListDropped(),
839
851
  ): Promise<AgentSessionRow[]> {
840
852
  const seen = new Set<string>()
841
853
  const pinnedCandidates: Array<{ file: string; native: string; project: string; mtimeMs: number; birthtimeMs: number; desktop?: string }> = []
@@ -854,7 +866,10 @@ export async function listClaudeSessions(
854
866
  if (!st?.isFile) continue
855
867
  const pinned = starredIds.has(native.toLowerCase())
856
868
  const fresh = now.getTime() - st.mtimeMs <= AGENT_SESSION_MAX_AGE_MS
857
- if (!pinned && !fresh) continue
869
+ if (!pinned && !fresh) {
870
+ dropped.age += 1
871
+ continue
872
+ }
858
873
  const candidate = {
859
874
  file,
860
875
  native,
@@ -888,8 +903,12 @@ export async function listClaudeSessions(
888
903
 
889
904
  const toRows = async (candidates: typeof recentCandidates, limit: number): Promise<AgentSessionRow[]> => {
890
905
  const rows: AgentSessionRow[] = []
891
- for (const candidate of candidates) {
892
- if (rows.length >= Math.max(0, limit)) break
906
+ for (let i = 0; i < candidates.length; i++) {
907
+ if (rows.length >= Math.max(0, limit)) {
908
+ dropped.limit += candidates.length - i
909
+ break
910
+ }
911
+ const candidate = candidates[i]
893
912
  let title = ''
894
913
  let project = candidate.project
895
914
  let firstPrompt = ''
@@ -935,6 +954,7 @@ export async function listCodexSessions(
935
954
  sessionsRoot: string,
936
955
  now: Date,
937
956
  cap = AGENT_SESSION_PER_PROVIDER_LIMIT,
957
+ dropped: AgentSessionListDropped = emptySessionListDropped(),
938
958
  ): Promise<AgentSessionRow[]> {
939
959
  const names = await loadCodexThreadNames(sessionsRoot)
940
960
  const pinnedIds = await loadCodexPinnedIds(sessionsRoot)
@@ -947,7 +967,10 @@ export async function listCodexSessions(
947
967
  const fileId = idFromCodexFilename(name)
948
968
  const pinned = fileId ? pinnedIds.has(fileId) : false
949
969
  const fresh = now.getTime() - st.mtimeMs <= AGENT_SESSION_MAX_AGE_MS
950
- if (!pinned && !fresh) continue
970
+ if (!pinned && !fresh) {
971
+ dropped.age += 1
972
+ continue
973
+ }
951
974
  const candidate = { file, name, mtimeMs: st.mtimeMs, birthtimeMs: st.birthtimeMs }
952
975
  if (pinned) pinnedCandidates.push(candidate)
953
976
  else recentCandidates.push(candidate)
@@ -957,8 +980,12 @@ export async function listCodexSessions(
957
980
 
958
981
  const toRows = async (candidates: typeof recentCandidates, limit: number): Promise<AgentSessionRow[]> => {
959
982
  const rows: AgentSessionRow[] = []
960
- for (const candidate of candidates) {
961
- if (rows.length >= Math.max(0, limit)) break
983
+ for (let i = 0; i < candidates.length; i++) {
984
+ if (rows.length >= Math.max(0, limit)) {
985
+ dropped.limit += candidates.length - i
986
+ break
987
+ }
988
+ const candidate = candidates[i]
962
989
  const meta = await peekCodexMeta(candidate.file)
963
990
  if (!meta || meta.subagent) continue
964
991
  // THE FILENAME WINS WHEN THEY DISAGREE.
@@ -1011,6 +1038,7 @@ export async function listCursorSessions(
1011
1038
  cap = AGENT_SESSION_PER_PROVIDER_LIMIT,
1012
1039
  composerDb = '',
1013
1040
  pinnedIds: ReadonlySet<string> = new Set(),
1041
+ dropped: AgentSessionListDropped = emptySessionListDropped(),
1014
1042
  ): Promise<AgentSessionRow[]> {
1015
1043
  const composerNames = composerDb ? await loadCursorComposerNames(composerDb) : new Map<string, string>()
1016
1044
  const byId = new Map<string, { file: string; sessionDir: string; project: string; mtimeMs: number; birthtimeMs: number }>()
@@ -1024,9 +1052,15 @@ export async function listCursorSessions(
1024
1052
  const file = join(transcripts, sessionDir, `${sessionDir}.jsonl`)
1025
1053
  const st = await fileStat(file)
1026
1054
  if (!st?.isFile) continue
1027
- if (!pinned && st.size > AGENT_SESSION_MAX_FILE_BYTES) continue
1055
+ if (!pinned && st.size > AGENT_SESSION_MAX_FILE_BYTES) {
1056
+ dropped.oversized += 1
1057
+ continue
1058
+ }
1028
1059
  const fresh = now.getTime() - st.mtimeMs <= AGENT_SESSION_MAX_AGE_MS
1029
- if (!pinned && !fresh) continue
1060
+ if (!pinned && !fresh) {
1061
+ dropped.age += 1
1062
+ continue
1063
+ }
1030
1064
  const next = {
1031
1065
  file,
1032
1066
  sessionDir,
@@ -1045,8 +1079,12 @@ export async function listCursorSessions(
1045
1079
 
1046
1080
  const toRows = async (candidates: typeof recentCandidates, limit: number): Promise<AgentSessionRow[]> => {
1047
1081
  const rows: AgentSessionRow[] = []
1048
- for (const candidate of candidates) {
1049
- if (rows.length >= Math.max(0, limit)) break
1082
+ for (let i = 0; i < candidates.length; i++) {
1083
+ if (rows.length >= Math.max(0, limit)) {
1084
+ dropped.limit += candidates.length - i
1085
+ break
1086
+ }
1087
+ const candidate = candidates[i]
1050
1088
  const peek = await peekCursorDiscussion(candidate.file)
1051
1089
  const firstPrompt = await firstCursorUserTitle(candidate.file) ?? peek.lastUser ?? ''
1052
1090
  const title = composerNames.get(candidate.sessionDir)
@@ -1141,6 +1179,7 @@ export async function listAgentSessions(
1141
1179
  live: AgentSessionRow[] = [],
1142
1180
  limit = AGENT_SESSION_LIST_LIMIT,
1143
1181
  sort: AgentSessionSort = 'updated',
1182
+ dropped: AgentSessionListDropped = emptySessionListDropped(),
1144
1183
  ): Promise<AgentSessionRow[]> {
1145
1184
  const starredIds = await loadClaudeStarredIds(roots.claudeDesktopConfig)
1146
1185
  const cursorPinned = await loadCursorPinnedIds(roots.cursorWorkspaceStorage)
@@ -1159,9 +1198,9 @@ export async function listAgentSessions(
1159
1198
  const cap = AGENT_SESSION_PER_PROVIDER_LIMIT
1160
1199
  let rows = dedupeSessions([
1161
1200
  ...enrichedLive,
1162
- ...await listClaudeSessions(roots.claudeProjects, now, liveIds, cap, starredIds, roots.claudeCodeSessions),
1163
- ...await listCodexSessions(roots.codexSessions, now, cap),
1164
- ...await listCursorSessions(roots.cursorProjects, now, cap, roots.cursorComposerDb, cursorPinned),
1201
+ ...await listClaudeSessions(roots.claudeProjects, now, liveIds, cap, starredIds, roots.claudeCodeSessions, dropped),
1202
+ ...await listCodexSessions(roots.codexSessions, now, cap, dropped),
1203
+ ...await listCursorSessions(roots.cursorProjects, now, cap, roots.cursorComposerDb, cursorPinned, dropped),
1165
1204
  ])
1166
1205
  if (sort === 'opened') {
1167
1206
  rows = rows.filter(entry => {
@@ -1178,6 +1217,8 @@ export async function listAgentSessions(
1178
1217
  // pin-boosting here made the lens show July stars instead of today.
1179
1218
  rows.sort((a, b) => (b.modified || '').localeCompare(a.modified || ''))
1180
1219
  }
1220
+ const extras = Math.max(0, rows.length - Math.max(0, limit))
1221
+ if (extras) dropped.limit += extras
1181
1222
  return rows.slice(0, limit)
1182
1223
  }
1183
1224
 
@@ -11,8 +11,10 @@
11
11
  // `?sort=opened` keeps the same window on session start instead.
12
12
  // Search scans titles, sidebar names, first prompts, and transcript heads
13
13
  // without the 7-day list window. Literal /search is registered first.
14
- // Does not need COS_SCRIPTS_DIR. Codex subagents stay out. Files over 32 MB
15
- // still appear in the list; detail remains capped.
14
+ // Does not need COS_SCRIPTS_DIR. Codex subagents stay out.
15
+ // Codex files over 32 MB still appear in the list. Cursor files over 32 MB
16
+ // are skipped and counted on `dropped.oversized`. The list payload now says
17
+ // what each cap hid.
16
18
 
17
19
  import { Router } from 'express'
18
20
  import { stat } from 'node:fs/promises'
@@ -23,6 +25,7 @@ import {
23
25
  agentSessionRoots,
24
26
  findAgentSessionFile,
25
27
  listAgentSessions,
28
+ emptySessionListDropped,
26
29
  loadCursorComposerNames,
27
30
  parseAgentSession,
28
31
  type AgentProvider,
@@ -268,7 +271,8 @@ agentSessionsRouter.get('/agent-sessions', async (req, res) => {
268
271
  const limit = boundedInteger(req.query.limit, AGENT_SESSION_LIST_LIMIT, 1, AGENT_SESSION_LIST_MAX)
269
272
  const sort = asSort(req.query.sort)
270
273
  const live = await liveClaudeRows()
271
- const sessions = await listAgentSessions(agentSessionRoots(), new Date(), live, limit, sort)
274
+ const dropped = emptySessionListDropped()
275
+ const sessions = await listAgentSessions(agentSessionRoots(), new Date(), live, limit, sort, dropped)
272
276
  const scan = runningThreads(sessions)
273
277
  // Freshness is layered on AFTER occupancy, and only over what occupancy
274
278
  // found. `Date.now()` is read once so every row in a payload is judged
@@ -283,6 +287,10 @@ agentSessionsRouter.get('/agent-sessions', async (req, res) => {
283
287
  // True when a probe could not see clearly. The client must render "unknown"
284
288
  // rather than treating a quiet scan as "nothing is running".
285
289
  runningDegraded: running.degraded,
290
+ // LIST caps that previously dropped rows with no signal. Additive: an older
291
+ // client ignores this key. Zero means the walk found nothing to hide, not
292
+ // that the caps are off.
293
+ dropped,
286
294
  })
287
295
  } catch (error) {
288
296
  console.error(`[agent-sessions] list failed: ${error instanceof Error ? error.message : error}`)