Background pipeline status: registry + /api/graph/status + graph-view chips
The background extraction pipeline is invisible on the home surface — essential observability per the user's directive: the graph view must show what is currently running (extracting / auditing / merging) and for which chat. - extraction.ts: module-level registry of in-flight runs (chatId, phase, startedAt), updated at each phase transition in runExtraction and cleared in finally (done or failed). extractionStatuses() is the read; one process, one registry. - GET /api/graph/status: thin transport over the registry, chat titles joined from the chat store. - graph-view: polls every 2s and shows one chip per running pipeline (spinner + phase + title + elapsed seconds, bottom-left); when a run finishes (some -> none) the graph refetches, so merges land live. Status is cosmetic — poll errors never surface. - Unit test drives a mocked pipeline run and observes the registry mid-flight: 'extracting' during the extractor's generate, 'auditing' during the auditor's, empty after.
This commit is contained in:
@@ -304,6 +304,25 @@ function createAuditor(model: LanguageModel) {
|
||||
});
|
||||
}
|
||||
|
||||
// ─── the pipeline status registry ─────────────────────────────────────────────
|
||||
|
||||
/** The pipeline phases, in order — what the home status endpoint shows. */
|
||||
export type ExtractionPhase = "extracting" | "auditing" | "merging";
|
||||
|
||||
/** One in-flight pipeline run. Module-level: one process, one registry. */
|
||||
interface RunningExtraction {
|
||||
chatId: string;
|
||||
phase: ExtractionPhase;
|
||||
startedAt: number;
|
||||
}
|
||||
|
||||
const runningExtractions = new Map<string, RunningExtraction>();
|
||||
|
||||
/** In-flight pipeline runs (oldest first) — the /api/graph/status payload. */
|
||||
export function extractionStatuses(): RunningExtraction[] {
|
||||
return [...runningExtractions.values()].sort((a, b) => a.startedAt - b.startedAt);
|
||||
}
|
||||
|
||||
// ─── the pipeline ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface ExtractionModels {
|
||||
@@ -327,6 +346,14 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
|
||||
const auditorAgent = createAuditor(deps.auditor ?? modelFor("auditor"));
|
||||
const inFlight = new Set<string>();
|
||||
|
||||
function setPhase(chatId: string, phase: ExtractionPhase): void {
|
||||
runningExtractions.set(chatId, {
|
||||
chatId,
|
||||
phase,
|
||||
startedAt: runningExtractions.get(chatId)?.startedAt ?? Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
async function extract(chatId: string): Promise<SessionFindings> {
|
||||
const record = chatStore.getChat(chatId);
|
||||
if (!record) throw new Error(`Chat not found: ${chatId}`);
|
||||
@@ -388,8 +415,11 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
console.info(`[extraction ${chatId}] pipeline start`);
|
||||
setPhase(chatId, "extracting");
|
||||
const findings = await extract(chatId);
|
||||
setPhase(chatId, "auditing");
|
||||
const auditResult = await audit(chatId, findings);
|
||||
setPhase(chatId, "merging");
|
||||
// The merger re-reads the chat record itself — events are rebuilt
|
||||
// from the transcript, never from stale copies.
|
||||
const stats = mergeFindings(chatId, auditResult, { chatStore, graphStore });
|
||||
@@ -405,6 +435,7 @@ export function createExtractionPipeline(deps: ExtractionPipelineDeps): Extracti
|
||||
throw err;
|
||||
} finally {
|
||||
inFlight.delete(chatId);
|
||||
runningExtractions.delete(chatId); // done or failed — no longer running
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user