Files
learnharness/src/server/observability.ts
T

64 lines
2.7 KiB
TypeScript

/**
* Server-side observability surface — the barrel-approved exports the
* transport layer (app/api/*) reads for background-process status.
*
* THE ACTIVITY FEED (2026-09 observability design): the bottom-right dock is
* visible in every view, so it needs the complete running-process picture —
* sub-agent jobs across ALL chats (not just the open one) plus the global
* extraction runs — in one poll. The dock is BACKGROUND PROCESSES ONLY
* (user decision, 2026-09-11): teaching sessions already have their own
* observability, so in-flight teacher turns are deliberately NOT in the
* feed. /api/jobs stays chat-scoped untouched: it is the transcript
* machinery's poll and carries turnInFlight for the re-attach seam
* (chat-transport); the dock is a different consumer with a different
* shape, served here. Single sources of truth are unchanged: the job
* store and the pipeline registry — this file only composes them.
*/
import { extractionStatuses, type ExtractionPhase } from "../agent/subagents/extraction.ts";
import { chatStore } from "../store/chats.ts";
import { listJobs } from "./jobs.ts";
import type { JobRecord } from "../core/contracts.ts";
export { extractionStatuses, type ExtractionPhase };
export { chatStore };
/** A running sub-agent job, joined with its chat's title for the dock. */
export interface ActivityJob extends JobRecord {
title: string;
}
/** One extraction-pipeline run (title joined), oldest first. */
export interface ActivityExtraction {
chatId: string;
title: string;
phase: ExtractionPhase;
startedAt: number;
}
/** The dock's whole world in one payload: every running process, every view.
* Jobs are RUNNING-ONLY — finished jobs belong to their chat's transcript;
* the blob disappears when the process ends. Cost math is NOT done here:
* raw usage rides on the records and the client folds it with the pure
* helpers in core/prices.ts (transport stays thin). */
export function activityFeed(): { jobs: ActivityJob[]; extractions: ActivityExtraction[] } {
const chats = chatStore.listChats();
const jobs: ActivityJob[] = [];
for (const chat of chats) {
for (const job of listJobs(chat.id)) {
if (job.status === "running") {
jobs.push({ ...job, title: chat.title ?? chat.id });
}
}
}
const extractions: ActivityExtraction[] = extractionStatuses().map((run) => ({
chatId: run.chatId,
title: chatStore.getChat(run.chatId)?.title ?? run.chatId,
phase: run.phase,
startedAt: run.startedAt,
}));
// oldest-first on last activity: the stable blob order (ISO strings sort chronologically)
jobs.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
return { jobs, extractions: extractions.sort((a, b) => a.startedAt - b.startedAt) };
}