@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 +12 -0
- package/package.json +1 -1
- package/server/lib/agent-session-store.ts +54 -13
- package/server/routes/agent-sessions.ts +11 -3
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
|
@@ -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)
|
|
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 (
|
|
892
|
-
if (rows.length >= Math.max(0, limit))
|
|
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)
|
|
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 (
|
|
961
|
-
if (rows.length >= Math.max(0, limit))
|
|
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)
|
|
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)
|
|
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 (
|
|
1049
|
-
if (rows.length >= Math.max(0, limit))
|
|
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.
|
|
15
|
-
// still appear in the list
|
|
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
|
|
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}`)
|