From 90b8e7edf8e460902a1cac6a81148506d7afcdc4 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Fri, 15 May 2026 18:13:35 +0200 Subject: [PATCH] feat(headless): expose memory extraction diagnostics --- src/headless-query.ts | 42 +++++++++++++++++++ .../headless-query-memory-extraction.test.ts | 32 ++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/tests/headless-query-memory-extraction.test.ts diff --git a/src/headless-query.ts b/src/headless-query.ts index 30376a6d4..a5ca374bc 100644 --- a/src/headless-query.ts +++ b/src/headless-query.ts @@ -183,6 +183,17 @@ export interface QuerySnapshot { // section itself so this is an estimate, not a meter. estimated_total_tokens: number; }; + memoryExtraction?: { + total_attempts: number; + recent_attempts: Array<{ + unit_key: string; + status: string; + failure_class: string | null; + reason: string | null; + error: string | null; + created_at: string; + }>; + }; schedule?: { pending_count: number; overdue_count: number; @@ -413,6 +424,36 @@ export async function buildQuerySnapshot( // runtime_counters unavailable on legacy DBs — fine, drop the section. } + let memoryExtraction: QuerySnapshot["memoryExtraction"]; + try { + const memoryDbModule = (await jiti.import( + sfExtensionPath("sf-db/sf-db-memory"), + {}, + )) as { listMemoryExtractionAttempts: (limit?: number) => any[] }; + const sfDbModule = (await jiti.import(sfExtensionPath("sf-db"), {})) as { + _getAdapter: () => any; + }; + const recent = memoryDbModule.listMemoryExtractionAttempts(5); + const adapter = sfDbModule._getAdapter(); + const total = + adapter + ?.prepare("SELECT count(*) AS cnt FROM memory_extraction_attempts") + .get()?.cnt ?? recent.length; + memoryExtraction = { + total_attempts: Number(total), + recent_attempts: recent.map((row) => ({ + unit_key: String(row.unit_key ?? ""), + status: String(row.status ?? ""), + failure_class: row.failure_class ? String(row.failure_class) : null, + reason: row.reason ? String(row.reason) : null, + error: row.error ? String(row.error) : null, + created_at: String(row.created_at ?? ""), + })), + }; + } catch { + // Older DBs may not have the extraction attempt table yet. + } + const snapshot: QuerySnapshot = { schemaVersion: 1, state, @@ -428,6 +469,7 @@ export async function buildQuerySnapshot( }, uokDiagnostics, ...(memoryInjection ? { memoryInjection } : {}), + ...(memoryExtraction ? { memoryExtraction } : {}), schedule: scheduleEntries, }; diff --git a/src/tests/headless-query-memory-extraction.test.ts b/src/tests/headless-query-memory-extraction.test.ts new file mode 100644 index 000000000..d36e59e8b --- /dev/null +++ b/src/tests/headless-query-memory-extraction.test.ts @@ -0,0 +1,32 @@ +/** + * Smoke test for memoryExtraction diagnostics in headless-query output. + * + * Source-level guard: the query snapshot exposes recent extraction attempts + * and their failure_class so operators can inspect memory closeout health + * without sqlite3 or interactive /memory status. + */ + +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { test } from "vitest"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const querySrc = readFileSync( + join(__dirname, "..", "headless-query.ts"), + "utf-8", +); + +test("QuerySnapshot type declares memoryExtraction section", () => { + assert.match(querySrc, /memoryExtraction\?:/); + assert.match(querySrc, /total_attempts:\s*number/); + assert.match(querySrc, /recent_attempts:/); + assert.match(querySrc, /failure_class:\s*string \| null/); +}); + +test("buildQuerySnapshot reads memory extraction attempts", () => { + assert.match(querySrc, /listMemoryExtractionAttempts\(5\)/); + assert.match(querySrc, /memory_extraction_attempts/); + assert.match(querySrc, /failure_class/); +});