/** * questions/ports/question.persistence.port — question persistence contracts. * * Protocol-level persistence contract for structured questions generated by * QuestionerAgent. Implementations live in the backend and are injected into * ProtocolDeps. * * IND-547: canonical question persistence port. */ import type { Question, QuestionMode, QuestionPurpose, QuestionStrategy, QuestionDetection, QuestionActor, QuestionAnswer, UnderspecificationType } from "../domain/question.schema.js"; /** Shape accepted by `persist()` — everything needed to insert a question row. */ export interface PersistableQuestion { detection: QuestionDetection; actors: QuestionActor[]; payload: Question; strategy: QuestionStrategy; /** Internal QUD repair category; null when the question repairs no underspecification. */ underspecificationType?: UnderspecificationType | null; /** Conversation ID — set when the question originates from a chat session. */ conversationId?: string; } /** Shape returned by `findPending()` — a persisted question with its DB id and status. */ export interface PersistedQuestion { id: string; detection: QuestionDetection; actors: QuestionActor[]; payload: Question; status: "pending" | "answered" | "dismissed"; answer: QuestionAnswer | null; createdAt: string; } /** Optional filters for `findPending()`. */ export interface QuestionFilters { mode?: QuestionMode; purpose?: QuestionPurpose; sourceType?: string; sourceId?: string; /** Optional selected-intent scope. When `scopeType === 'intent'`, `scopeId` is the selected intent id. */ scopeType?: 'intent'; scopeId?: string; } /** * Resolution outcome for a single awaited chat question. * `timeout` means the wait budget elapsed (or the run was aborted) before the * user responded — the question stays `pending` in the database. */ export interface ChatQuestionAnswerOutcome { questionId: string; status: "answered" | "dismissed" | "timeout"; answer?: QuestionAnswer; } /** * Host bridge for the orchestrator's blocking `ask_user_question` tool. * `persist` synchronously inserts chat-mode question rows; `awaitAnswers` * blocks until every question resolves (answer/dismiss via the questions * REST endpoints) or the wait times out / the run is aborted. * * Implementations live in the backend (in-memory per-question wait bus, * single-instance semantics like the chat steer/queue interrupt emitter). */ export interface ChatQuestionsHost { persist(batch: PersistableQuestion[]): Promise; awaitAnswers(questionIds: string[], opts?: { timeoutMs?: number; signal?: AbortSignal; }): Promise; } /** * Narrow persistence port for ambient question delivery adapters. * Inject this instead of QuestionerDatabase when only persistence is needed. */ export interface QuestionerDatabase { /** Persist a batch of generated questions (up to 3 per generation). * @returns The IDs of the inserted rows. */ persist(questions: PersistableQuestion[]): Promise; /** Find pending questions for a user, optionally filtered by mode/source. */ findPending(userId: string, filters?: QuestionFilters): Promise; /** Record an answer for a question. Sets status to "answered". * Only succeeds if the user is an actor on a pending question. * @returns `true` if updated, `false` if not found, not pending, or unauthorized. */ answer(questionId: string, userId: string, answer: QuestionAnswer): Promise; /** Dismiss a question. Sets status to "dismissed". * Only succeeds if the user is an actor on a pending question. * @returns `true` if updated, `false` if not found, not pending, or unauthorized. */ dismiss(questionId: string, userId: string): Promise; }