From 265f2afa5044e97d9e661fe02bbd80a53c744810 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Mon, 7 Sep 2026 20:09:18 +0000 Subject: [PATCH] Fix character enrichment claim isolation --- PlotLine.Tests/Program.cs | 3 ++ ...e27Q_CharacterEnrichmentClaimIsolation.sql | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 PlotLine/Sql/190_Phase27Q_CharacterEnrichmentClaimIsolation.sql diff --git a/PlotLine.Tests/Program.cs b/PlotLine.Tests/Program.cs index 4ca6e6d..80ff724 100644 --- a/PlotLine.Tests/Program.cs +++ b/PlotLine.Tests/Program.cs @@ -1278,6 +1278,7 @@ static void StoryIntelligenceCharacterEnrichmentPromotesStructuredFacts() var models = File.ReadAllText(Path.Combine(root, "Models/CharacterEnrichmentModels.cs")); var service = File.ReadAllText(Path.Combine(root, "Services/CharacterEnrichmentService.cs")); var migration = File.ReadAllText(Path.Combine(root, "Sql/188_Phase27O_CharacterEnrichmentStructuredFacts.sql")); + var claimIsolationMigration = File.ReadAllText(Path.Combine(root, "Sql/190_Phase27Q_CharacterEnrichmentClaimIsolation.sql")); Assert(models.Contains("CharacterEnrichmentFactResult Sex", StringComparison.Ordinal), "Character enrichment should extract sex as an evidenced structured fact."); Assert(service.Contains("Do not place ascertainable facts only in the summary", StringComparison.Ordinal), "Prompt should require structured facts as well as summary prose."); @@ -1292,6 +1293,8 @@ static void StoryIntelligenceCharacterEnrichmentPromotesStructuredFacts() && migration.Contains("INNER JOIN @Results", StringComparison.Ordinal), "SQL completion should replace same-run evidence idempotently."); Assert(migration.Contains("BEGIN TRANSACTION", StringComparison.Ordinal) && migration.Contains("COMMIT TRANSACTION", StringComparison.Ordinal), "Character enrichment claim and completion should be transactional."); + Assert(claimIsolationMigration.Contains("CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_ClaimNext", StringComparison.Ordinal) + && !claimIsolationMigration.Contains("READPAST", StringComparison.OrdinalIgnoreCase), "Character enrichment claim locking should avoid READPAST isolation failures."); } static void CharacterDetailCanQueueSingleCharacterEnrichment() diff --git a/PlotLine/Sql/190_Phase27Q_CharacterEnrichmentClaimIsolation.sql b/PlotLine/Sql/190_Phase27Q_CharacterEnrichmentClaimIsolation.sql new file mode 100644 index 0000000..6e1fdfb --- /dev/null +++ b/PlotLine/Sql/190_Phase27Q_CharacterEnrichmentClaimIsolation.sql @@ -0,0 +1,53 @@ +CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_ClaimNext + @LeaseMinutes int = 90 +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + DECLARE @RunID int; + + BEGIN TRANSACTION; + + SELECT TOP (1) @RunID = CharacterEnrichmentRunID + FROM dbo.CharacterEnrichmentRuns WITH (UPDLOCK, HOLDLOCK) + WHERE IsCurrent = 1 + AND ( + Status = N'Queued' + OR (Status = N'Running' AND StartedUtc < DATEADD(minute, -ABS(@LeaseMinutes), SYSUTCDATETIME())) + ) + ORDER BY QueuedUtc, CharacterEnrichmentRunID; + + IF @RunID IS NULL + BEGIN + COMMIT TRANSACTION; + SELECT TOP (0) * FROM dbo.CharacterEnrichmentRuns; + RETURN; + END; + + UPDATE dbo.CharacterEnrichmentRuns + SET Status = N'Running', + StartedUtc = SYSUTCDATETIME(), + AttemptCount = AttemptCount + 1, + CurrentStage = N'Preparing character analysis', + CurrentMessage = N'Preparing character analysis...', + UpdatedUtc = SYSUTCDATETIME() + WHERE CharacterEnrichmentRunID = @RunID + AND IsCurrent = 1 + AND ( + Status = N'Queued' + OR (Status = N'Running' AND StartedUtc < DATEADD(minute, -ABS(@LeaseMinutes), SYSUTCDATETIME())) + ); + + IF @@ROWCOUNT = 0 + BEGIN + COMMIT TRANSACTION; + SELECT TOP (0) * FROM dbo.CharacterEnrichmentRuns; + RETURN; + END; + + SELECT * FROM dbo.CharacterEnrichmentRuns WHERE CharacterEnrichmentRunID = @RunID; + + COMMIT TRANSACTION; +END; +GO