@gotcos/glasses-server 6.44.5 → 6.44.6
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 +26 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/server/lib/cos-context-browser.ts +6 -4
- package/server/lib/python-bridge.ts +6 -1
- package/server/routes/memory.ts +54 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
## 6.44.6
|
|
2
|
+
|
|
3
|
+
What /qa found in 6.44.5 before anyone installed it.
|
|
4
|
+
|
|
5
|
+
- `GET /api/context/learning/review`: the strict To review set (the bridge's
|
|
6
|
+
`learning-to-review`, the same set the status block counts) as event rows,
|
|
7
|
+
one page of up to 200. 6.44.5's list approximated it with a kind filter and
|
|
8
|
+
showed "50 of 837" beside a chip that said 121.
|
|
9
|
+
- Every user value now rides as one argv token (`--q=...`, `--id=...`,
|
|
10
|
+
`--entity=...`), so a search term or entity name that begins with `-` can no
|
|
11
|
+
longer be read by the bridge's argument parser as a flag.
|
|
12
|
+
- An older COS bridge prints its unknown-command answer to stdout; the server
|
|
13
|
+
probed only stderr, so every learning route on such a bridge answered 503
|
|
14
|
+
instead of `cos_pipeline_not_configured`. Both are probed now.
|
|
15
|
+
- A `found: false` entity answer is a 404 with a class Control can name,
|
|
16
|
+
`index_missing` or `entity_not_found`, never a 503; the null-normalizer
|
|
17
|
+
fallback says `record_not_found`.
|
|
18
|
+
- `/api/context/status` answers `Cache-Control: private, no-store` like the
|
|
19
|
+
eight routes that shipped with it; it carries private counts.
|
|
20
|
+
- Every learning and graph route logs the bridge failure it used to swallow,
|
|
21
|
+
and the search shape passes the bridge's own `scope` through.
|
|
22
|
+
- On the COS side (same night): a JSON error exits 0 so it is parsed rather
|
|
23
|
+
than rejected, the bot-memory reach is bounded at 2.5 s so a wedged Qdrant
|
|
24
|
+
cannot blank the file stores, and entity descriptions cap at 12.
|
|
25
|
+
|
|
1
26
|
## 6.44.5
|
|
2
27
|
|
|
3
28
|
Recent learning and the knowledge graph, read-only, for COS Control.
|
|
@@ -8,7 +33,7 @@ Recent learning and the knowledge graph, read-only, for COS Control.
|
|
|
8
33
|
the command, times out, or answers `{ error }` leaves `memory` and `threads`
|
|
9
34
|
exactly as they were and the two blocks absent. Nothing an existing client
|
|
10
35
|
reads has changed; a client that wants the blocks checks for them.
|
|
11
|
-
-
|
|
36
|
+
- Seven read routes: `GET /api/context/learning` (cursor-paged events with a
|
|
12
37
|
per-store coverage map), `GET /api/context/learning/status`,
|
|
13
38
|
`GET /api/context/learning/:id`, `GET /api/context/graph/status`,
|
|
14
39
|
`GET /api/context/graph/search`, `GET /api/context/graph/entity`,
|
package/README.md
CHANGED
|
@@ -425,7 +425,7 @@ coverage map), `/api/context/learning/status`, `/api/context/learning/:id`, and
|
|
|
425
425
|
`/api/context/graph/{status,search,entity,passages}`, plus `POST
|
|
426
426
|
/api/context/graph/index`, which only asks the pipeline to start a detached index
|
|
427
427
|
build and answers 202. Nothing in the file tier can answer these, so they return
|
|
428
|
-
503
|
|
428
|
+
503 `cos_pipeline_not_configured` there; older servers 404 them, which is how a client
|
|
429
429
|
tells the versions apart. `/api/context/status` carries `learning` and `graph`
|
|
430
430
|
blocks when the bridge can produce them and omits them otherwise.
|
|
431
431
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.44.
|
|
3
|
+
"version": "6.44.6",
|
|
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": {
|
|
@@ -472,6 +472,8 @@ export function normalizeGraphBlock(value: unknown): GraphBlock | null {
|
|
|
472
472
|
export const LEARNING_EVENT_ID_PATTERN = /^evt_[a-f0-9]{16}$/
|
|
473
473
|
export const LEARNING_EVENT_TYPES = new Set(['captured', 'proposed', 'promotable', 'saved', 'retrieved', 'used', 'checked', 'dismissed', 'reverted', 'reopened', 'consolidated', 'previewed'])
|
|
474
474
|
const LEARNING_LIST_LIMIT = 50
|
|
475
|
+
/** The strict To review set is small (121 today); one page shows it whole. */
|
|
476
|
+
export const LEARNING_REVIEW_LIMIT = 200
|
|
475
477
|
const LEARNING_DETAIL_KEYS = ['shape', 'task', 'kind', 'layer', 'date', 'future', 'logged_times', 'memory_type', 'capture', 'source', 'content', 'before', 'after', 'rule', 'status', 'occurrences', 'threshold', 'entry', 'truncated'] as const
|
|
476
478
|
|
|
477
479
|
export interface LearningEvent {
|
|
@@ -553,11 +555,11 @@ export function normalizeLearningCoverage(value: unknown): LearningCoverage {
|
|
|
553
555
|
return out
|
|
554
556
|
}
|
|
555
557
|
|
|
556
|
-
export function normalizeLearningEvents(value: unknown, limit: number): {
|
|
558
|
+
export function normalizeLearningEvents(value: unknown, limit: number, max = LEARNING_LIST_LIMIT): {
|
|
557
559
|
events: LearningEvent[]; total: number; next_cursor: { since_ts: string; since_event_id: string } | null; coverage: LearningCoverage
|
|
558
560
|
} {
|
|
559
561
|
const source = asRecord(value) ?? {}
|
|
560
|
-
const cap = Math.max(1, Math.min(limit,
|
|
562
|
+
const cap = Math.max(1, Math.min(limit, max))
|
|
561
563
|
const events = (Array.isArray(source.events) ? source.events : []).slice(0, cap)
|
|
562
564
|
.map(item => normalizeLearningEvent(item, 240)).filter((e): e is LearningEvent => !!e)
|
|
563
565
|
const cursor = asRecord(source.next_cursor)
|
|
@@ -568,6 +570,7 @@ export function normalizeLearningEvents(value: unknown, limit: number): {
|
|
|
568
570
|
total: integerOrAbsent(source.total) ?? events.length,
|
|
569
571
|
next_cursor: sinceTs && sinceId ? { since_ts: sinceTs, since_event_id: sinceId } : null,
|
|
570
572
|
coverage: normalizeLearningCoverage(source.coverage),
|
|
573
|
+
...(integerOrAbsent(source.review_count) !== undefined ? { review_count: integerOrAbsent(source.review_count) } : {}),
|
|
571
574
|
}
|
|
572
575
|
}
|
|
573
576
|
|
|
@@ -579,7 +582,6 @@ export function normalizeLearningEventDetail(value: unknown): (LearningEvent & {
|
|
|
579
582
|
for (const key of LEARNING_DETAIL_KEYS) {
|
|
580
583
|
const item = raw[key]
|
|
581
584
|
if (item === undefined || item === null) continue
|
|
582
|
-
if (key === 'bodies' as string) continue
|
|
583
585
|
if (typeof item === 'boolean') detail[key] = item
|
|
584
586
|
else if (Number.isInteger(item)) detail[key] = item
|
|
585
587
|
else if (typeof item === 'string') detail[key] = cleanContextText(item, 1200)
|
|
@@ -708,7 +710,7 @@ export function normalizeGraphSearch(value: unknown, limit: number): Record<stri
|
|
|
708
710
|
return {
|
|
709
711
|
items,
|
|
710
712
|
total: integerOrAbsent(source.total) ?? items.length,
|
|
711
|
-
scope: 'full-index',
|
|
713
|
+
scope: stringOrAbsent(source.scope, 32) ?? 'full-index',
|
|
712
714
|
index_built_at: isoOrAbsent(source.index_built_at) ?? null,
|
|
713
715
|
index_state: stringOrAbsent(source.index_state, 32) ?? 'missing',
|
|
714
716
|
matcher: stringOrAbsent(source.matcher, 16) ?? null,
|
|
@@ -55,6 +55,7 @@ export const LEARNING_COMMANDS = [
|
|
|
55
55
|
'graph-entity',
|
|
56
56
|
'graph-passages',
|
|
57
57
|
'graph-index-build',
|
|
58
|
+
'learning-to-review',
|
|
58
59
|
] as const
|
|
59
60
|
|
|
60
61
|
// The optional Python bridge is available only when the user points us at a real
|
|
@@ -226,6 +227,7 @@ function standaloneNoop(args: string[]): unknown {
|
|
|
226
227
|
case 'graph-entity':
|
|
227
228
|
case 'graph-passages':
|
|
228
229
|
case 'graph-index-build':
|
|
230
|
+
case 'learning-to-review':
|
|
229
231
|
return { error: 'cos_pipeline_not_configured' }
|
|
230
232
|
case 'task-rows':
|
|
231
233
|
case 'task-capture':
|
|
@@ -255,7 +257,10 @@ function callPythonDirect(args: string[], timeoutMs: number, input?: string): Pr
|
|
|
255
257
|
{ cwd: COS_SCRIPTS_DIR!, timeout: timeoutMs, maxBuffer: 1024 * 1024 },
|
|
256
258
|
(err, stdout, stderr) => {
|
|
257
259
|
if (err) {
|
|
258
|
-
|
|
260
|
+
// The bridge prints its unknown-command answer to STDOUT and exits 1
|
|
261
|
+
// (cos_api_bridge.py), so an older checkout must be probed there too, or
|
|
262
|
+
// every learning route answers 503 instead of not-configured (QA 2026-09-06).
|
|
263
|
+
const msg = stderr?.trim() || stdout?.trim() || err.message
|
|
259
264
|
if (typeof msg === 'string' && msg.includes('unknown command')) {
|
|
260
265
|
return resolvePromise({ error: { code: 'cos_pipeline_not_configured', message: msg } })
|
|
261
266
|
}
|
package/server/routes/memory.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { searchMemories } from '../lib/context-library-search.js'
|
|
|
13
13
|
function contextConfigured(): boolean {
|
|
14
14
|
return contextSourceAvailable() !== null
|
|
15
15
|
}
|
|
16
|
-
import {
|
|
16
|
+
import { LEARNING_REVIEW_LIMIT,
|
|
17
17
|
GRAPH_ENTITY_ID_LIMIT,
|
|
18
18
|
LEARNING_EVENT_ID_PATTERN,
|
|
19
19
|
MEMORY_ID_PATTERN,
|
|
@@ -35,6 +35,7 @@ export const memoryRouter = Router()
|
|
|
35
35
|
let overviewCache: { expiresAt: number; value: ReturnType<typeof normalizeMemoryOverview> } | null = null
|
|
36
36
|
|
|
37
37
|
memoryRouter.get('/context/status', async (_req, res) => {
|
|
38
|
+
noStore(res)
|
|
38
39
|
if (!contextConfigured()) {
|
|
39
40
|
const state = pythonBridgeState()
|
|
40
41
|
res.json(normalizeContextBrowserStatus({
|
|
@@ -105,7 +106,7 @@ function sendBridgeAnswer(res: import('express').Response, data: unknown, normal
|
|
|
105
106
|
}
|
|
106
107
|
const value = normalize(data)
|
|
107
108
|
if (value === null || value === undefined) {
|
|
108
|
-
res.status(404).json({ error: '
|
|
109
|
+
res.status(404).json({ error: 'record_not_found' })
|
|
109
110
|
return
|
|
110
111
|
}
|
|
111
112
|
res.json(value)
|
|
@@ -124,7 +125,8 @@ memoryRouter.get('/context/learning/status', async (_req, res) => {
|
|
|
124
125
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
125
126
|
try {
|
|
126
127
|
sendBridgeAnswer(res, await callPython(['learning-status', '--no-memory'], 8_000), normalizeLearningStatus)
|
|
127
|
-
} catch {
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.warn('[context] learning bridge failure:', (error as Error).message)
|
|
128
130
|
res.status(503).json({ error: 'learning_unavailable' })
|
|
129
131
|
}
|
|
130
132
|
})
|
|
@@ -137,13 +139,30 @@ memoryRouter.get('/context/learning', async (req, res) => {
|
|
|
137
139
|
const sinceTs = typeof req.query.since_ts === 'string' && Number.isFinite(Date.parse(req.query.since_ts)) ? req.query.since_ts.slice(0, 40) : ''
|
|
138
140
|
const sinceId = typeof req.query.since_event_id === 'string' && LEARNING_EVENT_ID_PATTERN.test(req.query.since_event_id) ? req.query.since_event_id : ''
|
|
139
141
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
140
|
-
const args = ['learning-events',
|
|
141
|
-
if (kind) args.push(
|
|
142
|
-
if (sinceTs) args.push(
|
|
143
|
-
if (sinceId) args.push(
|
|
142
|
+
const args = ['learning-events', `--days=${days}`, `--limit=${limit}`]
|
|
143
|
+
if (kind) args.push(`--kind=${kind}`)
|
|
144
|
+
if (sinceTs) args.push(`--since-ts=${sinceTs}`)
|
|
145
|
+
if (sinceId) args.push(`--since-event-id=${sinceId}`)
|
|
144
146
|
try {
|
|
145
147
|
sendBridgeAnswer(res, await callPython(args, 8_000), value => normalizeLearningEvents(value, limit))
|
|
146
|
-
} catch {
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.warn('[context] learning bridge failure:', (error as Error).message)
|
|
150
|
+
res.status(503).json({ error: 'learning_unavailable' })
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
memoryRouter.get('/context/learning/review', async (req, res) => {
|
|
155
|
+
noStore(res)
|
|
156
|
+
// The strict To review set (learning_events.to_review) as event rows, so the
|
|
157
|
+
// chip, Doctor and the list count the same thing. Small set: one page.
|
|
158
|
+
const limit = boundedInteger(req.query.limit, LEARNING_REVIEW_LIMIT, 1, LEARNING_REVIEW_LIMIT)
|
|
159
|
+
const days = req.query.days === undefined ? null : boundedInteger(req.query.days, 3650, 1, 3650)
|
|
160
|
+
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
161
|
+
try {
|
|
162
|
+
const args = ['learning-to-review', `--limit=${limit}`, ...(days ? [`--days=${days}`] : [])]
|
|
163
|
+
sendBridgeAnswer(res, await callPython(args, 8_000), value => normalizeLearningEvents(value, limit, LEARNING_REVIEW_LIMIT))
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.warn('[context] learning review bridge failure:', (error as Error).message)
|
|
147
166
|
res.status(503).json({ error: 'learning_unavailable' })
|
|
148
167
|
}
|
|
149
168
|
})
|
|
@@ -153,8 +172,9 @@ memoryRouter.get('/context/learning/:id', async (req, res) => {
|
|
|
153
172
|
if (!LEARNING_EVENT_ID_PATTERN.test(req.params.id)) { res.status(400).json({ error: 'invalid_event_id' }); return }
|
|
154
173
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
155
174
|
try {
|
|
156
|
-
sendBridgeAnswer(res, await callPython(['learning-event',
|
|
157
|
-
} catch {
|
|
175
|
+
sendBridgeAnswer(res, await callPython(['learning-event', `--id=${req.params.id}`], 8_000), normalizeLearningEventDetail)
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.warn('[context] learning bridge failure:', (error as Error).message)
|
|
158
178
|
res.status(503).json({ error: 'learning_unavailable' })
|
|
159
179
|
}
|
|
160
180
|
})
|
|
@@ -166,7 +186,8 @@ memoryRouter.get('/context/graph/status', async (_req, res) => {
|
|
|
166
186
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
167
187
|
try {
|
|
168
188
|
sendBridgeAnswer(res, await callPython(['graph-status'], 8_000), normalizeGraphStatus)
|
|
169
|
-
} catch {
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.warn('[context] graph bridge failure:', (error as Error).message)
|
|
170
191
|
res.status(503).json({ error: 'graph_unavailable' })
|
|
171
192
|
}
|
|
172
193
|
})
|
|
@@ -182,11 +203,14 @@ memoryRouter.get('/context/graph/search', async (req, res) => {
|
|
|
182
203
|
const offset = boundedInteger(req.query.offset, 0, 0, 100_000)
|
|
183
204
|
const type = typeof req.query.type === 'string' ? req.query.type.replace(/[^A-Za-z0-9_ -]/g, '').slice(0, 40) : ''
|
|
184
205
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
185
|
-
|
|
186
|
-
|
|
206
|
+
// One token per value (`--q=...`): a value beginning with `-` is then never
|
|
207
|
+
// read by argparse as a flag (QA 2026-09-06).
|
|
208
|
+
const args = ['graph-search', `--q=${query}`, `--limit=${limit}`, `--offset=${offset}`]
|
|
209
|
+
if (type) args.push(`--type=${type}`)
|
|
187
210
|
try {
|
|
188
211
|
sendBridgeAnswer(res, await callPython(args, 8_000), value => normalizeGraphSearch(value, limit))
|
|
189
|
-
} catch {
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.warn('[context] graph bridge failure:', (error as Error).message)
|
|
190
214
|
res.status(503).json({ error: 'graph_unavailable' })
|
|
191
215
|
}
|
|
192
216
|
})
|
|
@@ -202,9 +226,16 @@ memoryRouter.get('/context/graph/entity', async (req, res) => {
|
|
|
202
226
|
const offset = boundedInteger(req.query.offset, 0, 0, 100_000)
|
|
203
227
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
204
228
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
229
|
+
const answer = await callPython(['graph-entity', `--id=${id}`, `--offset=${offset}`, `--limit=${limit}`], 8_000)
|
|
230
|
+
// A no-index or unknown-entity answer is a PAYLOAD from the bridge (found: false);
|
|
231
|
+
// it becomes a 404 with the class Control renders, never a 503 (QA 2026-09-06).
|
|
232
|
+
if (bridgePayload(answer) && answer.found !== true) {
|
|
233
|
+
res.status(404).json({ error: answer.index_state === 'missing' ? 'index_missing' : 'entity_not_found' })
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
sendBridgeAnswer(res, answer, value => normalizeGraphEntity(value))
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.warn('[context] graph bridge failure:', (error as Error).message)
|
|
208
239
|
res.status(503).json({ error: 'graph_unavailable' })
|
|
209
240
|
}
|
|
210
241
|
})
|
|
@@ -222,11 +253,12 @@ memoryRouter.get('/context/graph/passages', async (req, res) => {
|
|
|
222
253
|
const limit = boundedInteger(req.query.limit, 5, 1, 5)
|
|
223
254
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
224
255
|
const args = entity
|
|
225
|
-
? ['graph-passages',
|
|
226
|
-
: ['graph-passages',
|
|
256
|
+
? ['graph-passages', `--entity=${entity}`, `--limit=${limit}`]
|
|
257
|
+
: ['graph-passages', `--relation-a=${relationA}`, `--relation-b=${relationB}`, `--limit=${limit}`]
|
|
227
258
|
try {
|
|
228
259
|
sendBridgeAnswer(res, await callPython(args, 8_000), normalizeGraphPassages)
|
|
229
|
-
} catch {
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.warn('[context] graph bridge failure:', (error as Error).message)
|
|
230
262
|
res.status(503).json({ error: 'graph_unavailable' })
|
|
231
263
|
}
|
|
232
264
|
})
|
|
@@ -245,7 +277,8 @@ memoryRouter.post('/context/graph/index', async (_req, res) => {
|
|
|
245
277
|
const code = bridgeErrorCode(data)
|
|
246
278
|
if (code) { res.status(503).json({ error: code }); return }
|
|
247
279
|
res.status(202).json(normalizeIndexBuildKickoff(data))
|
|
248
|
-
} catch {
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.warn('[context] graph bridge failure:', (error as Error).message)
|
|
249
282
|
res.status(503).json({ error: 'graph_unavailable' })
|
|
250
283
|
}
|
|
251
284
|
})
|