CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceRun_ClaimNextPendingFair @MaxConcurrentRunsPerBook int = 2, @MaxConcurrentRunsPerUser int = 3, @ClaimLeaseMinutes int = 90 AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @Now datetime2(7) = SYSUTCDATETIME(); DECLARE @SafeMaxPerBook int = CASE WHEN ISNULL(@MaxConcurrentRunsPerBook, 0) < 1 THEN 1 ELSE @MaxConcurrentRunsPerBook END; DECLARE @SafeMaxPerUser int = CASE WHEN ISNULL(@MaxConcurrentRunsPerUser, 0) < 1 THEN 1 ELSE @MaxConcurrentRunsPerUser END; DECLARE @SafeClaimLeaseMinutes int = CASE WHEN ISNULL(@ClaimLeaseMinutes, 0) < 15 THEN 15 ELSE @ClaimLeaseMinutes END; DECLARE @StoryIntelligenceRunID int; BEGIN TRANSACTION; UPDATE dbo.StoryIntelligenceRuns SET Status = N'Pending', CurrentStage = N'Pending', CurrentMessage = N'Returned to the Story Intelligence queue after an expired worker claim.', StartedUtc = NULL, UpdatedUtc = @Now WHERE Status = N'Running' AND CancellationRequestedUtc IS NULL AND UpdatedUtc < DATEADD(minute, -@SafeClaimLeaseMinutes, @Now); ;WITH PendingRuns AS ( SELECT pending.StoryIntelligenceRunID, pending.CreatedUtc, pending.UserID, pending.BookID, ActiveForBook = ( SELECT COUNT_BIG(1) FROM dbo.StoryIntelligenceRuns activeBook WHERE activeBook.Status = N'Running' AND activeBook.BookID = pending.BookID ), ActiveForUser = ( SELECT COUNT_BIG(1) FROM dbo.StoryIntelligenceRuns activeUser WHERE activeUser.Status = N'Running' AND activeUser.UserID = pending.UserID ) FROM dbo.StoryIntelligenceRuns pending WITH (UPDLOCK, READPAST) WHERE pending.Status = N'Pending' AND ( pending.BookID IS NULL OR ( SELECT COUNT_BIG(1) FROM dbo.StoryIntelligenceRuns activeBook WHERE activeBook.Status = N'Running' AND activeBook.BookID = pending.BookID ) < @SafeMaxPerBook ) AND ( SELECT COUNT_BIG(1) FROM dbo.StoryIntelligenceRuns activeUser WHERE activeUser.Status = N'Running' AND activeUser.UserID = pending.UserID ) < @SafeMaxPerUser ) SELECT TOP (1) @StoryIntelligenceRunID = StoryIntelligenceRunID FROM PendingRuns ORDER BY ActiveForBook, ActiveForUser, CreatedUtc, StoryIntelligenceRunID; IF @StoryIntelligenceRunID IS NOT NULL BEGIN UPDATE dbo.StoryIntelligenceRuns SET Status = N'Running', StartedUtc = @Now, CurrentStage = N'ChapterStructure', CurrentMessage = N'Running Chapter Structure analysis.', UpdatedUtc = @Now WHERE StoryIntelligenceRunID = @StoryIntelligenceRunID AND Status = N'Pending'; END; COMMIT TRANSACTION; IF @StoryIntelligenceRunID IS NULL BEGIN SELECT TOP (0) StoryIntelligenceRunID, UserID, ProjectID, BookID, ChapterID, ChapterNumber, Status, SourceType, SourceLabel, SourceText, SourceWordCount, SourceCharacterCount, SourceParagraphCount, SourceChapterCount, PromptVersion, PromptVersionsSummary, KnownCharactersJson, Model, StartedUtc, CompletedUtc, FailureStage, TotalInputTokens, TotalOutputTokens, TotalTokens, TotalDurationMs, EstimatedCostGBP, EstimatedCostUSD, ErrorMessage, ErrorDetail, CurrentStage, CurrentMessage, TotalDetectedScenes, CompletedScenes, FailedScenes, CancellationRequestedUtc, CancelledUtc, CreatedUtc, UpdatedUtc FROM dbo.StoryIntelligenceRuns; RETURN; END; SELECT StoryIntelligenceRunID, UserID, ProjectID, BookID, ChapterID, ChapterNumber, Status, SourceType, SourceLabel, SourceText, SourceWordCount, SourceCharacterCount, SourceParagraphCount, SourceChapterCount, PromptVersion, PromptVersionsSummary, KnownCharactersJson, Model, StartedUtc, CompletedUtc, FailureStage, TotalInputTokens, TotalOutputTokens, TotalTokens, TotalDurationMs, EstimatedCostGBP, EstimatedCostUSD, ErrorMessage, ErrorDetail, CurrentStage, CurrentMessage, TotalDetectedScenes, CompletedScenes, FailedScenes, CancellationRequestedUtc, CancelledUtc, CreatedUtc, UpdatedUtc FROM dbo.StoryIntelligenceRuns WHERE StoryIntelligenceRunID = @StoryIntelligenceRunID; END; GO CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceRun_ListActiveBookSummaryForUser @UserID int AS BEGIN SET NOCOUNT ON; SELECT r.UserID, r.ProjectID, p.ProjectName AS ProjectTitle, r.BookID, b.BookTitle, b.Subtitle AS BookSubtitle, COUNT(1) AS RunCount, SUM(CASE WHEN r.Status = N'Pending' THEN 1 ELSE 0 END) AS PendingRunCount, SUM(CASE WHEN r.Status = N'Running' THEN 1 ELSE 0 END) AS RunningRunCount, SUM(CASE WHEN r.Status IN (N'Completed', N'CompletedWithWarnings') THEN 1 ELSE 0 END) AS CompletedRunCount, SUM(CASE WHEN r.Status IN (N'Failed', N'Cancelled') THEN 1 ELSE 0 END) AS FailedRunCount, SUM(ISNULL(r.TotalDetectedScenes, 0)) AS TotalDetectedScenes, SUM(ISNULL(r.CompletedScenes, 0)) AS CompletedScenes, SUM(ISNULL(r.FailedScenes, 0)) AS FailedScenes, SUM(ISNULL(r.TotalDurationMs, 0)) AS TotalDurationMs, MIN(r.CreatedUtc) AS FirstCreatedUtc, MAX(r.UpdatedUtc) AS UpdatedUtc FROM dbo.StoryIntelligenceRuns r LEFT JOIN dbo.Projects p ON p.ProjectID = r.ProjectID LEFT JOIN dbo.Books b ON b.BookID = r.BookID WHERE r.UserID = @UserID AND r.Status IN (N'Pending', N'Running') GROUP BY r.UserID, r.ProjectID, p.ProjectName, r.BookID, b.BookTitle, b.Subtitle ORDER BY MAX(r.UpdatedUtc) DESC, MIN(r.CreatedUtc); END; GO IF NOT EXISTS ( SELECT 1 FROM sys.indexes WHERE name = N'IX_StoryIntelligenceRuns_QueueFairness' AND object_id = OBJECT_ID(N'dbo.StoryIntelligenceRuns') ) BEGIN CREATE INDEX IX_StoryIntelligenceRuns_QueueFairness ON dbo.StoryIntelligenceRuns (Status, UserID, BookID, CreatedUtc, StoryIntelligenceRunID) INCLUDE (ProjectID, ChapterID, ChapterNumber, UpdatedUtc, TotalDetectedScenes, CompletedScenes, FailedScenes, TotalDurationMs); END; GO