@gotcos/glasses-server 6.44.9 → 6.44.10

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,16 @@
1
+ ## 6.44.10
2
+
3
+ Ingest progress for COS Control.
4
+
5
+ - `GET /api/context/graph/ingest/progress` (the bridge's `graph-ingest-progress`,
6
+ read-only): where the current or last Control-started run stands. The run's
7
+ total, how many are indexed and failed, the document in flight with its
8
+ estimated calls, the last eight outcomes with their seconds, the budget line,
9
+ the queue's pending count, and the log's last lines. A run started somewhere
10
+ else (a Claude session) holds the lock but writes no log here, so it reports
11
+ as `external` with the counts only. COS Control 0.5.195's Sync card polls it
12
+ every five seconds while the ingest lock is held.
13
+
1
14
  ## 6.44.9
2
15
 
3
16
  Knowledge from zero: the setup path behind COS Control's Knowledge tab.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotcos/glasses-server",
3
- "version": "6.44.9",
3
+ "version": "6.44.10",
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": {
@@ -923,6 +923,38 @@ export function normalizeGraphAnswer(value: unknown): { question: string; mode:
923
923
  return { question: stringOrAbsent(s.question, KNOWLEDGE_ASK_MAX_CHARS) ?? '', mode: stringOrAbsent(s.mode, 16) ?? 'hybrid', answer, elapsed_s: typeof s.elapsed_s === 'number' && Number.isFinite(s.elapsed_s) ? s.elapsed_s : null }
924
924
  }
925
925
 
926
+ export interface IngestProgressItem { id: string; outcome: 'indexed' | 'failed' | 'unknown'; seconds: number | null; reason: string | null }
927
+
928
+ /** `graph-ingest-progress`: where the current or last Control-started run stands. */
929
+ export function normalizeIngestProgress(value: unknown): Record<string, unknown> {
930
+ const s = asRecord(value) ?? {}
931
+ const current = asRecord(s.current)
932
+ const budget = asRecord(s.budget)
933
+ const items: IngestProgressItem[] = (Array.isArray(s.items) ? s.items : []).flatMap((row) => {
934
+ const r = asRecord(row); const id = r ? stringOrAbsent(r.id, 200) : undefined
935
+ if (!id) return []
936
+ const outcome: IngestProgressItem['outcome'] = r!.outcome === 'indexed' ? 'indexed' : r!.outcome === 'failed' ? 'failed' : 'unknown'
937
+ return [{ id, outcome, seconds: typeof r!.seconds === 'number' && Number.isFinite(r!.seconds) ? r!.seconds : null, reason: stringOrAbsent(r!.reason, 200) ?? null }]
938
+ }).slice(-8)
939
+ return {
940
+ running: s.running === true,
941
+ pid: integerOrAbsent(s.pid) ?? null,
942
+ external: s.external === true,
943
+ pending: integerOrAbsent(s.pending) ?? null,
944
+ total: integerOrAbsent(s.total) ?? null,
945
+ done: integerOrAbsent(s.done) ?? 0,
946
+ failed: integerOrAbsent(s.failed) ?? 0,
947
+ current: current && stringOrAbsent(current.id, 200) ? { id: stringOrAbsent(current.id, 200)!, est_calls: integerOrAbsent(current.est_calls) ?? null } : null,
948
+ items,
949
+ remaining_calls: integerOrAbsent(s.remaining_calls) ?? null,
950
+ budget: budget && integerOrAbsent(budget.used) !== undefined && integerOrAbsent(budget.cap) !== undefined ? { used: integerOrAbsent(budget.used)!, cap: integerOrAbsent(budget.cap)! } : null,
951
+ ended: s.ended === true,
952
+ log_tail: (Array.isArray(s.log_tail) ? s.log_tail : []).filter((l): l is string => typeof l === 'string').map(l => l.slice(0, 300)).slice(-24),
953
+ log_age_s: typeof s.log_age_s === 'number' && Number.isFinite(s.log_age_s) ? s.log_age_s : null,
954
+ lock: normalizeIngestKickoff({ lock: s.lock }).lock,
955
+ }
956
+ }
957
+
926
958
  export function normalizeContextBrowserStatus(value: unknown): ContextBrowserStatus {
927
959
  const source = value && typeof value === 'object' && !Array.isArray(value)
928
960
  ? value as Record<string, unknown> : {}
@@ -64,6 +64,7 @@ export const LEARNING_COMMANDS = [
64
64
  'graph-ingest-sample',
65
65
  'graph-ask',
66
66
  'graph-schedule',
67
+ 'graph-ingest-progress',
67
68
  ] as const
68
69
 
69
70
  // The optional Python bridge is available only when the user points us at a real
@@ -244,6 +245,7 @@ function standaloneNoop(args: string[]): unknown {
244
245
  case 'graph-ingest-sample':
245
246
  case 'graph-ask':
246
247
  case 'graph-schedule':
248
+ case 'graph-ingest-progress':
247
249
  return { error: 'cos_pipeline_not_configured' }
248
250
  case 'task-rows':
249
251
  case 'task-capture':
@@ -29,6 +29,7 @@ import { normalizeReviewDecision, LEARNING_REVIEW_LIMIT,
29
29
  normalizeKnowledgeSources,
30
30
  normalizeSampleKickoff,
31
31
  normalizeGraphAnswer,
32
+ normalizeIngestProgress,
32
33
  KNOWLEDGE_SETUP_SAMPLE_MAX,
33
34
  KNOWLEDGE_ASK_MAX_CHARS,
34
35
  normalizeLearningEventDetail,
@@ -345,6 +346,21 @@ memoryRouter.post('/context/graph/ingest', async (req, res) => {
345
346
  }
346
347
  })
347
348
 
349
+ /** Where the current or last Control-started ingest stands (6.44.10). Read-only. */
350
+ memoryRouter.get('/context/graph/ingest/progress', async (_req, res) => {
351
+ noStore(res)
352
+ if (!contextConfigured()) { res.status(503).json({ error: pythonBridgeState() }); return }
353
+ try {
354
+ const data = await callPython(['graph-ingest-progress'], 10_000)
355
+ const code = bridgeErrorCode(data)
356
+ if (code) { res.status(503).json({ error: code }); return }
357
+ res.json(normalizeIngestProgress(data))
358
+ } catch (error) {
359
+ console.warn('[context] progress bridge failure:', (error as Error).message)
360
+ res.status(503).json({ error: 'graph_unavailable' })
361
+ }
362
+ })
363
+
348
364
  // ── Knowledge setup (6.44.9): from zero to a first index, in COS Control ──
349
365
  //
350
366
  // Six bridge commands behind one guided path: the readiness checklist, the