/** * POST /ingest — receives hook payloads from the emit.cjs scripts generated * by core/inject/claude-settings.ts. * * Self-contained: the integrator mounts this router on the main app * (`app.use(createIngestRouter(deps))`) and owns everything else (session * manager, WS bridges, canvas). This file has no knowledge of express app * setup beyond its own router. */ import { type Router } from "express"; import type { AnalyticsEvent, HarnessKind } from "../shared/types.js"; import type { NormalizeContext } from "../core/collector/normalizer.js"; import { type SeqCounter } from "../core/collector/seq.js"; export interface IngestSessionContext { harness: HarnessKind; userId: string | null; tenantId: string | null; machineId: string; /** Already-known agent session id, if resolved from an earlier session.start. */ agentSessionId: string | null; } export interface IngestDeps { /** Authenticate and return the server-owned epoch bound to this capability. */ authenticate: (harnessSessionId: string, token: string) => string | null; /** * True while this exact PTY generation may finish ingest work. A terminal * event already admitted before exit may complete until a replacement PTY * takes ownership; the epoch still comes only from trusted server state. */ isCurrentRuntime: (harnessSessionId: string, runtimeEpoch: string) => boolean; /** Raw hook payload -> AnalyticsEvent, or null to skip (e.g. PreToolUse). */ normalize: (hookEvent: string, hookPayload: Record, context: NormalizeContext) => AnalyticsEvent | null; /** Look up session context for a harnessSessionId. Undefined = unknown session, drop. */ resolveSession: (harnessSessionId: string) => IngestSessionContext | undefined; /** Called once a session.start event reveals the agent's own session id. */ /** False rejects a SessionStart whose vendor identity conflicts with its pin. */ onAgentSessionResolved: (harnessSessionId: string, agentSessionId: string, source: unknown, runtimeEpoch: string) => boolean | Promise; /** * Called once a SessionStart(-equivalent) event is actually processed for * a session — the signal that its TUI is genuinely interactive, not just * that its pty exists. See `SessionManager.setReady`/`HarnessSession.ready` * for what this gates. Fires alongside `onAgentSessionResolved` (same * event), kept separate since "ready" and "agent session id known" are * conceptually distinct even though they happen to co-occur today. */ onSessionReady?: (harnessSessionId: string, runtimeEpoch: string) => void; store: { append(event: AnalyticsEvent): Promise; }; batcher: { enqueue(event: AnalyticsEvent): void; }; /** Optional transcript backfill for turn.completed / session.end. */ enrichFromTranscript?: (event: AnalyticsEvent, transcriptPath: string | undefined) => Promise; /** Called for every successfully normalized event (after any transcript * enrichment), before it's persisted — e.g. to feed a tool.call event's * command/output text to dev-server port detection. */ onNormalizedEvent?: (event: AnalyticsEvent, runtimeEpoch: string) => void; /** * Called for every event AFTER it has been persisted to the local store — * the seam for consumers that need to read the store back and see this event * in it. Archiving a session's record on `session.end` is exactly that: fired * from `onNormalizedEvent` it would race the append and store a record whose * `endedAt` is null. * * Synchronous and best-effort, like the other hooks here: whatever it starts * is the consumer's to detach, and it must not throw. */ onEventPersisted?: (event: AnalyticsEvent, runtimeEpoch: string) => void; /** * Called for every raw hook event BEFORE normalization — fired even for * hook events that don't produce an analytics event. A UI-transport-only * passthrough seam (bypasses the analytics normalize/store pipeline) for * consumers that need to observe raw hook activity. Currently unused; * retained as an extension point. */ onRawHookEvent?: (hookEvent: string, harnessSessionId: string, payload: Record, runtimeEpoch: string) => void; onError?: (err: unknown) => void; /** Injectable for tests; defaults to a fresh per-router counter. */ seqCounter?: SeqCounter; } /** * Exported so an in-process, non-HTTP event source (currently: the Codex * transcript tailer, which has no hook script to POST here) can feed the * exact same normalize -> transcript-enrich -> store -> batcher pipeline * that a real hook POST goes through, without a needless HTTP round-trip * back into this same server. Field names deliberately match the HTTP body * shape (hookEvent/harnessSessionId/payload) so callers can construct one * directly from whatever `{hookEvent, payload}` pair their event source * already produces. */ export interface IngestRequestBody { hookEvent?: string; receivedAt?: string; harnessSessionId?: string; payload?: Record; } export interface IngestRouterOptions { /** Test seam; production permits sustained hook traffic while bounding floods. */ rateLimitWindowMs?: number; rateLimitMax?: number; } export declare function processIngest(body: IngestRequestBody, deps: IngestDeps, seqCounter: SeqCounter, runtimeEpoch: string): Promise; export declare function createIngestRouter(deps: IngestDeps, options?: IngestRouterOptions): Router; //# sourceMappingURL=ingest.d.ts.map