@gotcos/glasses-server 6.44.5 → 6.44.7
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 +36 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/server/lib/cos-context-browser.ts +23 -4
- package/server/lib/python-bridge.ts +8 -1
- package/server/routes/memory.ts +74 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
## 6.44.7
|
|
2
|
+
|
|
3
|
+
One learning write: a review decision.
|
|
4
|
+
|
|
5
|
+
- `POST /api/context/learning/:id/review` with `{ "decision": "dismissed" | "reopened" }`
|
|
6
|
+
appends a review-ledger row through the bridge's `learning-decide`. A
|
|
7
|
+
dismissed proposal leaves the To review set on the next read; reopened puts
|
|
8
|
+
it back. Nothing else is written: no skill, no memory, no graph. COS Control
|
|
9
|
+
0.5.191's Memories tab uses it for Dismiss and Restore proposal.
|
|
10
|
+
|
|
11
|
+
## 6.44.6
|
|
12
|
+
|
|
13
|
+
What /qa found in 6.44.5 before anyone installed it.
|
|
14
|
+
|
|
15
|
+
- `GET /api/context/learning/review`: the strict To review set (the bridge's
|
|
16
|
+
`learning-to-review`, the same set the status block counts) as event rows,
|
|
17
|
+
one page of up to 200. 6.44.5's list approximated it with a kind filter and
|
|
18
|
+
showed "50 of 837" beside a chip that said 121.
|
|
19
|
+
- Every user value now rides as one argv token (`--q=...`, `--id=...`,
|
|
20
|
+
`--entity=...`), so a search term or entity name that begins with `-` can no
|
|
21
|
+
longer be read by the bridge's argument parser as a flag.
|
|
22
|
+
- An older COS bridge prints its unknown-command answer to stdout; the server
|
|
23
|
+
probed only stderr, so every learning route on such a bridge answered 503
|
|
24
|
+
instead of `cos_pipeline_not_configured`. Both are probed now.
|
|
25
|
+
- A `found: false` entity answer is a 404 with a class Control can name,
|
|
26
|
+
`index_missing` or `entity_not_found`, never a 503; the null-normalizer
|
|
27
|
+
fallback says `record_not_found`.
|
|
28
|
+
- `/api/context/status` answers `Cache-Control: private, no-store` like the
|
|
29
|
+
eight routes that shipped with it; it carries private counts.
|
|
30
|
+
- Every learning and graph route logs the bridge failure it used to swallow,
|
|
31
|
+
and the search shape passes the bridge's own `scope` through.
|
|
32
|
+
- On the COS side (same night): a JSON error exits 0 so it is parsed rather
|
|
33
|
+
than rejected, the bot-memory reach is bounded at 2.5 s so a wedged Qdrant
|
|
34
|
+
cannot blank the file stores, and entity descriptions cap at 12.
|
|
35
|
+
|
|
1
36
|
## 6.44.5
|
|
2
37
|
|
|
3
38
|
Recent learning and the knowledge graph, read-only, for COS Control.
|
|
@@ -8,7 +43,7 @@ Recent learning and the knowledge graph, read-only, for COS Control.
|
|
|
8
43
|
the command, times out, or answers `{ error }` leaves `memory` and `threads`
|
|
9
44
|
exactly as they were and the two blocks absent. Nothing an existing client
|
|
10
45
|
reads has changed; a client that wants the blocks checks for them.
|
|
11
|
-
-
|
|
46
|
+
- Seven read routes: `GET /api/context/learning` (cursor-paged events with a
|
|
12
47
|
per-store coverage map), `GET /api/context/learning/status`,
|
|
13
48
|
`GET /api/context/learning/:id`, `GET /api/context/graph/status`,
|
|
14
49
|
`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.7",
|
|
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,
|
|
@@ -774,6 +776,23 @@ export function normalizeGraphPassages(value: unknown): Record<string, unknown>
|
|
|
774
776
|
}
|
|
775
777
|
}
|
|
776
778
|
|
|
779
|
+
/** The review decision the bridge wrote back: lesson, decision, stamp, id, who. */
|
|
780
|
+
export function normalizeReviewDecision(value: unknown): Record<string, unknown> | null {
|
|
781
|
+
const source = asRecord(value)
|
|
782
|
+
const row = asRecord(source?.decision)
|
|
783
|
+
if (!row) return null
|
|
784
|
+
const decision = stringOrAbsent(row.decision, 16)
|
|
785
|
+
const lessonId = stringOrAbsent(row.lesson_id, 200)
|
|
786
|
+
if (!lessonId || (decision !== 'dismissed' && decision !== 'reopened')) return null
|
|
787
|
+
return {
|
|
788
|
+
lesson_id: lessonId,
|
|
789
|
+
decision,
|
|
790
|
+
ts: isoOrAbsent(row.ts) ?? null,
|
|
791
|
+
event_id: LEARNING_EVENT_ID_PATTERN.test(String(row.event_id ?? '')) ? String(row.event_id) : null,
|
|
792
|
+
by: stringOrAbsent(row.by, 32) ?? null,
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
777
796
|
export function normalizeIndexBuildKickoff(value: unknown): { started: boolean; already_running: boolean; pid: number | null; receipt: Record<string, unknown> | null } {
|
|
778
797
|
const source = asRecord(value) ?? {}
|
|
779
798
|
return {
|
|
@@ -55,6 +55,8 @@ export const LEARNING_COMMANDS = [
|
|
|
55
55
|
'graph-entity',
|
|
56
56
|
'graph-passages',
|
|
57
57
|
'graph-index-build',
|
|
58
|
+
'learning-to-review',
|
|
59
|
+
'learning-decide',
|
|
58
60
|
] as const
|
|
59
61
|
|
|
60
62
|
// The optional Python bridge is available only when the user points us at a real
|
|
@@ -226,6 +228,8 @@ function standaloneNoop(args: string[]): unknown {
|
|
|
226
228
|
case 'graph-entity':
|
|
227
229
|
case 'graph-passages':
|
|
228
230
|
case 'graph-index-build':
|
|
231
|
+
case 'learning-to-review':
|
|
232
|
+
case 'learning-decide':
|
|
229
233
|
return { error: 'cos_pipeline_not_configured' }
|
|
230
234
|
case 'task-rows':
|
|
231
235
|
case 'task-capture':
|
|
@@ -255,7 +259,10 @@ function callPythonDirect(args: string[], timeoutMs: number, input?: string): Pr
|
|
|
255
259
|
{ cwd: COS_SCRIPTS_DIR!, timeout: timeoutMs, maxBuffer: 1024 * 1024 },
|
|
256
260
|
(err, stdout, stderr) => {
|
|
257
261
|
if (err) {
|
|
258
|
-
|
|
262
|
+
// The bridge prints its unknown-command answer to STDOUT and exits 1
|
|
263
|
+
// (cos_api_bridge.py), so an older checkout must be probed there too, or
|
|
264
|
+
// every learning route answers 503 instead of not-configured (QA 2026-09-06).
|
|
265
|
+
const msg = stderr?.trim() || stdout?.trim() || err.message
|
|
259
266
|
if (typeof msg === 'string' && msg.includes('unknown command')) {
|
|
260
267
|
return resolvePromise({ error: { code: 'cos_pipeline_not_configured', message: msg } })
|
|
261
268
|
}
|
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 { normalizeReviewDecision, 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,50 @@ 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)
|
|
166
|
+
res.status(503).json({ error: 'learning_unavailable' })
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
memoryRouter.post('/context/learning/:id/review', async (req, res) => {
|
|
171
|
+
noStore(res)
|
|
172
|
+
// The one learning write (6.44.7): a review decision on a lesson, appended to
|
|
173
|
+
// the review ledger by the bridge. Dismissed leaves To review, reopened
|
|
174
|
+
// returns; nothing else is touched. The lesson id is a store id, not an event id.
|
|
175
|
+
const lessonId = String(req.params.id)
|
|
176
|
+
const decision = typeof req.body?.decision === 'string' ? req.body.decision : ''
|
|
177
|
+
if (!lessonId || lessonId.length > 200 || CONTROL_CHARACTER.test(lessonId)) { res.status(400).json({ error: 'invalid_lesson_id' }); return }
|
|
178
|
+
if (decision !== 'dismissed' && decision !== 'reopened') { res.status(400).json({ error: 'invalid_decision' }); return }
|
|
179
|
+
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
180
|
+
try {
|
|
181
|
+
const note = typeof req.body?.note === 'string' ? req.body.note.slice(0, 400) : ''
|
|
182
|
+
const answer = await callPython(['learning-decide', `--id=${lessonId}`, `--decision=${decision}`, ...(note ? [`--note=${note}`] : [])], 8_000)
|
|
183
|
+
sendBridgeAnswer(res, answer, value => normalizeReviewDecision(value))
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.warn('[context] learning decide bridge failure:', (error as Error).message)
|
|
147
186
|
res.status(503).json({ error: 'learning_unavailable' })
|
|
148
187
|
}
|
|
149
188
|
})
|
|
@@ -153,8 +192,9 @@ memoryRouter.get('/context/learning/:id', async (req, res) => {
|
|
|
153
192
|
if (!LEARNING_EVENT_ID_PATTERN.test(req.params.id)) { res.status(400).json({ error: 'invalid_event_id' }); return }
|
|
154
193
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
155
194
|
try {
|
|
156
|
-
sendBridgeAnswer(res, await callPython(['learning-event',
|
|
157
|
-
} catch {
|
|
195
|
+
sendBridgeAnswer(res, await callPython(['learning-event', `--id=${req.params.id}`], 8_000), normalizeLearningEventDetail)
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.warn('[context] learning bridge failure:', (error as Error).message)
|
|
158
198
|
res.status(503).json({ error: 'learning_unavailable' })
|
|
159
199
|
}
|
|
160
200
|
})
|
|
@@ -166,7 +206,8 @@ memoryRouter.get('/context/graph/status', async (_req, res) => {
|
|
|
166
206
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
167
207
|
try {
|
|
168
208
|
sendBridgeAnswer(res, await callPython(['graph-status'], 8_000), normalizeGraphStatus)
|
|
169
|
-
} catch {
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.warn('[context] graph bridge failure:', (error as Error).message)
|
|
170
211
|
res.status(503).json({ error: 'graph_unavailable' })
|
|
171
212
|
}
|
|
172
213
|
})
|
|
@@ -182,11 +223,14 @@ memoryRouter.get('/context/graph/search', async (req, res) => {
|
|
|
182
223
|
const offset = boundedInteger(req.query.offset, 0, 0, 100_000)
|
|
183
224
|
const type = typeof req.query.type === 'string' ? req.query.type.replace(/[^A-Za-z0-9_ -]/g, '').slice(0, 40) : ''
|
|
184
225
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
185
|
-
|
|
186
|
-
|
|
226
|
+
// One token per value (`--q=...`): a value beginning with `-` is then never
|
|
227
|
+
// read by argparse as a flag (QA 2026-09-06).
|
|
228
|
+
const args = ['graph-search', `--q=${query}`, `--limit=${limit}`, `--offset=${offset}`]
|
|
229
|
+
if (type) args.push(`--type=${type}`)
|
|
187
230
|
try {
|
|
188
231
|
sendBridgeAnswer(res, await callPython(args, 8_000), value => normalizeGraphSearch(value, limit))
|
|
189
|
-
} catch {
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.warn('[context] graph bridge failure:', (error as Error).message)
|
|
190
234
|
res.status(503).json({ error: 'graph_unavailable' })
|
|
191
235
|
}
|
|
192
236
|
})
|
|
@@ -202,9 +246,16 @@ memoryRouter.get('/context/graph/entity', async (req, res) => {
|
|
|
202
246
|
const offset = boundedInteger(req.query.offset, 0, 0, 100_000)
|
|
203
247
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
204
248
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
249
|
+
const answer = await callPython(['graph-entity', `--id=${id}`, `--offset=${offset}`, `--limit=${limit}`], 8_000)
|
|
250
|
+
// A no-index or unknown-entity answer is a PAYLOAD from the bridge (found: false);
|
|
251
|
+
// it becomes a 404 with the class Control renders, never a 503 (QA 2026-09-06).
|
|
252
|
+
if (bridgePayload(answer) && answer.found !== true) {
|
|
253
|
+
res.status(404).json({ error: answer.index_state === 'missing' ? 'index_missing' : 'entity_not_found' })
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
sendBridgeAnswer(res, answer, value => normalizeGraphEntity(value))
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.warn('[context] graph bridge failure:', (error as Error).message)
|
|
208
259
|
res.status(503).json({ error: 'graph_unavailable' })
|
|
209
260
|
}
|
|
210
261
|
})
|
|
@@ -222,11 +273,12 @@ memoryRouter.get('/context/graph/passages', async (req, res) => {
|
|
|
222
273
|
const limit = boundedInteger(req.query.limit, 5, 1, 5)
|
|
223
274
|
if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
|
|
224
275
|
const args = entity
|
|
225
|
-
? ['graph-passages',
|
|
226
|
-
: ['graph-passages',
|
|
276
|
+
? ['graph-passages', `--entity=${entity}`, `--limit=${limit}`]
|
|
277
|
+
: ['graph-passages', `--relation-a=${relationA}`, `--relation-b=${relationB}`, `--limit=${limit}`]
|
|
227
278
|
try {
|
|
228
279
|
sendBridgeAnswer(res, await callPython(args, 8_000), normalizeGraphPassages)
|
|
229
|
-
} catch {
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.warn('[context] graph bridge failure:', (error as Error).message)
|
|
230
282
|
res.status(503).json({ error: 'graph_unavailable' })
|
|
231
283
|
}
|
|
232
284
|
})
|
|
@@ -245,7 +297,8 @@ memoryRouter.post('/context/graph/index', async (_req, res) => {
|
|
|
245
297
|
const code = bridgeErrorCode(data)
|
|
246
298
|
if (code) { res.status(503).json({ error: code }); return }
|
|
247
299
|
res.status(202).json(normalizeIndexBuildKickoff(data))
|
|
248
|
-
} catch {
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.warn('[context] graph bridge failure:', (error as Error).message)
|
|
249
302
|
res.status(503).json({ error: 'graph_unavailable' })
|
|
250
303
|
}
|
|
251
304
|
})
|