import { type FeedbackPolicy } from './feedback-policy.js'; import { type FeedbackEventEnvelope, type FeedbackWebhookDestination } from './feedback-outbox.js'; export type SkillFeedbackLevel = 'L0' | 'L1' | 'L2'; export interface SkillFeedbackContext { runtime?: string; agent?: string; model?: string; platform?: string; session?: string; turn?: string; [key: string]: string | undefined; } export type TurnDeliveryScope = 'thread' | 'chat' | 'direct' | 'unknown'; export type TurnDeliveryCardMode = 'feedback' | 'card' | 'text'; export type TurnDeliveryStatus = 'delivered' | 'completed' | 'failed' | 'cancelled' | 'ambiguous'; export interface TurnDeliveryCorrelation { botAppId: string; sessionId: string; turnId: string; nativeSessionId?: string; platform: string; platformMessageId: string; platformAppId: string; chatId?: string; topicRootId?: string; dispatchAttempt?: number; contentRef?: string; scope?: TurnDeliveryScope; workflowId?: string; taskId?: string; parentTaskId?: string; cliId?: string; cliVersion?: string; model?: string; reasoningEffort?: string; skillName?: string; skillVersion?: string; cardMode: TurnDeliveryCardMode; status: TurnDeliveryStatus; durationMs?: number; usage?: Record; createdAt?: string; completedAt?: string; } export interface RecordTurnDeliveryInput extends TurnDeliveryCorrelation { content: string; policy?: FeedbackPolicy; baseCard?: Record; requesterSubjectId?: string; webhookDestinations?: FeedbackWebhookDestination[]; context?: Record; /** Distinguishes multiple canonical deliveries of the same worker turn. */ correlationDiscriminator?: string; } export interface RecordTurnTerminalInput { botAppId: string; sessionId: string; turnId: string; nativeSessionId?: string; dispatchAttempt?: number; status: Exclude; completedAt?: string; durationMs?: number; usage?: Record; } export interface TurnCompletionEventPayload { type: 'turn.completed'; version: 1; eventId: string; time: string; status: Exclude; deliveryId: string; contentHash: string; contentRef?: string; platform: string; platformMessageId: string; platformAppId: string; botAppId: string; sessionId: string; turnId: string; nativeSessionId?: string; dispatchAttempt?: number; chatId?: string; topicRootId?: string; durationMs?: number; usage?: Record; cliId?: string; cliVersion?: string; model?: string; reasoningEffort?: string; skillName?: string; skillVersion?: string; workflowId?: string; taskId?: string; parentTaskId?: string; } export declare class SkillFeedbackStore { readonly path: string; private readonly db; private constructor(); /** * Cross-process-safe migration. Multiple OS processes (each PM2 bot daemon * plus every `botmux send` subprocess) legitimately open the SAME * botmux-feedback.sqlite under a shared dataDir at cold start. The version is * therefore re-read INSIDE each step's BEGIN IMMEDIATE write lock: SQLite * serializes the writers, so a loser that raced in at version 0 sees the * winner's committed version and skips — instead of re-running bare * `ALTER TABLE ADD COLUMN` (no IF NOT EXISTS) and throwing `duplicate column`. * `BEGIN IMMEDIATE` acquisition is retried on `database is locked` beyond the * 5s busy_timeout so a burst of concurrent cold starts settles rather than * failing the open (which would poison the module-level store cache). */ private runMigrations; /** * Apply one migration step atomically and idempotently across processes. * Re-reads user_version after acquiring the write lock and only mutates when * still in the step's applicable range, so a process that lost the race is a * no-op instead of replaying DDL the winner already committed. Returns the * version observed under the lock so the caller can stop once fully migrated. */ private migrateStep; /** * Switch to WAL with bounded retry. `PRAGMA journal_mode=WAL` takes a write * lock to rewrite the DB header on a fresh file; under a cold-start stampede * it may either throw `database is locked` OR silently return the prior mode * (e.g. "delete") without switching. Both are retried — we verify the query * result actually reports "wal" rather than trusting a non-throwing call. */ private enableWalWithRetry; /** * Run `fn` inside a BEGIN IMMEDIATE / COMMIT, retrying the transaction when * the write lock cannot be acquired (`database is locked` / `SQLITE_BUSY`) * even after busy_timeout — expected when many bot daemons cold-start against * a shared dataDir at once. Non-lock errors roll back and rethrow. */ private withImmediateWrite; static open(dataDir: string): Promise; close(): void; private validateSchemaV1; pragmas(): { journalMode: string; foreignKeys: number; busyTimeout: number; }; debugCounts(): { responses: number; deliveries: number; }; schemaVersion(): number; integrityCheck(): { integrity: string; foreignKeys: unknown[]; }; recordTurnDelivery(input: RecordTurnDeliveryInput): ReturnType; recordTurnTerminal(input: RecordTurnTerminalInput): TurnCompletionEventPayload | undefined; /** * Nonblocking variant for the daemon's per-turn hot path. Acquiring the write * lock with `DatabaseSync` + busy_timeout>0 SYNCHRONOUSLY blocks the whole * Node event loop until the lock frees (measured multi-second stalls when a * `botmux send` subprocess held the lock). Here we temporarily drop * busy_timeout to 0 so `BEGIN IMMEDIATE` fails FAST on contention, return a * discriminable {busy:true} instead of throwing, and restore the timeout in * finally. The set/try/restore never crosses an await, and no Store method * holds a transaction across an await, so no same-process caller can observe * the borrowed 0 timeout. The daemon retries busy turns on a timer (which * yields the loop between attempts) instead of blocking inline. */ tryRecordTurnTerminal(input: RecordTurnTerminalInput): { done: true; payload: TurnCompletionEventPayload | undefined; } | { done: false; busy: true; }; /** Shared turn-terminal transaction body (caller owns BEGIN/COMMIT/ROLLBACK). */ private applyTurnTerminal; listTurnCompletionEvents(): Array<{ eventId: string; eventType: 'turn.completed'; version: 1; deliveryId: string; createdAt: string; payload: TurnCompletionEventPayload; }>; private insertFeedbackEvent; listFeedbackEvents(): Array; listFeedbackOutbox(): Array; claimFeedbackOutbox(input: { now: number; limit: number; claimToken: string; }): Array; settleFeedbackOutboxDelivered(outboxId: string, claimToken: string, httpStatus: number, deliveredAt: string): boolean; rescheduleFeedbackOutbox(outboxId: string, claimToken: string, input: { now: number; nextAttemptAt: number; error: string; httpStatus?: number; permanent?: boolean; }): boolean; resetExpiredFeedbackOutboxClaims(now: number, staleAfterMs: number): number; private reconcileTurnCompletion; createResponse(input: { interactionId: string; skillRunId?: string; content: string; contentRef?: string; context?: SkillFeedbackContext; }): { responseId: string; interactionId: string; contentHash: string; contentRef?: string; }; getResponse(responseId: string): ReturnType | undefined; createDelivery(input: { responseId: string; platform: string; platformAppId: string; platformMessageId: string; level?: SkillFeedbackLevel; policy?: FeedbackPolicy; baseCard?: Record; requesterSubjectId?: string; context?: SkillFeedbackContext; }): ReturnType; findDeliveryByPlatformMessage(platform: string, platformAppId: string, platformMessageId: string): ReturnType | undefined; recordFeedback(input: { platform: string; platformAppId: string; platformMessageId: string; operatorSubjectId: string; result: string; semantic?: 'positive' | 'progress' | 'negative'; reasonKey?: string; comment?: string; callbackKey: string; webhookDestinations?: FeedbackWebhookDestination[]; }): { status: 'accepted' | 'duplicate' | 'revised'; feedback: ReturnType; feedbackId?: string; }; listFeedbackRevisions(deliveryId: string, operatorSubjectId: string): Array>; getLatestFeedback(deliveryId: string, operatorSubjectId: string): ReturnType | undefined; private mapResponse; private mapDelivery; private mapFeedback; } export declare function getSkillFeedbackStore(dataDir: string): Promise; export declare function __testOnly_closeSkillFeedbackStores(): Promise; //# sourceMappingURL=skill-feedback-store.d.ts.map