diff --git a/src/core/tdai-core.ts b/src/core/tdai-core.ts index 977d4a2..b2f66c0 100644 --- a/src/core/tdai-core.ts +++ b/src/core/tdai-core.ts @@ -68,6 +68,37 @@ export interface TdaiCoreOptions { instanceId?: string; } +export interface TdaiDiagnostics { + dataDir: string; + store: { + vectorStore: boolean; + embeddingService: boolean; + l0Count: number | null; + l1Count: number | null; + countError?: string; + }; + scheduler: { + enabled: boolean; + started: boolean; + queues: ReturnType | null; + }; + checkpoint: { + totalProcessed: number; + l0ConversationsCount: number; + totalMemoriesExtracted: number; + memoriesSinceLastPersona: number; + scenesProcessed: number; + }; + sessions: Array<{ + sessionKey: string; + trackedInMemory: boolean; + bufferedMessages: number; + schedulerState: ReturnType | null; + checkpointRunnerState: unknown; + checkpointPipelineState: unknown; + }>; +} + // ============================ // TdaiCore // ============================ @@ -363,6 +394,74 @@ export class TdaiCore { await this.scheduler.flushSession(sessionKey); } + /** + * Read-only diagnostics for Gateway/CLI observability. + * + * This intentionally avoids starting the scheduler by itself: diagnostics + * should report current state, not create new pipeline timers. + */ + async getDiagnostics(): Promise { + await this.storeReady?.catch(() => {}); + + const checkpoint = new CheckpointManager(this.dataDir, this.logger); + const cp = await checkpoint.read(); + const scheduler = this.scheduler; + + let l0Count: number | null = null; + let l1Count: number | null = null; + let countError: string | undefined; + if (this.vectorStore) { + try { + l0Count = await this.vectorStore.countL0(); + l1Count = await this.vectorStore.countL1(); + } catch (err) { + countError = err instanceof Error ? err.message : String(err); + } + } + + const sessionKeys = new Set([ + ...Object.keys(cp.runner_states ?? {}), + ...Object.keys(cp.pipeline_states ?? {}), + ...(scheduler?.getSessionKeys() ?? []), + ]); + + const sessions = Array.from(sessionKeys).sort().map((sessionKey) => { + const schedulerState = scheduler?.getSessionState(sessionKey) ?? null; + return { + sessionKey, + trackedInMemory: schedulerState !== null, + bufferedMessages: scheduler?.getBufferedMessageCount(sessionKey) ?? 0, + schedulerState, + checkpointRunnerState: cp.runner_states?.[sessionKey] ?? null, + checkpointPipelineState: cp.pipeline_states?.[sessionKey] ?? null, + }; + }); + + return { + dataDir: this.dataDir, + store: { + vectorStore: !!this.vectorStore, + embeddingService: !!this.embeddingService, + l0Count, + l1Count, + ...(countError ? { countError } : {}), + }, + scheduler: { + enabled: !!scheduler, + started: this.isSchedulerStarted(), + queues: scheduler?.getQueueSizes() ?? null, + }, + checkpoint: { + totalProcessed: cp.total_processed, + l0ConversationsCount: cp.l0_conversations_count, + totalMemoriesExtracted: cp.total_memories_extracted, + memoriesSinceLastPersona: cp.memories_since_last_persona, + scenesProcessed: cp.scenes_processed, + }, + sessions, + }; + } + // ============================ // Accessors (for migration bridge) // ============================ diff --git a/src/gateway/server.ts b/src/gateway/server.ts index b4c66ec..5769622 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -9,6 +9,7 @@ * POST /search/conversations — L0 conversation search * POST /session/end — Session end + flush * POST /seed — Batch seed historical conversations (L0 → L1) + * GET /diagnostics/sessions — Read-only session/pipeline diagnostics * * Built with Node.js native `http` module — no Express/Fastify dependency. * Designed to run as a managed sidecar alongside Hermes. @@ -37,6 +38,7 @@ import type { SessionEndResponse, SeedRequest, SeedResponse, + DiagnosticsResponse, GatewayErrorResponse, } from "./types.js"; import type { Logger } from "../core/types.js"; @@ -272,6 +274,8 @@ export class TdaiGateway { return await this.handleSessionEnd(req, res); case "POST /seed": return await this.handleSeed(req, res); + case "GET /diagnostics/sessions": + return await this.handleDiagnosticsSessions(res); default: sendError(res, 404, `Not found: ${method} ${pathname}`); } @@ -368,6 +372,11 @@ export class TdaiGateway { sendJson(res, 200, response); } + private async handleDiagnosticsSessions(res: http.ServerResponse): Promise { + const response: DiagnosticsResponse = await this.core.getDiagnostics(); + sendJson(res, 200, response); + } + private async handleRecall(req: http.IncomingMessage, res: http.ServerResponse): Promise { const body = await parseJsonBody(req); diff --git a/src/gateway/types.ts b/src/gateway/types.ts index 50b2ff4..53dbf66 100644 --- a/src/gateway/types.ts +++ b/src/gateway/types.ts @@ -25,6 +25,41 @@ export interface HealthResponse { }; } +// ============================ +// /diagnostics/sessions +// ============================ + +export interface DiagnosticsResponse { + dataDir: string; + store: { + vectorStore: boolean; + embeddingService: boolean; + l0Count: number | null; + l1Count: number | null; + countError?: string; + }; + scheduler: { + enabled: boolean; + started: boolean; + queues: unknown; + }; + checkpoint: { + totalProcessed: number; + l0ConversationsCount: number; + totalMemoriesExtracted: number; + memoriesSinceLastPersona: number; + scenesProcessed: number; + }; + sessions: Array<{ + sessionKey: string; + trackedInMemory: boolean; + bufferedMessages: number; + schedulerState: unknown; + checkpointRunnerState: unknown; + checkpointPipelineState: unknown; + }>; +} + // ============================ // /recall // ============================