@db3.ai/app 0.1.0-beta.1 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -9
- package/agent-instructions.md +76 -0
- package/dist/ai/AIErrors.d.ts +7 -1
- package/dist/ai/AIErrors.js +8 -1
- package/dist/ai/AIFailover.d.ts +10 -0
- package/dist/ai/AIFailover.js +3 -2
- package/dist/ai/AIProviderAdmission.d.ts +101 -0
- package/dist/ai/AIProviderAdmission.js +336 -0
- package/dist/ai/AIRateLimiter.d.ts +11 -2
- package/dist/ai/AIRateLimiter.js +125 -10
- package/dist/ai/AIRequestTrackingError.d.ts +22 -0
- package/dist/ai/AIRequestTrackingError.js +33 -0
- package/dist/ai/Agent.d.ts +7 -7
- package/dist/ai/Agent.js +84 -56
- package/dist/ai/Ai.d.ts +11 -3
- package/dist/ai/Ai.js +94 -44
- package/dist/ai/OpenAIProviderError.js +1 -1
- package/dist/ai/ProviderSseObserver.d.ts +19 -0
- package/dist/ai/ProviderSseObserver.js +98 -0
- package/dist/ai/aiServiceTier.d.ts +5 -0
- package/dist/ai/aiServiceTier.js +14 -0
- package/dist/ai/chunkEmbeddingText.d.ts +14 -0
- package/dist/ai/chunkEmbeddingText.js +147 -0
- package/dist/ai/contracts/AI.d.ts +16 -0
- package/dist/ai/contracts/AIRequestErrorDiagnostics.d.ts +12 -0
- package/dist/ai/contracts/AIRequestErrorDiagnostics.js +1 -0
- package/dist/ai/contracts/AIServiceTier.d.ts +2 -0
- package/dist/ai/contracts/AIServiceTier.js +1 -0
- package/dist/ai/contracts/Agent.d.ts +3 -0
- package/dist/ai/contracts/EmbeddingTextChunk.d.ts +25 -0
- package/dist/ai/contracts/EmbeddingTextChunk.js +1 -0
- package/dist/ai/contracts/Pricing.d.ts +7 -1
- package/dist/ai/contracts/Providers.d.ts +2 -0
- package/dist/ai/contracts/index.d.ts +2 -0
- package/dist/ai/contracts/index.js +1 -0
- package/dist/ai/index.d.ts +8 -2
- package/dist/ai/index.js +5 -2
- package/dist/ai/{Models.d.ts → modelPricing.d.ts} +2 -2
- package/dist/ai/{Models.js → modelPricing.js} +38 -4
- package/dist/apps/AppInstallation.d.ts +21 -0
- package/dist/apps/AppInstallation.js +16 -0
- package/dist/apps/AppRouteError.d.ts +6 -0
- package/dist/apps/AppRouteError.js +10 -0
- package/dist/apps/AppService.d.ts +24 -0
- package/dist/apps/AppService.js +21 -0
- package/dist/apps/Apps.d.ts +59 -0
- package/dist/apps/Apps.js +566 -0
- package/dist/apps/appsPlugin.d.ts +3 -0
- package/dist/apps/appsPlugin.js +36 -0
- package/dist/apps/commands/disableApp.d.ts +2 -0
- package/dist/apps/commands/disableApp.js +5 -0
- package/dist/apps/commands/enableApp.d.ts +2 -0
- package/dist/apps/commands/enableApp.js +5 -0
- package/dist/apps/commands/index.d.ts +10 -0
- package/dist/apps/commands/index.js +27 -0
- package/dist/apps/commands/installApp.d.ts +2 -0
- package/dist/apps/commands/installApp.js +5 -0
- package/dist/apps/commands/listApps.d.ts +2 -0
- package/dist/apps/commands/listApps.js +5 -0
- package/dist/apps/commands/makeAppMigration.d.ts +2 -0
- package/dist/apps/commands/makeAppMigration.js +9 -0
- package/dist/apps/commands/rollbackApp.d.ts +2 -0
- package/dist/apps/commands/rollbackApp.js +5 -0
- package/dist/apps/commands/uninstallApp.d.ts +2 -0
- package/dist/apps/commands/uninstallApp.js +5 -0
- package/dist/apps/contracts/AppAction.d.ts +2 -0
- package/dist/apps/contracts/AppAction.js +1 -0
- package/dist/apps/contracts/AppDefinition.d.ts +45 -0
- package/dist/apps/contracts/AppDefinition.js +1 -0
- package/dist/apps/contracts/AppDescription.d.ts +32 -0
- package/dist/apps/contracts/AppDescription.js +1 -0
- package/dist/apps/contracts/AppManifest.d.ts +30 -0
- package/dist/apps/contracts/AppManifest.js +1 -0
- package/dist/apps/contracts/AppNavigation.d.ts +9 -0
- package/dist/apps/contracts/AppNavigation.js +1 -0
- package/dist/apps/contracts/AppNavigationBadge.d.ts +6 -0
- package/dist/apps/contracts/AppNavigationBadge.js +1 -0
- package/dist/apps/contracts/AppNavigationContext.d.ts +8 -0
- package/dist/apps/contracts/AppNavigationContext.js +1 -0
- package/dist/apps/contracts/AppNavigationResult.d.ts +12 -0
- package/dist/apps/contracts/AppNavigationResult.js +1 -0
- package/dist/apps/contracts/AppRoute.d.ts +19 -0
- package/dist/apps/contracts/AppRoute.js +1 -0
- package/dist/apps/contracts/DiscoveredApp.d.ts +10 -0
- package/dist/apps/contracts/DiscoveredApp.js +1 -0
- package/dist/apps/contracts/InstalledApps.d.ts +3 -0
- package/dist/apps/contracts/InstalledApps.js +1 -0
- package/dist/apps/contracts/index.d.ts +11 -0
- package/dist/apps/contracts/index.js +1 -0
- package/dist/apps/defineApp.d.ts +7 -0
- package/dist/apps/defineApp.js +8 -0
- package/dist/apps/discoverApps.d.ts +3 -0
- package/dist/apps/discoverApps.js +62 -0
- package/dist/apps/fastify.d.ts +1 -0
- package/dist/apps/fastify.js +1 -0
- package/dist/apps/index.d.ts +7 -0
- package/dist/apps/index.js +7 -0
- package/dist/apps/normalizeAppNavigation.d.ts +3 -0
- package/dist/apps/normalizeAppNavigation.js +30 -0
- package/dist/apps/registerAppRoutes.d.ts +6 -0
- package/dist/apps/registerAppRoutes.js +52 -0
- package/dist/apps/validateAppDefinitions.d.ts +3 -0
- package/dist/apps/validateAppDefinitions.js +65 -0
- package/dist/apps/vite.d.ts +1 -0
- package/dist/apps/vite.js +1 -0
- package/dist/apps/writeAppTypes.d.ts +2 -0
- package/dist/apps/writeAppTypes.js +13 -0
- package/dist/auth/Auth.d.ts +18 -0
- package/dist/auth/Auth.js +89 -13
- package/dist/auth/PasswordLoginAttempt.d.ts +50 -0
- package/dist/auth/PasswordLoginAttempt.js +60 -0
- package/dist/auth/PasswordSuspendedError.d.ts +10 -0
- package/dist/auth/PasswordSuspendedError.js +13 -0
- package/dist/auth/contracts/PasswordAuthProviderOptions.d.ts +26 -0
- package/dist/auth/contracts/PasswordAuthProviderOptions.js +1 -0
- package/dist/auth/contracts/PasswordSuspension.d.ts +7 -0
- package/dist/auth/contracts/PasswordSuspension.js +1 -0
- package/dist/auth/contracts/PasswordSuspensionOptions.d.ts +10 -0
- package/dist/auth/contracts/PasswordSuspensionOptions.js +1 -0
- package/dist/auth/contracts/index.d.ts +3 -0
- package/dist/auth/contracts/index.js +3 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +2 -0
- package/dist/auth/providers/PasswordAuthProvider.d.ts +19 -23
- package/dist/auth/providers/PasswordAuthProvider.js +96 -8
- package/dist/auth/providers/resolveAuthProviders.js +1 -0
- package/dist/cli/Cli.d.ts +2 -0
- package/dist/cli/Cli.js +8 -0
- package/dist/cli/commands/repl.js +2 -1
- package/dist/cli/contracts/CliCommand.d.ts +2 -0
- package/dist/cli/contracts/CliCommandInfo.d.ts +12 -0
- package/dist/cli/contracts/CliCommandInfo.js +1 -0
- package/dist/cli/contracts/index.d.ts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/listDb3Commands.d.ts +8 -0
- package/dist/cli/listDb3Commands.js +10 -0
- package/dist/cli/runDb3.d.ts +3 -6
- package/dist/cli/runDb3.js +5 -59
- package/dist/cli/withDb3Cli.d.ts +4 -0
- package/dist/cli/withDb3Cli.js +60 -0
- package/dist/db/ActiveQueryBuilder.d.ts +8 -0
- package/dist/db/ActiveQueryBuilder.js +11 -0
- package/dist/db/ActiveRecord.d.ts +6 -0
- package/dist/db/ActiveRecord.js +8 -0
- package/dist/db/connection.js +14 -2
- package/dist/db/fields/TimestampField.d.ts +1 -1
- package/dist/db/fields/TimestampField.js +7 -12
- package/dist/db/migrations/DatabaseMigrationManager.d.ts +3 -1
- package/dist/db/migrations/DatabaseMigrationManager.js +9 -3
- package/dist/db/migrations/contracts/DatabaseMigration.d.ts +7 -0
- package/dist/db/migrations/index.d.ts +1 -1
- package/dist/db/migrations/index.js +1 -1
- package/dist/db/migrations/{KnexMigrationRenderer.d.ts → renderKnexMigration.d.ts} +4 -3
- package/dist/db/migrations/{KnexMigrationRenderer.js → renderKnexMigration.js} +7 -6
- package/dist/flows/FlowCompiler.js +1 -1
- package/dist/flows/Flows.js +1 -1
- package/dist/flows/index.d.ts +1 -1
- package/dist/flows/index.js +1 -1
- package/dist/health/Health.d.ts +41 -0
- package/dist/health/Health.js +153 -0
- package/dist/health/contracts/Health.d.ts +36 -0
- package/dist/health/contracts/Health.js +1 -0
- package/dist/health/contracts/HealthHttp.d.ts +15 -0
- package/dist/health/contracts/HealthHttp.js +1 -0
- package/dist/health/contracts/index.d.ts +2 -0
- package/dist/health/contracts/index.js +1 -0
- package/dist/health/databaseHealthCheck.d.ts +15 -0
- package/dist/health/databaseHealthCheck.js +18 -0
- package/dist/health/fastify.d.ts +13 -0
- package/dist/health/fastify.js +44 -0
- package/dist/health/index.d.ts +3 -0
- package/dist/health/index.js +2 -0
- package/dist/in-app/InApp.d.ts +33 -0
- package/dist/in-app/InApp.js +191 -0
- package/dist/in-app/InAppError.d.ts +6 -0
- package/dist/in-app/InAppError.js +10 -0
- package/dist/in-app/InAppRecord.d.ts +36 -0
- package/dist/in-app/InAppRecord.js +24 -0
- package/dist/in-app/contracts/InAppInbox.d.ts +45 -0
- package/dist/in-app/contracts/InAppInbox.js +1 -0
- package/dist/in-app/contracts/InAppMessage.d.ts +22 -0
- package/dist/in-app/contracts/InAppMessage.js +1 -0
- package/dist/in-app/contracts/InAppScope.d.ts +17 -0
- package/dist/in-app/contracts/InAppScope.js +1 -0
- package/dist/in-app/contracts/index.d.ts +3 -0
- package/dist/in-app/contracts/index.js +3 -0
- package/dist/in-app/index.d.ts +4 -0
- package/dist/in-app/index.js +4 -0
- package/dist/in-app/validation.d.ts +21 -0
- package/dist/in-app/validation.js +50 -0
- package/dist/index.d.ts +2 -14
- package/dist/index.js +2 -5
- package/dist/logging/DevelopmentConsole.d.ts +42 -0
- package/dist/logging/DevelopmentConsole.js +162 -0
- package/dist/logging/Log.d.ts +3 -1
- package/dist/logging/Log.js +5 -2
- package/dist/logging/PrettyLogFormatter.d.ts +16 -0
- package/dist/logging/PrettyLogFormatter.js +151 -0
- package/dist/logging/acquireDevelopmentConsole.d.ts +6 -0
- package/dist/logging/acquireDevelopmentConsole.js +21 -0
- package/dist/logging/contracts/DevelopmentConsoleOptions.d.ts +16 -0
- package/dist/logging/contracts/DevelopmentConsoleOptions.js +1 -0
- package/dist/logging/contracts/EmailLogOptions.d.ts +8 -0
- package/dist/logging/contracts/EmailLogOptions.js +1 -0
- package/dist/logging/contracts/HttpExchangeMonitorOptions.d.ts +12 -0
- package/dist/logging/contracts/HttpExchangeMonitorOptions.js +1 -0
- package/dist/logging/contracts/LogTransportOptions.d.ts +19 -0
- package/dist/logging/contracts/LogTransportOptions.js +1 -0
- package/dist/logging/contracts/LoggingOptions.d.ts +12 -1
- package/dist/logging/contracts/index.d.ts +4 -0
- package/dist/logging/contracts/index.js +4 -0
- package/dist/logging/drivers/PinoLoggerDriver.d.ts +3 -1
- package/dist/logging/drivers/PinoLoggerDriver.js +75 -16
- package/dist/logging/formatLogEmail.d.ts +4 -0
- package/dist/logging/formatLogEmail.js +21 -0
- package/dist/logging/highlightQueueMessage.d.ts +11 -0
- package/dist/logging/highlightQueueMessage.js +36 -0
- package/dist/logging/httpBodyCapture.d.ts +24 -0
- package/dist/logging/httpBodyCapture.js +321 -0
- package/dist/logging/index.d.ts +2 -0
- package/dist/logging/index.js +2 -0
- package/dist/logging/redactLogEmail.d.ts +2 -0
- package/dist/logging/redactLogEmail.js +12 -0
- package/dist/logging/registerHttpExchangeMonitor.d.ts +10 -0
- package/dist/logging/registerHttpExchangeMonitor.js +43 -0
- package/dist/logging/transports/EmailLogTransport.d.ts +18 -0
- package/dist/logging/transports/EmailLogTransport.js +47 -0
- package/dist/mail/Mail.d.ts +2 -0
- package/dist/mail/Mail.js +3 -0
- package/dist/mail/MailDeliveryError.d.ts +9 -0
- package/dist/mail/MailDeliveryError.js +16 -0
- package/dist/mail/index.d.ts +1 -0
- package/dist/mail/index.js +1 -0
- package/dist/mail/transports/MailgunTransport.js +2 -0
- package/dist/mail/transports/ResendTransport.d.ts +3 -2
- package/dist/mail/transports/ResendTransport.js +22 -14
- package/dist/media/MediaManager.d.ts +4 -2
- package/dist/media/MediaManager.js +40 -8
- package/dist/network/BlockedUrlError.d.ts +16 -0
- package/dist/network/BlockedUrlError.js +19 -0
- package/dist/network/assertPublicUrl.d.ts +22 -0
- package/dist/network/assertPublicUrl.js +67 -0
- package/dist/network/contracts/GuardedFetch.d.ts +35 -0
- package/dist/network/contracts/GuardedFetch.js +1 -0
- package/dist/network/contracts/PublicUrlOptions.d.ts +17 -0
- package/dist/network/contracts/PublicUrlOptions.js +1 -0
- package/dist/network/contracts/index.d.ts +2 -0
- package/dist/network/contracts/index.js +2 -0
- package/dist/network/guardedFetch.d.ts +29 -0
- package/dist/network/guardedFetch.js +133 -0
- package/dist/network/index.d.ts +6 -0
- package/dist/network/index.js +6 -0
- package/dist/network/playwright/GuardedBrowserProxy.d.ts +32 -0
- package/dist/network/playwright/GuardedBrowserProxy.js +172 -0
- package/dist/network/playwright/contracts/GuardedBrowserContextOptions.d.ts +37 -0
- package/dist/network/playwright/contracts/GuardedBrowserContextOptions.js +1 -0
- package/dist/network/playwright/contracts/index.d.ts +1 -0
- package/dist/network/playwright/contracts/index.js +1 -0
- package/dist/network/playwright/index.d.ts +2 -0
- package/dist/network/playwright/index.js +2 -0
- package/dist/network/playwright/newGuardedBrowserContext.d.ts +21 -0
- package/dist/network/playwright/newGuardedBrowserContext.js +86 -0
- package/dist/network/privateNetworkAddresses.d.ts +44 -0
- package/dist/network/privateNetworkAddresses.js +157 -0
- package/dist/network/publicAddressLookup.d.ts +26 -0
- package/dist/network/publicAddressLookup.js +42 -0
- package/dist/notifications/Notifications.d.ts +27 -0
- package/dist/notifications/Notifications.js +99 -0
- package/dist/notifications/contracts/Notification.d.ts +56 -0
- package/dist/notifications/contracts/Notification.js +1 -0
- package/dist/notifications/contracts/NotificationDelivery.d.ts +11 -0
- package/dist/notifications/contracts/NotificationDelivery.js +1 -0
- package/dist/notifications/contracts/index.d.ts +2 -0
- package/dist/notifications/contracts/index.js +2 -0
- package/dist/notifications/index.d.ts +2 -0
- package/dist/notifications/index.js +2 -0
- package/dist/queue/JobRunAttempt.d.ts +16 -0
- package/dist/queue/JobRunAttempt.js +17 -0
- package/dist/queue/JobRunEvent.d.ts +15 -0
- package/dist/queue/JobRunEvent.js +16 -0
- package/dist/queue/JobRunStoppedError.d.ts +6 -0
- package/dist/queue/JobRunStoppedError.js +10 -0
- package/dist/queue/Queue.d.ts +19 -4
- package/dist/queue/Queue.js +122 -9
- package/dist/queue/QueueTerminalError.d.ts +9 -0
- package/dist/queue/QueueTerminalError.js +12 -0
- package/dist/queue/QueueWorker.d.ts +12 -5
- package/dist/queue/QueueWorker.js +94 -9
- package/dist/queue/QueueWorkerReporter.d.ts +17 -0
- package/dist/queue/QueueWorkerReporter.js +71 -0
- package/dist/queue/QueueableJob.d.ts +2 -2
- package/dist/queue/RunnableJob.d.ts +78 -0
- package/dist/queue/RunnableJob.js +284 -0
- package/dist/queue/console.d.ts +8 -0
- package/dist/queue/console.js +53 -19
- package/dist/queue/contracts/QueueDriver.d.ts +8 -0
- package/dist/queue/contracts/QueueJobRunner.d.ts +4 -0
- package/dist/queue/contracts/QueueJobRunner.js +1 -0
- package/dist/queue/contracts/QueuePayload.d.ts +1 -1
- package/dist/queue/contracts/QueueSelection.d.ts +11 -0
- package/dist/queue/contracts/QueueSelection.js +1 -0
- package/dist/queue/contracts/QueueService.d.ts +18 -4
- package/dist/queue/contracts/QueueWorkerLifecycle.d.ts +2 -2
- package/dist/queue/contracts/QueueWorkerSnapshot.d.ts +14 -0
- package/dist/queue/contracts/QueueWorkerSnapshot.js +1 -0
- package/dist/queue/contracts/QueueableJob.d.ts +7 -3
- package/dist/queue/contracts/QueueableJobContract.d.ts +12 -0
- package/dist/queue/contracts/QueueableJobContract.js +1 -0
- package/dist/queue/contracts/index.d.ts +4 -0
- package/dist/queue/contracts/index.js +2 -0
- package/dist/queue/drivers/DatabaseQueueDriver.d.ts +4 -0
- package/dist/queue/drivers/DatabaseQueueDriver.js +27 -3
- package/dist/queue/drivers/RedisQueueDriver.d.ts +7 -0
- package/dist/queue/drivers/RedisQueueDriver.js +37 -0
- package/dist/queue/index.d.ts +5 -0
- package/dist/queue/index.js +5 -0
- package/dist/queue/jobEnvelopeHasPrefix.d.ts +3 -0
- package/dist/queue/jobEnvelopeHasPrefix.js +4 -0
- package/dist/scheduler/ScheduledEvent.d.ts +8 -4
- package/dist/scheduler/ScheduledEvent.js +17 -3
- package/dist/scheduler/ScheduledJob.d.ts +1 -1
- package/dist/scheduler/Scheduler.d.ts +5 -0
- package/dist/scheduler/Scheduler.js +92 -28
- package/dist/scheduler/SchedulerCheckpointRecord.d.ts +16 -0
- package/dist/scheduler/SchedulerCheckpointRecord.js +53 -0
- package/dist/scheduler/SchedulerWorker.d.ts +3 -5
- package/dist/scheduler/SchedulerWorker.js +58 -14
- package/dist/scheduler/console.js +3 -1
- package/dist/scheduler/contracts/Schedule.d.ts +19 -0
- package/dist/scheduler/contracts/Schedule.js +1 -0
- package/dist/scheduler/contracts/Scheduler.d.ts +18 -6
- package/dist/scheduler/contracts/SchedulerCheckpoint.d.ts +23 -0
- package/dist/scheduler/contracts/SchedulerCheckpoint.js +1 -0
- package/dist/scheduler/contracts/SchedulerConsole.d.ts +5 -0
- package/dist/scheduler/contracts/SchedulerWorker.d.ts +6 -1
- package/dist/scheduler/contracts/index.d.ts +2 -0
- package/dist/scheduler/contracts/index.js +2 -0
- package/dist/scheduler/index.d.ts +1 -0
- package/dist/scheduler/index.js +1 -0
- package/dist/server/App.d.ts +36 -4
- package/dist/server/App.js +57 -7
- package/dist/server/contracts/HttpErrorHandlerOptions.d.ts +29 -0
- package/dist/server/contracts/HttpErrorHandlerOptions.js +1 -0
- package/dist/server/contracts/index.d.ts +1 -0
- package/dist/server/contracts/index.js +1 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +4 -0
- package/dist/server/publicServerErrorMessage.d.ts +11 -0
- package/dist/server/publicServerErrorMessage.js +16 -0
- package/dist/server/registerBrowserJsonFormatting.d.ts +16 -0
- package/dist/server/registerBrowserJsonFormatting.js +100 -0
- package/dist/server/registerHttpErrorHandler.d.ts +20 -0
- package/dist/server/registerHttpErrorHandler.js +93 -0
- package/dist/ssr/vite/contracts/ViteSsrOptions.d.ts +2 -0
- package/dist/ssr/vite/fastifyViteSsr.js +4 -3
- package/dist/storage/drivers/FlystorageDisk.js +2 -2
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.js +2 -2
- package/dist/websocket/WebSocketChannels.d.ts +18 -0
- package/dist/websocket/WebSocketChannels.js +129 -0
- package/dist/websocket/WebSocketConnection.d.ts +25 -0
- package/dist/websocket/WebSocketConnection.js +244 -0
- package/dist/websocket/WebSockets.d.ts +41 -0
- package/dist/websocket/WebSockets.js +194 -0
- package/dist/websocket/client/WebSocketClient.d.ts +21 -0
- package/dist/websocket/client/WebSocketClient.js +230 -0
- package/dist/websocket/client/index.d.ts +3 -0
- package/dist/websocket/client/index.js +1 -0
- package/dist/websocket/contracts/WebSocketChannel.d.ts +20 -0
- package/dist/websocket/contracts/WebSocketChannel.js +1 -0
- package/dist/websocket/contracts/WebSocketClient.d.ts +45 -0
- package/dist/websocket/contracts/WebSocketClient.js +1 -0
- package/dist/websocket/contracts/WebSocketEndpoint.d.ts +43 -0
- package/dist/websocket/contracts/WebSocketEndpoint.js +1 -0
- package/dist/websocket/contracts/WebSocketOptions.d.ts +48 -0
- package/dist/websocket/contracts/WebSocketOptions.js +1 -0
- package/dist/websocket/contracts/WebSocketPresence.d.ts +6 -0
- package/dist/websocket/contracts/WebSocketPresence.js +1 -0
- package/dist/websocket/contracts/WebSocketServiceOptions.d.ts +12 -0
- package/dist/websocket/contracts/WebSocketServiceOptions.js +1 -0
- package/dist/websocket/contracts/index.d.ts +6 -0
- package/dist/websocket/contracts/index.js +1 -0
- package/dist/websocket/defineChannel.d.ts +8 -0
- package/dist/websocket/defineChannel.js +13 -0
- package/dist/websocket/defineWebSocket.d.ts +14 -0
- package/dist/websocket/defineWebSocket.js +4 -0
- package/dist/websocket/fastify.d.ts +11 -0
- package/dist/websocket/fastify.js +38 -0
- package/dist/websocket/index.d.ts +5 -0
- package/dist/websocket/index.js +4 -0
- package/package.json +108 -7
- package/src/ai/AIErrors.ts +10 -0
- package/src/ai/AIFailover.ts +3 -2
- package/src/ai/AIProviderAdmission.ts +329 -0
- package/src/ai/AIRateLimiter.ts +134 -10
- package/src/ai/AIRequestTrackingError.ts +37 -0
- package/src/ai/Agent.ts +85 -71
- package/src/ai/Ai.ts +104 -48
- package/src/ai/OpenAIProviderError.ts +1 -1
- package/src/ai/ProviderSseObserver.ts +89 -0
- package/src/ai/README.md +216 -11
- package/src/ai/aiServiceTier.ts +14 -0
- package/src/ai/chunkEmbeddingText.ts +137 -0
- package/src/ai/contracts/AI.ts +17 -0
- package/src/ai/contracts/AIRequestErrorDiagnostics.ts +12 -0
- package/src/ai/contracts/AIServiceTier.ts +2 -0
- package/src/ai/contracts/Agent.ts +3 -0
- package/src/ai/contracts/EmbeddingTextChunk.ts +26 -0
- package/src/ai/contracts/Pricing.ts +7 -1
- package/src/ai/contracts/Providers.ts +2 -0
- package/src/ai/contracts/index.ts +2 -0
- package/src/ai/examples/prepareDocumentEmbeddings.ts +20 -0
- package/src/ai/index.ts +9 -2
- package/src/ai/{Models.ts → modelPricing.ts} +37 -4
- package/src/apps/AppInstallation.ts +25 -0
- package/src/apps/AppRouteError.ts +8 -0
- package/src/apps/AppService.ts +30 -0
- package/src/apps/Apps.ts +508 -0
- package/src/apps/README.md +354 -0
- package/src/apps/appsPlugin.ts +34 -0
- package/src/apps/commands/disableApp.ts +6 -0
- package/src/apps/commands/enableApp.ts +6 -0
- package/src/apps/commands/index.ts +29 -0
- package/src/apps/commands/installApp.ts +6 -0
- package/src/apps/commands/listApps.ts +6 -0
- package/src/apps/commands/makeAppMigration.ts +9 -0
- package/src/apps/commands/rollbackApp.ts +6 -0
- package/src/apps/commands/uninstallApp.ts +6 -0
- package/src/apps/contracts/AppAction.ts +2 -0
- package/src/apps/contracts/AppDefinition.ts +49 -0
- package/src/apps/contracts/AppDescription.ts +27 -0
- package/src/apps/contracts/AppManifest.ts +32 -0
- package/src/apps/contracts/AppNavigation.ts +10 -0
- package/src/apps/contracts/AppNavigationBadge.ts +6 -0
- package/src/apps/contracts/AppNavigationContext.ts +6 -0
- package/src/apps/contracts/AppNavigationResult.ts +9 -0
- package/src/apps/contracts/AppRoute.ts +18 -0
- package/src/apps/contracts/DiscoveredApp.ts +11 -0
- package/src/apps/contracts/InstalledApps.ts +2 -0
- package/src/apps/contracts/index.ts +11 -0
- package/src/apps/defineApp.ts +10 -0
- package/src/apps/discoverApps.ts +56 -0
- package/src/apps/fastify.ts +1 -0
- package/src/apps/index.ts +7 -0
- package/src/apps/normalizeAppNavigation.ts +29 -0
- package/src/apps/registerAppRoutes.ts +48 -0
- package/src/apps/validateAppDefinitions.ts +52 -0
- package/src/apps/vite.ts +1 -0
- package/src/apps/writeAppTypes.ts +14 -0
- package/src/auth/Auth.ts +89 -23
- package/src/auth/PasswordLoginAttempt.ts +72 -0
- package/src/auth/PasswordSuspendedError.ts +13 -0
- package/src/auth/README.md +94 -4
- package/src/auth/contracts/PasswordAuthProviderOptions.ts +30 -0
- package/src/auth/contracts/PasswordSuspension.ts +7 -0
- package/src/auth/contracts/PasswordSuspensionOptions.ts +10 -0
- package/src/auth/contracts/index.ts +3 -0
- package/src/auth/examples/runPasswordAuth.ts +6 -6
- package/src/auth/index.ts +2 -0
- package/src/auth/providers/PasswordAuthProvider.ts +96 -30
- package/src/auth/providers/resolveAuthProviders.ts +1 -0
- package/src/cli/Cli.ts +9 -0
- package/src/cli/README.md +19 -0
- package/src/cli/commands/repl.ts +2 -1
- package/src/cli/contracts/CliCommand.ts +2 -0
- package/src/cli/contracts/CliCommandInfo.ts +13 -0
- package/src/cli/contracts/index.ts +1 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/listDb3Commands.ts +12 -0
- package/src/cli/runDb3.ts +5 -51
- package/src/cli/withDb3Cli.ts +55 -0
- package/src/db/ActiveQueryBuilder.ts +12 -0
- package/src/db/ActiveRecord.ts +9 -0
- package/src/db/README.md +45 -1
- package/src/db/connection.ts +12 -1
- package/src/db/examples/FieldNote.ts +7 -13
- package/src/db/examples/KnowledgeNote.ts +7 -13
- package/src/db/examples/runNoteMigrations.ts +11 -11
- package/src/db/fields/TimestampField.ts +8 -17
- package/src/db/migrations/DatabaseMigrationManager.ts +12 -4
- package/src/db/migrations/contracts/DatabaseMigration.ts +9 -0
- package/src/db/migrations/index.ts +1 -1
- package/src/db/migrations/{KnexMigrationRenderer.ts → renderKnexMigration.ts} +8 -14
- package/src/flows/FlowCompiler.ts +1 -1
- package/src/flows/Flows.ts +1 -1
- package/src/flows/index.ts +1 -1
- package/src/health/Health.ts +163 -0
- package/src/health/README.md +46 -0
- package/src/health/contracts/Health.ts +47 -0
- package/src/health/contracts/HealthHttp.ts +19 -0
- package/src/health/contracts/index.ts +2 -0
- package/src/health/databaseHealthCheck.ts +21 -0
- package/src/health/fastify.ts +49 -0
- package/src/health/index.ts +3 -0
- package/src/in-app/InApp.ts +173 -0
- package/src/in-app/InAppError.ts +8 -0
- package/src/in-app/InAppRecord.ts +41 -0
- package/src/in-app/README.md +193 -0
- package/src/in-app/contracts/InAppInbox.ts +51 -0
- package/src/in-app/contracts/InAppMessage.ts +19 -0
- package/src/in-app/contracts/InAppScope.ts +12 -0
- package/src/in-app/contracts/index.ts +3 -0
- package/src/in-app/examples/sendReportReady.ts +14 -0
- package/src/in-app/index.ts +4 -0
- package/src/in-app/validation.ts +49 -0
- package/src/index.ts +2 -22
- package/src/logging/DevelopmentConsole.ts +154 -0
- package/src/logging/Log.ts +5 -2
- package/src/logging/PrettyLogFormatter.ts +165 -0
- package/src/logging/README.md +52 -109
- package/src/logging/acquireDevelopmentConsole.ts +19 -0
- package/src/logging/contracts/DevelopmentConsoleOptions.ts +13 -0
- package/src/logging/contracts/EmailLogOptions.ts +9 -0
- package/src/logging/contracts/HttpExchangeMonitorOptions.ts +13 -0
- package/src/logging/contracts/LogTransportOptions.ts +10 -0
- package/src/logging/contracts/LoggingOptions.ts +12 -1
- package/src/logging/contracts/index.ts +4 -0
- package/src/logging/development/README.md +88 -0
- package/src/logging/drivers/PinoLoggerDriver.ts +72 -20
- package/src/logging/examples/runDevelopmentLogs.ts +36 -0
- package/src/logging/formatLogEmail.ts +24 -0
- package/src/logging/highlightQueueMessage.ts +38 -0
- package/src/logging/httpBodyCapture.ts +393 -0
- package/src/logging/index.ts +2 -0
- package/src/logging/redactLogEmail.ts +12 -0
- package/src/logging/registerHttpExchangeMonitor.ts +45 -0
- package/src/logging/transports/EmailLogTransport.ts +46 -0
- package/src/mail/Mail.ts +5 -0
- package/src/mail/MailDeliveryError.ts +8 -0
- package/src/mail/README.md +11 -1
- package/src/mail/index.ts +1 -0
- package/src/mail/transports/MailgunTransport.ts +2 -0
- package/src/mail/transports/ResendTransport.ts +24 -22
- package/src/media/MediaManager.ts +31 -10
- package/src/media/README.md +4 -0
- package/src/media/examples/runPrivateFiles.ts +2 -2
- package/src/network/BlockedUrlError.ts +19 -0
- package/src/network/README.md +133 -0
- package/src/network/assertPublicUrl.ts +77 -0
- package/src/network/contracts/GuardedFetch.ts +40 -0
- package/src/network/contracts/PublicUrlOptions.ts +17 -0
- package/src/network/contracts/index.ts +2 -0
- package/src/network/examples/runGuardedFetch.ts +35 -0
- package/src/network/guardedFetch.ts +150 -0
- package/src/network/index.ts +6 -0
- package/src/network/playwright/GuardedBrowserProxy.ts +172 -0
- package/src/network/playwright/contracts/GuardedBrowserContextOptions.ts +43 -0
- package/src/network/playwright/contracts/index.ts +1 -0
- package/src/network/playwright/index.ts +2 -0
- package/src/network/playwright/newGuardedBrowserContext.ts +74 -0
- package/src/network/privateNetworkAddresses.ts +166 -0
- package/src/network/publicAddressLookup.ts +49 -0
- package/src/notifications/Notifications.ts +105 -0
- package/src/notifications/README.md +59 -0
- package/src/notifications/contracts/Notification.ts +65 -0
- package/src/notifications/contracts/NotificationDelivery.ts +11 -0
- package/src/notifications/contracts/index.ts +2 -0
- package/src/notifications/examples/ReportReadyNotification.ts +31 -0
- package/src/notifications/index.ts +2 -0
- package/src/queue/JobRunAttempt.ts +17 -0
- package/src/queue/JobRunEvent.ts +16 -0
- package/src/queue/JobRunStoppedError.ts +8 -0
- package/src/queue/Queue.ts +112 -11
- package/src/queue/QueueTerminalError.ts +12 -0
- package/src/queue/QueueWorker.ts +86 -7
- package/src/queue/QueueWorkerReporter.ts +59 -0
- package/src/queue/QueueableJob.ts +2 -2
- package/src/queue/README.md +142 -1
- package/src/queue/RunnableJob.ts +272 -0
- package/src/queue/console.ts +54 -21
- package/src/queue/contracts/QueueDriver.ts +9 -0
- package/src/queue/contracts/QueueJobRunner.ts +4 -0
- package/src/queue/contracts/QueuePayload.ts +1 -1
- package/src/queue/contracts/QueueSelection.ts +11 -0
- package/src/queue/contracts/QueueService.ts +19 -4
- package/src/queue/contracts/QueueWorkerLifecycle.ts +2 -2
- package/src/queue/contracts/QueueWorkerSnapshot.ts +15 -0
- package/src/queue/contracts/QueueableJob.ts +7 -3
- package/src/queue/contracts/QueueableJobContract.ts +13 -0
- package/src/queue/contracts/index.ts +6 -0
- package/src/queue/drivers/DatabaseQueueDriver.ts +25 -3
- package/src/queue/drivers/RedisQueueDriver.ts +36 -0
- package/src/queue/examples/runQueueSelection.ts +34 -0
- package/src/queue/index.ts +7 -0
- package/src/queue/jobEnvelopeHasPrefix.ts +6 -0
- package/src/scheduler/README.md +50 -9
- package/src/scheduler/ScheduledEvent.ts +20 -5
- package/src/scheduler/ScheduledJob.ts +1 -1
- package/src/scheduler/Scheduler.ts +73 -27
- package/src/scheduler/SchedulerCheckpointRecord.ts +51 -0
- package/src/scheduler/SchedulerWorker.ts +54 -19
- package/src/scheduler/console.ts +3 -1
- package/src/scheduler/contracts/Schedule.ts +22 -0
- package/src/scheduler/contracts/Scheduler.ts +21 -6
- package/src/scheduler/contracts/SchedulerCheckpoint.ts +23 -0
- package/src/scheduler/contracts/SchedulerConsole.ts +5 -0
- package/src/scheduler/contracts/SchedulerWorker.ts +6 -1
- package/src/scheduler/contracts/index.ts +2 -0
- package/src/scheduler/examples/runSchedulerWindow.ts +54 -0
- package/src/scheduler/index.ts +1 -0
- package/src/server/App.ts +73 -10
- package/src/server/README.md +95 -2
- package/src/server/contracts/HttpErrorHandlerOptions.ts +29 -0
- package/src/server/contracts/index.ts +1 -0
- package/src/server/examples/createFirstServer.ts +5 -3
- package/src/server/index.ts +4 -0
- package/src/server/publicServerErrorMessage.ts +17 -0
- package/src/server/registerBrowserJsonFormatting.ts +94 -0
- package/src/server/registerHttpErrorHandler.ts +96 -0
- package/src/ssr/README.md +6 -0
- package/src/ssr/vite/contracts/ViteSsrOptions.ts +2 -0
- package/src/ssr/vite/fastifyViteSsr.ts +4 -3
- package/src/storage/drivers/FlystorageDisk.ts +2 -2
- package/src/storage/index.ts +2 -2
- package/src/websocket/README.md +458 -0
- package/src/websocket/WebSocketChannels.ts +99 -0
- package/src/websocket/WebSocketConnection.ts +162 -0
- package/src/websocket/WebSockets.ts +157 -0
- package/src/websocket/client/WebSocketClient.ts +188 -0
- package/src/websocket/client/index.ts +3 -0
- package/src/websocket/contracts/WebSocketChannel.ts +23 -0
- package/src/websocket/contracts/WebSocketClient.ts +48 -0
- package/src/websocket/contracts/WebSocketEndpoint.ts +46 -0
- package/src/websocket/contracts/WebSocketOptions.ts +50 -0
- package/src/websocket/contracts/WebSocketPresence.ts +6 -0
- package/src/websocket/contracts/WebSocketServiceOptions.ts +12 -0
- package/src/websocket/contracts/index.ts +6 -0
- package/src/websocket/defineChannel.ts +14 -0
- package/src/websocket/defineWebSocket.ts +15 -0
- package/src/websocket/examples/board/LiveBoard.ts +97 -0
- package/src/websocket/examples/board/SummarizeBoardJob.ts +52 -0
- package/src/websocket/examples/board/boardSnapshot.ts +16 -0
- package/src/websocket/examples/board/createBoardClient.ts +43 -0
- package/src/websocket/examples/board/createBoardStore.ts +58 -0
- package/src/websocket/examples/board/registerBoardRoutes.ts +31 -0
- package/src/websocket/examples/createChannelSync.ts +66 -0
- package/src/websocket/examples/deploymentEndpoint.ts +13 -0
- package/src/websocket/examples/deploymentSnapshot.ts +36 -0
- package/src/websocket/examples/resourceChannels.ts +18 -0
- package/src/websocket/examples/userEndpoint.ts +27 -0
- package/src/websocket/fastify.ts +39 -0
- package/src/websocket/index.ts +5 -0
- package/templates/AGENTS.md +33 -2
- /package/dist/flows/{FlowValueValidator.d.ts → flowValueValidator.d.ts} +0 -0
- /package/dist/flows/{FlowValueValidator.js → flowValueValidator.js} +0 -0
- /package/dist/storage/{mime.d.ts → mimeTypeFromPath.d.ts} +0 -0
- /package/dist/storage/{mime.js → mimeTypeFromPath.js} +0 -0
- /package/dist/storage/{contents.d.ts → storageContentsToBuffer.d.ts} +0 -0
- /package/dist/storage/{contents.js → storageContentsToBuffer.js} +0 -0
- /package/src/flows/{FlowValueValidator.ts → flowValueValidator.ts} +0 -0
- /package/src/storage/{mime.ts → mimeTypeFromPath.ts} +0 -0
- /package/src/storage/{contents.ts → storageContentsToBuffer.ts} +0 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { ulid } from '@db3.ai/pure/ulid';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { ActiveRecord } from '@db3.ai/app/db';
|
|
4
|
+
import { QueueTerminalError, QueueRetryLaterError } from '@db3.ai/app/queue';
|
|
5
|
+
import { app } from '../server/appContext';
|
|
6
|
+
import { AiRateLimitBucket } from './AiRateLimitBucket';
|
|
7
|
+
import type { AIProviderAttempt } from './contracts/Providers';
|
|
8
|
+
import { isNetworkFailure } from './AIFailover';
|
|
9
|
+
import { ProviderSseObserver } from './ProviderSseObserver';
|
|
10
|
+
import { isOpenAIQuotaError, openAIProviderError } from './OpenAIProviderError';
|
|
11
|
+
|
|
12
|
+
/** Account outage policy, independent of model capacity and application billing. */
|
|
13
|
+
export interface AIProviderAdmissionOptions {
|
|
14
|
+
/** First transient cooldown; defaults to 30 seconds. */
|
|
15
|
+
initialSeconds?: number;
|
|
16
|
+
/** Maximum transient cooldown; defaults to five minutes. */
|
|
17
|
+
maxSeconds?: number;
|
|
18
|
+
/** Continuous failure episode deadline; defaults to fifteen minutes. */
|
|
19
|
+
failureWindowSeconds?: number;
|
|
20
|
+
/** Recovery ownership lifetime; must exceed the request timeout. Defaults to 90 seconds. */
|
|
21
|
+
recoveryLeaseSeconds?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Durable episode metadata stored only on the account's dedicated bucket row. */
|
|
25
|
+
interface AdmissionState {
|
|
26
|
+
generation: string;
|
|
27
|
+
failures: number;
|
|
28
|
+
deadline: number;
|
|
29
|
+
retryAt: number;
|
|
30
|
+
stopped: boolean;
|
|
31
|
+
reason: 'quota' | 'outage';
|
|
32
|
+
probe: string | null;
|
|
33
|
+
probeUntil: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Token fences a recovery completion against newer failures and expired leases. */
|
|
37
|
+
export interface AIProviderAdmissionLease {
|
|
38
|
+
bucketKey: string;
|
|
39
|
+
generation: string | null;
|
|
40
|
+
probe: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Queue backpressure that releases the job's reservation without spending a try. */
|
|
44
|
+
export class AIProviderDeferredError extends QueueRetryLaterError {
|
|
45
|
+
/** True only for the request that observed the provider failure. */
|
|
46
|
+
providerStarted = false;
|
|
47
|
+
/** Recognized rejection diagnostics for the observing request; absent for blocked work. */
|
|
48
|
+
readonly providerCode: string | null;
|
|
49
|
+
readonly diagnostics: { status: number | null; requestId: string | null };
|
|
50
|
+
/** Creates an account cooldown without retaining provider payloads. */
|
|
51
|
+
constructor(readonly retryAt: Date, readonly retryUntil: Date, providerCode: string | null = null, diagnostics: { status?: number | null; requestId?: string | null } = {}) {
|
|
52
|
+
super(Math.max(1, Math.ceil((retryAt.getTime() - Date.now()) / 1000)), 'AI provider temporarily unavailable; waiting for controlled recovery.');
|
|
53
|
+
this.providerCode = safeProviderCode(providerCode);
|
|
54
|
+
this.diagnostics = { status: safeProviderStatus(diagnostics.status), requestId: safeProviderRequestId(diagnostics.requestId) };
|
|
55
|
+
this.name = 'AIProviderDeferredError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Terminal admission failure; exhausted accounts require explicit operator recovery. */
|
|
60
|
+
export class AIProviderStoppedError extends QueueTerminalError {
|
|
61
|
+
/** Stable admission code; original provider code stays separate. */
|
|
62
|
+
readonly code = 'ai_provider_stopped';
|
|
63
|
+
/** Recognized original rejection code; never an arbitrary provider message. */
|
|
64
|
+
readonly providerCode: string | null;
|
|
65
|
+
/** Actual rejection status and provider support identifier; absent for blocked work. */
|
|
66
|
+
readonly status: number | null;
|
|
67
|
+
readonly requestId: string | null;
|
|
68
|
+
/** Distinguishes account admission expiry from an individual run stopped to prevent replay. */
|
|
69
|
+
readonly stopStage: 'quota' | 'deadline' | 'stream-output';
|
|
70
|
+
/** True only for the request that observed the provider failure. */
|
|
71
|
+
providerStarted = false;
|
|
72
|
+
/** Creates a terminal failure without provider credentials or raw error messages. */
|
|
73
|
+
constructor(readonly reason: 'quota' | 'outage', providerCode: string | null = null, diagnostics: { status?: number | null; requestId?: string | null; stopStage?: 'stream-output' } = {}) {
|
|
74
|
+
super(diagnostics.stopStage === 'stream-output' ? 'AI provider interrupted after streamed output; run stopped to prevent unsafe replay.' : reason === 'quota' ? 'AI provider credits exhausted; work stopped until operator recovery.' : 'AI provider failure deadline reached; work stopped until operator recovery.');
|
|
75
|
+
this.providerCode = safeProviderCode(providerCode);
|
|
76
|
+
this.status = safeProviderStatus(diagnostics.status);
|
|
77
|
+
this.requestId = safeProviderRequestId(diagnostics.requestId);
|
|
78
|
+
this.stopStage = diagnostics.stopStage ?? (reason === 'quota' ? 'quota' : 'deadline');
|
|
79
|
+
this.name = 'AIProviderStoppedError';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Coordinates account-wide outages using existing SQL bucket metadata.
|
|
85
|
+
* Closed accounts admit normally. Failed accounts admit one leased real request
|
|
86
|
+
* after cooldown. Exhaustion and expired episodes remain stopped across restarts;
|
|
87
|
+
* reset() is an explicit operator action and never executes or replays work.
|
|
88
|
+
*/
|
|
89
|
+
export class AIProviderAdmission {
|
|
90
|
+
/** Carries typed HTTP/SSE failures through SDKs that discard provider error codes. */
|
|
91
|
+
readonly #requestFailures = new WeakMap<AIProviderAttempt, Error>();
|
|
92
|
+
readonly #options: Required<AIProviderAdmissionOptions>;
|
|
93
|
+
/** Validates bounded outage policy; invalid values fail at boot. */
|
|
94
|
+
constructor(options: AIProviderAdmissionOptions = {}) {
|
|
95
|
+
this.#options = { initialSeconds: 30, maxSeconds: 300, failureWindowSeconds: 900, recoveryLeaseSeconds: 90, ...options };
|
|
96
|
+
if (Object.values(this.#options).some(value => !Number.isFinite(value) || value <= 0) || this.#options.initialSeconds > this.#options.maxSeconds) throw new Error('Invalid AI provider admission policy.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Admits normal work or exclusively leases a recovery request under a SQL lock.
|
|
101
|
+
* Account identity includes provider, origin and a one-way credential digest,
|
|
102
|
+
* never endpoint/model or website. Other accounts remain independent.
|
|
103
|
+
*/
|
|
104
|
+
async acquire(attempt: AIProviderAttempt, now = Date.now(), requestTimeoutMs = 0): Promise<AIProviderAdmissionLease> {
|
|
105
|
+
const bucketKey = accountKey(attempt);
|
|
106
|
+
return this.#locked(attempt, async bucket => {
|
|
107
|
+
const state = bucket.metadata?.admission as AdmissionState | undefined;
|
|
108
|
+
if (!state) return { bucketKey, generation: null, probe: null };
|
|
109
|
+
if (state.stopped || now >= state.deadline) throw new AIProviderStoppedError(state.reason);
|
|
110
|
+
const retryAt = Math.max(state.retryAt, state.probe ? state.probeUntil : 0);
|
|
111
|
+
if (now < retryAt) throw new AIProviderDeferredError(new Date(Math.min(retryAt, state.deadline)), new Date(state.deadline));
|
|
112
|
+
state.probe = randomUUID();
|
|
113
|
+
state.probeUntil = now + Math.max(this.#options.recoveryLeaseSeconds * 1000, requestTimeoutMs > 0 ? requestTimeoutMs + 30_000 : 0);
|
|
114
|
+
bucket.metadata = { admission: state };
|
|
115
|
+
await bucket.save();
|
|
116
|
+
return { bucketKey, generation: state.generation, probe: state.probe };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Records an actual provider failure before releasing external admission.
|
|
122
|
+
* Ordinary 429 capacity never trips an account outage. Late failures extend
|
|
123
|
+
* the same deadline; they cannot reopen quota or start a fresh episode.
|
|
124
|
+
*/
|
|
125
|
+
async failure(attempt: AIProviderAttempt, lease: AIProviderAdmissionLease, error: unknown, now = Date.now()): Promise<Error | null> {
|
|
126
|
+
// A caller stopping its request says nothing about provider availability.
|
|
127
|
+
// Leave an existing recovery lease bounded; cancellation is not recovery proof.
|
|
128
|
+
if (isRequestCancellation(error)) return null;
|
|
129
|
+
const details = openAIProviderError(error);
|
|
130
|
+
const quota = isOpenAIQuotaError(error);
|
|
131
|
+
const transient = !quota && isTransientProviderFailure(error);
|
|
132
|
+
// Optional work cannot declare an account outage from its own transport failure.
|
|
133
|
+
if (transient && attempt.transientFailureScope === 'request') return null;
|
|
134
|
+
if (!quota && !transient) {
|
|
135
|
+
await this.success(attempt, lease, now);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return this.#locked(attempt, async bucket => {
|
|
139
|
+
const previous = bucket.metadata?.admission as AdmissionState | undefined;
|
|
140
|
+
// A stale recovery completion cannot overwrite a newer episode/probe.
|
|
141
|
+
if (!quota && lease.probe && previous && (previous.generation !== lease.generation || previous.probe !== lease.probe)) return previous.stopped || now >= previous.deadline ? new AIProviderStoppedError(previous.reason) : new AIProviderDeferredError(new Date(Math.min(Math.max(previous.retryAt, previous.probeUntil), previous.deadline)), new Date(previous.deadline));
|
|
142
|
+
const failures = (previous?.failures ?? 0) + 1;
|
|
143
|
+
const deadline = previous?.deadline ?? now + this.#options.failureWindowSeconds * 1000;
|
|
144
|
+
const state: AdmissionState = {
|
|
145
|
+
generation: randomUUID(), failures, deadline,
|
|
146
|
+
retryAt: Math.min(deadline, now + Math.min(this.#options.maxSeconds, this.#options.initialSeconds * 2 ** Math.min(failures - 1, 20)) * 1000),
|
|
147
|
+
stopped: quota || Boolean(previous?.stopped) || now >= deadline,
|
|
148
|
+
reason: quota || previous?.reason === 'quota' ? 'quota' : 'outage', probe: null, probeUntil: 0,
|
|
149
|
+
};
|
|
150
|
+
bucket.metadata = { admission: state };
|
|
151
|
+
await bucket.save();
|
|
152
|
+
const decision = state.stopped ? new AIProviderStoppedError(state.reason, details.code, { status: details.status, requestId: details.requestId }) : new AIProviderDeferredError(new Date(state.retryAt), new Date(deadline), details.code, { status: details.status, requestId: details.requestId });
|
|
153
|
+
decision.providerStarted = true;
|
|
154
|
+
return decision;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Clears an episode only for its still-owned recovery request before deadline. */
|
|
159
|
+
async success(attempt: AIProviderAttempt, lease: AIProviderAdmissionLease, now = Date.now()): Promise<void> {
|
|
160
|
+
if (!lease.probe) return;
|
|
161
|
+
try {
|
|
162
|
+
await this.#locked(attempt, async bucket => {
|
|
163
|
+
const state = bucket.metadata?.admission as AdmissionState | undefined;
|
|
164
|
+
if (!state || state.stopped || state.generation !== lease.generation || state.probe !== lease.probe || now >= Math.min(state.deadline, state.probeUntil)) return;
|
|
165
|
+
bucket.metadata = null;
|
|
166
|
+
await bucket.save();
|
|
167
|
+
});
|
|
168
|
+
} catch {
|
|
169
|
+
// A successful paid response must survive recovery bookkeeping failure.
|
|
170
|
+
try { app().log.warn({ stage: 'provider-admission:recovery' }, 'AI recovery bookkeeping failed; recovery lease remains bounded.'); } catch { /* Logging cannot replace a paid result. */ }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Explicit operator reset after repair; does not dispatch, probe or replay jobs. */
|
|
175
|
+
async reset(attempt: AIProviderAttempt): Promise<void> {
|
|
176
|
+
await this.#locked(attempt, async bucket => { bucket.metadata = null; await bucket.save(); });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Wraps each real HTTP request, including SDK model turns and nested tools.
|
|
181
|
+
* JSON failures and streaming failures trip the same account state. Recovery
|
|
182
|
+
* is cleared only after body consumption, so header arrival is not recovery.
|
|
183
|
+
*/
|
|
184
|
+
transport(attempt: AIProviderAttempt, fetcher: typeof fetch, requestTimeoutMs = 0): typeof fetch {
|
|
185
|
+
return async (url, init) => {
|
|
186
|
+
this.#requestFailures.delete(attempt);
|
|
187
|
+
const lease = await this.acquire(attempt, Date.now(), requestTimeoutMs);
|
|
188
|
+
let response: Response;
|
|
189
|
+
try { response = await fetcher(url, init); }
|
|
190
|
+
catch (error) { throw await this.failure(attempt, lease, error) ?? error; }
|
|
191
|
+
if (!response.headers.get('content-type')?.includes('text/event-stream')) {
|
|
192
|
+
let payload: { error?: unknown; response?: { error?: unknown } } | null;
|
|
193
|
+
try { payload = await response.clone().json() as typeof payload; }
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (!(error instanceof SyntaxError)) throw await this.failure(attempt, lease, error) ?? error;
|
|
196
|
+
payload = null;
|
|
197
|
+
}
|
|
198
|
+
const error = payload?.error ?? payload?.response?.error;
|
|
199
|
+
if (!response.ok || error) {
|
|
200
|
+
const decision = await this.failure(attempt, lease, { error, status: response.status, requestId: response.headers.get('x-request-id') });
|
|
201
|
+
if (decision) this.#requestFailures.set(attempt, decision);
|
|
202
|
+
} else if (payload !== null) await this.success(attempt, lease);
|
|
203
|
+
return response;
|
|
204
|
+
}
|
|
205
|
+
if (!response.body) return response;
|
|
206
|
+
const reader = response.body.getReader();
|
|
207
|
+
const decoder = new TextDecoder();
|
|
208
|
+
const guard = this;
|
|
209
|
+
let completed = false;
|
|
210
|
+
/** Classifies completed events without changing the SDK stream or clearing failed probes. */
|
|
211
|
+
const observer = new ProviderSseObserver(async data => {
|
|
212
|
+
if (data === '[DONE]') { completed = true; return; }
|
|
213
|
+
let event;
|
|
214
|
+
try { event = JSON.parse(data); } catch { observer.invalidate(); return; }
|
|
215
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) { observer.invalidate(); return; }
|
|
216
|
+
const error = event.error ?? event.response?.error ?? (event.type === 'error' ? event : null);
|
|
217
|
+
if (error) {
|
|
218
|
+
observer.invalidate();
|
|
219
|
+
if (isOpenAIQuotaError({ error }) || isTransientProviderFailure({ error })) {
|
|
220
|
+
const decision = await guard.failure(attempt, lease, { error, status: response.status, requestId: response.headers.get('x-request-id') });
|
|
221
|
+
if (decision) guard.#requestFailures.set(attempt, decision);
|
|
222
|
+
}
|
|
223
|
+
} else if (event.type === 'response.completed') {
|
|
224
|
+
if (event.response?.status === 'completed') completed = true;
|
|
225
|
+
else observer.invalidate();
|
|
226
|
+
}
|
|
227
|
+
else if (event.type === 'response.failed' || event.type === 'response.incomplete') observer.invalidate();
|
|
228
|
+
});
|
|
229
|
+
/** Treats undecodable UTF-8 conservatively while still observing subsequent data. */
|
|
230
|
+
const observe = async (text: string): Promise<void> => {
|
|
231
|
+
if (text.includes('\uFFFD')) observer.invalidate();
|
|
232
|
+
await observer.push(text);
|
|
233
|
+
};
|
|
234
|
+
const body = new ReadableStream<Uint8Array>({
|
|
235
|
+
/** Observes bounded complete SSE events while forwarding each original chunk. */
|
|
236
|
+
async pull(controller) {
|
|
237
|
+
try {
|
|
238
|
+
const item = await reader.read();
|
|
239
|
+
if (item.done) {
|
|
240
|
+
await observe(decoder.decode());
|
|
241
|
+
if (response.ok && completed && observer.complete) await guard.success(attempt, lease);
|
|
242
|
+
controller.close();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Bound temporary decoded fragments independently of upstream chunk size.
|
|
246
|
+
for (let offset = 0; offset < item.value.length; offset += 16_384) await observe(decoder.decode(item.value.subarray(offset, offset + 16_384), { stream: true }));
|
|
247
|
+
controller.enqueue(item.value);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
await reader.cancel().catch(() => {});
|
|
250
|
+
const decision = error instanceof AIProviderDeferredError || error instanceof AIProviderStoppedError ? error : await guard.failure(attempt, lease, error);
|
|
251
|
+
if (decision instanceof AIProviderDeferredError && !decision.diagnostics.requestId) decision.diagnostics.requestId = safeProviderRequestId(response.headers.get('x-request-id'));
|
|
252
|
+
controller.error(decision ?? error);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
/** Cancellation retains the bounded recovery lease rather than asserting success. */
|
|
256
|
+
async cancel(reason) { await reader.cancel(reason); },
|
|
257
|
+
});
|
|
258
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Returns the shared cooldown for a safe SDK retry of the current model request.
|
|
264
|
+
* The SDK vetoes emitted output and cancellation before consulting its policy.
|
|
265
|
+
* Retains the typed failure until transport actually starts the next attempt,
|
|
266
|
+
* because its stateful-request veto runs after policy evaluation. Every new
|
|
267
|
+
* attempt reacquires admission; terminal and cancellation decisions take priority.
|
|
268
|
+
*/
|
|
269
|
+
requestRetryDelay(attempt: AIProviderAttempt, fallback: unknown, now = Date.now()): number | null {
|
|
270
|
+
const failure = fallback instanceof AIProviderDeferredError || fallback instanceof AIProviderStoppedError ? fallback : this.#requestFailures.get(attempt) ?? fallback;
|
|
271
|
+
if (!(failure instanceof AIProviderDeferredError) || now >= failure.retryUntil.getTime()) return null;
|
|
272
|
+
return Math.max(1, Math.min(failure.retryAt.getTime(), failure.retryUntil.getTime()) - now);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Restores a typed HTTP/SSE admission failure after the SDK consumes its original event. */
|
|
276
|
+
requestFailure(attempt: AIProviderAttempt, fallback: unknown): unknown {
|
|
277
|
+
const failure = this.#requestFailures.get(attempt);
|
|
278
|
+
this.#requestFailures.delete(attempt);
|
|
279
|
+
return fallback instanceof AIProviderDeferredError || fallback instanceof AIProviderStoppedError || isRequestCancellation(fallback) ? fallback : failure ?? fallback;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Creates and locks the dedicated account row in an autocommit transaction. */
|
|
283
|
+
async #locked<T>(attempt: AIProviderAttempt, operation: (bucket: AiRateLimitBucket) => Promise<T>): Promise<T> {
|
|
284
|
+
const bucketKey = accountKey(attempt);
|
|
285
|
+
const db = app().db.knex;
|
|
286
|
+
// Use the service connection: caller transactions must not hide the trip from workers.
|
|
287
|
+
await db(AiRateLimitBucket.table).insert({ id: ulid(), bucket_key: bucketKey, provider: attempt.provider, endpoint: 'responses', limit_key: 'account-admission', created_at: new Date(), updated_at: new Date() }).onConflict('bucket_key').ignore();
|
|
288
|
+
return db.transaction(transaction => ActiveRecord.withDb(transaction, async () => {
|
|
289
|
+
const bucket = await AiRateLimitBucket.where('bucketKey', bucketKey).toKnex().forUpdate().first();
|
|
290
|
+
if (!bucket) throw new Error('AI provider admission row unavailable.');
|
|
291
|
+
const model = await AiRateLimitBucket.findOrFail(bucket.id);
|
|
292
|
+
return operation(model);
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Opaque credential identity; never persisted or logged in reversible form. */
|
|
298
|
+
function accountKey(attempt: AIProviderAttempt): string {
|
|
299
|
+
const digest = createHash('sha256').update(`${attempt.provider}\n${new URL(attempt.baseUrl).href.replace(/\/+$/g, '')}\n${attempt.apiKey}`).digest('hex');
|
|
300
|
+
return `${attempt.provider}:account:${digest}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Classifies only transport availability failures, without clearing recovery state. */
|
|
304
|
+
function isTransientProviderFailure(error: unknown): boolean {
|
|
305
|
+
const details = openAIProviderError(error);
|
|
306
|
+
return isNetworkFailure(error) || details.status === 408 || (details.status !== null && details.status >= 500) || ['server_error', 'service_unavailable', 'overloaded', 'overloaded_error'].includes(details.code ?? '');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Recognizes explicit caller cancellation through bounded SDK cause wrappers, excluding timeouts. */
|
|
310
|
+
function isRequestCancellation(error: unknown, depth = 0): boolean {
|
|
311
|
+
if (!(error instanceof Error) || depth > 4) return false;
|
|
312
|
+
if (error.name === 'AbortError' || error.name === 'APIUserAbortError') return true;
|
|
313
|
+
return isRequestCancellation(error.cause, depth + 1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Retains only recognized rejection codes, never arbitrary provider-supplied text. */
|
|
317
|
+
function safeProviderCode(value: string | null): string | null {
|
|
318
|
+
return value !== null && ['insufficient_quota', 'billing_hard_limit_reached', 'credit_balance_exhausted', 'server_error', 'service_unavailable', 'overloaded', 'overloaded_error', 'ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN'].includes(value) ? value : null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Reports rejection HTTP status; a successful SSE handshake is not a rejection status. */
|
|
322
|
+
function safeProviderStatus(value: unknown): number | null {
|
|
323
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 400 && value <= 599 ? value : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Bounds opaque support identifiers and rejects whitespace, headers and arbitrary messages. */
|
|
327
|
+
function safeProviderRequestId(value: unknown): string | null {
|
|
328
|
+
return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value) ? value : null;
|
|
329
|
+
}
|
package/src/ai/AIRateLimiter.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
1
2
|
import type { AIRateLimitAcquireInput, AIRateLimitLease, AIRateLimitSnapshot, AIProviderResponseMetadata } from './contracts/RateLimits';
|
|
2
3
|
export type { AIRateLimitAcquireInput, AIRateLimitLease, AIRateLimitSnapshot, AIProviderResponseMetadata } from './contracts/RateLimits';
|
|
3
4
|
import { QueueRetryLaterError } from '@db3.ai/app/queue';
|
|
@@ -9,6 +10,8 @@ import { AiRateLimitReservation } from './AiRateLimitReservation';
|
|
|
9
10
|
import { AI_PROVIDER, type AIProvider } from './contracts/AI.js';
|
|
10
11
|
|
|
11
12
|
const DEFAULT_RESERVATION_TTL_MS = 10 * 60 * 1000;
|
|
13
|
+
const BOOKKEEPING_WRITE_ATTEMPTS = 3;
|
|
14
|
+
const BOOKKEEPING_RETRY_DELAY_MS = 25;
|
|
12
15
|
const UNKNOWN_ACTIVE_BUCKET_DELAY_SECONDS = 1;
|
|
13
16
|
const UNKNOWN_RESET_DELAY_SECONDS = 60;
|
|
14
17
|
const ONE_MINUTE_MS = 60 * 1000;
|
|
@@ -58,6 +61,8 @@ interface ProviderDefaultRateLimit {
|
|
|
58
61
|
* Coordinates OpenAI provider calls against the latest observed rate-limit state.
|
|
59
62
|
*/
|
|
60
63
|
export class AIRateLimiter {
|
|
64
|
+
/** Failed caller transactions must surface their first error without follow-up writes. */
|
|
65
|
+
#transactionFailures = new WeakMap<object, unknown>();
|
|
61
66
|
/**
|
|
62
67
|
* Reserves capacity for one provider request.
|
|
63
68
|
*
|
|
@@ -120,7 +125,11 @@ export class AIRateLimiter {
|
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
/**
|
|
123
|
-
*
|
|
128
|
+
* Observes provider headers and releases a lease after provider execution.
|
|
129
|
+
*
|
|
130
|
+
* Autocommit bookkeeping failures preserve the provider outcome without replaying
|
|
131
|
+
* paid work. Caller-owned transaction failures propagate so their owner can
|
|
132
|
+
* handle rollback; this limiter never writes again in that failed transaction.
|
|
124
133
|
*
|
|
125
134
|
* @param lease - Reservation returned from acquire().
|
|
126
135
|
* @param metadata - Parsed response header metadata.
|
|
@@ -130,6 +139,7 @@ export class AIRateLimiter {
|
|
|
130
139
|
metadata: AIProviderResponseMetadata,
|
|
131
140
|
): Promise<void> {
|
|
132
141
|
if (!lease?.bucket) return;
|
|
142
|
+
const bucket = lease.bucket;
|
|
133
143
|
|
|
134
144
|
try {
|
|
135
145
|
const defaultRateLimit = providerDefaultRateLimit(
|
|
@@ -154,7 +164,7 @@ export class AIRateLimiter {
|
|
|
154
164
|
: false;
|
|
155
165
|
|
|
156
166
|
if (shouldConsumeDefault || shouldStoreMetadata) {
|
|
157
|
-
await lease
|
|
167
|
+
await this.#persistBookkeeping(lease, 'bucket:observe', () => bucket.save());
|
|
158
168
|
}
|
|
159
169
|
} finally {
|
|
160
170
|
await this.release(lease);
|
|
@@ -162,25 +172,124 @@ export class AIRateLimiter {
|
|
|
162
172
|
}
|
|
163
173
|
|
|
164
174
|
/**
|
|
165
|
-
* Releases a lease
|
|
175
|
+
* Releases a lease with best-effort autocommit persistence after provider work.
|
|
176
|
+
*
|
|
177
|
+
* The conditional write is authoritative: an already released or deleted
|
|
178
|
+
* row is settled too. In-memory release state changes only after success,
|
|
179
|
+
* so a failed write remains retryable even when the same lease is reused.
|
|
166
180
|
*
|
|
167
181
|
* @param lease - Reservation returned from acquire().
|
|
168
182
|
*/
|
|
169
183
|
async release(lease: AIRateLimitLease | null): Promise<void> {
|
|
170
184
|
if (!lease?.reservation?.id) return;
|
|
185
|
+
const reservation = lease.reservation;
|
|
186
|
+
const reservationId = reservation.id;
|
|
171
187
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
const releasedAt = reservation.releasedAt ?? new Date();
|
|
189
|
+
const persisted = await this.#persistBookkeeping(lease, 'reservation:release', () => AiRateLimitReservation
|
|
190
|
+
.where('id', reservationId)
|
|
191
|
+
.whereNull('releasedAt')
|
|
192
|
+
.patch({ releasedAt }));
|
|
176
193
|
|
|
177
|
-
|
|
178
|
-
}
|
|
194
|
+
if (!persisted) return;
|
|
195
|
+
reservation.assign({ releasedAt });
|
|
179
196
|
|
|
180
197
|
try {
|
|
181
198
|
await AiRateLimitReservation.cleanupReleasedAndExpired();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
this.#logBookkeepingFailure(lease, 'reservation:cleanup', error, 1, false);
|
|
201
|
+
this.#propagateTransactionFailure(error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Retries only recognized lock failures in idempotent bookkeeping writes.
|
|
207
|
+
*
|
|
208
|
+
* An ambient transaction may already have been rolled back by a deadlock,
|
|
209
|
+
* so failures propagate to its owner after one attempt. Only autocommit writes
|
|
210
|
+
* are retried here; the attempt budget does not shorten SQL lock timeouts,
|
|
211
|
+
* and no provider execution is included in the retry scope.
|
|
212
|
+
*
|
|
213
|
+
* @param lease - Safe identities associated with this bookkeeping step.
|
|
214
|
+
* @param stage - Bucket observation or reservation release.
|
|
215
|
+
* @param write - The exact idempotent database write to repeat.
|
|
216
|
+
* @returns Whether the write succeeded within its bounded attempt budget.
|
|
217
|
+
*/
|
|
218
|
+
async #persistBookkeeping(
|
|
219
|
+
lease: AIRateLimitLease,
|
|
220
|
+
stage: 'bucket:observe' | 'reservation:release',
|
|
221
|
+
write: () => Promise<unknown>,
|
|
222
|
+
): Promise<boolean> {
|
|
223
|
+
const database = ActiveRecord.getDb();
|
|
224
|
+
if (this.#transactionFailures.has(database)) throw this.#transactionFailures.get(database);
|
|
225
|
+
const maxAttempts = database.isTransaction ? 1 : BOOKKEEPING_WRITE_ATTEMPTS;
|
|
226
|
+
let lastError: unknown;
|
|
227
|
+
|
|
228
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
229
|
+
try {
|
|
230
|
+
await write();
|
|
231
|
+
if (attempt > 1) this.#logBookkeepingFailure(lease, stage, lastError, attempt, true);
|
|
232
|
+
return true;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
lastError = error;
|
|
235
|
+
if (!bookkeepingLockCode(error) || attempt === maxAttempts) {
|
|
236
|
+
this.#logBookkeepingFailure(lease, stage, error, attempt, false);
|
|
237
|
+
this.#propagateTransactionFailure(error);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
await delay(BOOKKEEPING_RETRY_DELAY_MS * attempt);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Notifies a caller transaction's owner and blocks subsequent limiter writes.
|
|
249
|
+
*
|
|
250
|
+
* InnoDB deadlocks roll back the entire transaction before Knex knows it has
|
|
251
|
+
* ended. Even statement-only failures belong to the transaction owner; no
|
|
252
|
+
* implicit continuation or paid provider retry is safe at this boundary.
|
|
253
|
+
*
|
|
254
|
+
* @param error - Original persistence failure to propagate unchanged.
|
|
255
|
+
*/
|
|
256
|
+
#propagateTransactionFailure(error: unknown): void {
|
|
257
|
+
const database = ActiveRecord.getDb();
|
|
258
|
+
if (!database.isTransaction) return;
|
|
259
|
+
this.#transactionFailures.set(database, error);
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Emits bounded identifiers and allowlisted lock codes without SQL payloads.
|
|
265
|
+
*
|
|
266
|
+
* @param lease - Coordination identities; prompts and responses are excluded.
|
|
267
|
+
* @param stage - The failed bookkeeping step.
|
|
268
|
+
* @param error - SQL failure whose message and bindings must be discarded.
|
|
269
|
+
* @param attempts - Number of database writes attempted.
|
|
270
|
+
* @param recovered - Whether a later write succeeded.
|
|
271
|
+
*/
|
|
272
|
+
#logBookkeepingFailure(
|
|
273
|
+
lease: AIRateLimitLease,
|
|
274
|
+
stage: 'bucket:observe' | 'reservation:release' | 'reservation:cleanup',
|
|
275
|
+
error: unknown,
|
|
276
|
+
attempts: number,
|
|
277
|
+
recovered: boolean,
|
|
278
|
+
): void {
|
|
279
|
+
try {
|
|
280
|
+
app().log.warn({
|
|
281
|
+
stage,
|
|
282
|
+
code: bookkeepingLockCode(error),
|
|
283
|
+
attempts,
|
|
284
|
+
recovered,
|
|
285
|
+
transactionFailed: Boolean(ActiveRecord.getDb().isTransaction),
|
|
286
|
+
bucketId: lease.bucket?.id ?? null,
|
|
287
|
+
reservationId: lease.reservation?.id ?? null,
|
|
288
|
+
aiRequestId: lease.reservation?.aiRequest?.id ?? null,
|
|
289
|
+
expiresAt: lease.reservation?.expiresAt?.toISOString() ?? null,
|
|
290
|
+
}, 'AI rate-limit bookkeeping write failed.');
|
|
182
291
|
} catch {
|
|
183
|
-
//
|
|
292
|
+
// Logging transport failures must not replace a completed provider outcome.
|
|
184
293
|
}
|
|
185
294
|
}
|
|
186
295
|
|
|
@@ -438,6 +547,10 @@ function providerDefaultRateLimit(
|
|
|
438
547
|
endpoint: AiRateLimitEndpoint,
|
|
439
548
|
model: string,
|
|
440
549
|
): ProviderDefaultRateLimit | null {
|
|
550
|
+
if (provider === AI_PROVIDER.openai && endpoint === AI_RATE_LIMIT_ENDPOINT.images && (model === 'gpt-image-2.5-flare' || model.startsWith('gpt-image-2.5-flare-'))) {
|
|
551
|
+
// Conservative local admission policy, not a claim about the account's provider tier.
|
|
552
|
+
return { limitRequests: 5, limitTokens: null, windowMs: ONE_MINUTE_MS, source: 'local:gpt-image-2.5-flare:conservative' };
|
|
553
|
+
}
|
|
441
554
|
if (
|
|
442
555
|
provider === AI_PROVIDER.openai
|
|
443
556
|
&& endpoint === AI_RATE_LIMIT_ENDPOINT.images
|
|
@@ -878,3 +991,14 @@ function numberOrZero(value: string | number | null | undefined): number {
|
|
|
878
991
|
|
|
879
992
|
return 0;
|
|
880
993
|
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Extracts only database lock codes that are safe to publish in operator logs.
|
|
997
|
+
*
|
|
998
|
+
* @param error - Database exception whose message and SQL are never exposed.
|
|
999
|
+
* @returns Recognized deadlock or lock-timeout code, otherwise null.
|
|
1000
|
+
*/
|
|
1001
|
+
function bookkeepingLockCode(error: unknown): 'ER_LOCK_DEADLOCK' | 'ER_LOCK_WAIT_TIMEOUT' | null {
|
|
1002
|
+
if (!error || typeof error !== 'object' || !('code' in error)) return null;
|
|
1003
|
+
return error.code === 'ER_LOCK_DEADLOCK' || error.code === 'ER_LOCK_WAIT_TIMEOUT' ? error.code : null;
|
|
1004
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** SQL codes safe to include in an operator alert without SQL bindings. */
|
|
2
|
+
type AIRequestTrackingSqlCode = 'ER_LOCK_DEADLOCK' | 'ER_LOCK_WAIT_TIMEOUT';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reports that a pending AI request could not be saved before provider work.
|
|
6
|
+
*
|
|
7
|
+
* Database exceptions can contain SQL-bound prompts. This error deliberately
|
|
8
|
+
* omits the original exception and exposes only an allowlisted code and stage.
|
|
9
|
+
*/
|
|
10
|
+
export class AIRequestTrackingError extends Error {
|
|
11
|
+
readonly name = 'AIRequestTrackingError';
|
|
12
|
+
/** The pending tracking write failed before any provider request was sent. */
|
|
13
|
+
readonly stage = 'tracking:pending-request';
|
|
14
|
+
/** Allowlisted database lock code, or null for another persistence failure. */
|
|
15
|
+
readonly code: AIRequestTrackingSqlCode | null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a payload-safe tracking failure from a database exception.
|
|
19
|
+
*
|
|
20
|
+
* @param error - Original persistence error; its message and SQL are discarded.
|
|
21
|
+
*/
|
|
22
|
+
constructor(error: unknown) {
|
|
23
|
+
super('Unable to persist the pending AI request before provider execution.');
|
|
24
|
+
this.code = trackingSqlCode(error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns only database lock codes that are safe for structured diagnostics.
|
|
30
|
+
*
|
|
31
|
+
* @param error - Original persistence error, whose message is ignored.
|
|
32
|
+
* @returns An allowlisted lock code or null.
|
|
33
|
+
*/
|
|
34
|
+
function trackingSqlCode(error: unknown): AIRequestTrackingSqlCode | null {
|
|
35
|
+
if (!error || typeof error !== 'object' || !('code' in error)) return null;
|
|
36
|
+
return error.code === 'ER_LOCK_DEADLOCK' || error.code === 'ER_LOCK_WAIT_TIMEOUT' ? error.code : null;
|
|
37
|
+
}
|