Extraction is real spend and background work; the dock needs to see and stop it. User decisions: persist all cost data (chat record), per-stage usage granularity, and TRUE cancellation for abort. USAGE CAPTURE (live + persisted): - extraction.ts: per-call onStepFinish captures each billed step's usage (SDK LanguageModelUsage -> CallUsage: input/output/cache read+write/reasoning) into the registry's live per-stage usage (extractor/auditor; the merger is deterministic — no model call). - /api/graph/status runs now carry usage (model + steps per stage); cost/context folds are the consumer's (core/prices.ts — nothing stores dollars). - ChatRecord.extractions: append-only per-run usage log (mirrors the turns log): startedAt key, at, durationMs, merge stats when the merger ran, stages. Persisted on success AND failure — a failed run still spent money. Idempotent per startedAt; the list payload (ChatSummary) never leaks it. - EmissionStats moved to contracts/graph.ts (its contract home); core/graph.ts re-exports for existing importers. - Verified end-to-end against the real pipeline (backfill one chat): per-step token counts, reasoning tokens, and the auditor's growing prompt-cache reads captured and persisted. TRUE ABORT (the dock's abort button, user decision): - RunningExtraction owns one AbortController per run, shared across phases; abortExtraction(chatId) fires it and returns whether a run was aborted (idempotent no-op otherwise). The pending model call receives the signal — the provider cancels the request, spend stops. throwIfAborted() at phase boundaries ends runs aborted in sync gaps. - Abort is a deliberate stop, not a failure: same ledger discipline (nothing merged, nothing checkpointed, partial usage persisted) but the run reports zero emissions instead of throwing. Exported via src/server/observability.ts for master's /api/activity/abort route. - Tests: mid-run abort (hanging mock that rejects on the signal), live-accumulation + persisted-record assertions, store-seam idempotency, and the summary-payload leak guard.
25 lines
952 B
TypeScript
25 lines
952 B
TypeScript
/**
|
|
* GET /api/graph/status — the in-flight background extraction runs
|
|
* (vision §1: the pipeline status must be visible on the home graph).
|
|
* Thin transport over the pipeline's module-level registry: phase
|
|
* (extracting / auditing / merging) per chat, oldest first. Titles joined
|
|
* from the chat store; a missing chat record still reports the run.
|
|
*/
|
|
import { chatStore, extractionStatuses } from "@/src/server";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export async function GET() {
|
|
const runs = extractionStatuses().map((run) => ({
|
|
chatId: run.chatId,
|
|
title: chatStore.getChat(run.chatId)?.title ?? run.chatId,
|
|
phase: run.phase,
|
|
startedAt: run.startedAt,
|
|
// Live per-stage usage (the money channel): model + per-step CallUsage,
|
|
// accumulated as steps bill. Cost and context-window folds are the
|
|
// consumer's (core/prices.ts helpers — nothing stores dollars).
|
|
usage: run.usage,
|
|
}));
|
|
return Response.json({ runs });
|
|
}
|