Merge claudedo/dc5d5776339b4da6a5fb628a5c2ed648
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Data;
|
||||
@@ -29,6 +30,16 @@ public static class TaskNumberAllocator
|
||||
RETURNING *
|
||||
""", AppSettingsEntity.SingletonId).AsNoTracking().ToListAsync(ct);
|
||||
|
||||
if (settings.Count == 0)
|
||||
{
|
||||
// The singleton row is missing (e.g. a corrupted/hand-edited DB) -- recreate it,
|
||||
// as its own separate insert/commit, and retry the UPDATE above rather than
|
||||
// indexing into an empty result. Doesn't count against MaxAttempts: it's not a
|
||||
// number collision.
|
||||
await EnsureSingletonRowAsync(context, ct);
|
||||
continue;
|
||||
}
|
||||
|
||||
entity.Number = settings[0].NextTaskNumber - 1;
|
||||
context.Tasks.Add(entity);
|
||||
|
||||
@@ -37,10 +48,40 @@ public static class TaskNumberAllocator
|
||||
await context.SaveChangesAsync(ct);
|
||||
return;
|
||||
}
|
||||
catch (DbUpdateException) when (attempt < MaxAttempts)
|
||||
catch (DbUpdateException ex)
|
||||
{
|
||||
context.Entry(entity).State = EntityState.Detached;
|
||||
if (attempt < MaxAttempts && IsNumberCollision(ex))
|
||||
continue;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task EnsureSingletonRowAsync(ClaudeDoDbContext context, CancellationToken ct)
|
||||
{
|
||||
var row = new AppSettingsEntity { Id = AppSettingsEntity.SingletonId };
|
||||
context.AppSettings.Add(row);
|
||||
try
|
||||
{
|
||||
await context.SaveChangesAsync(ct);
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
// A concurrent process already inserted the singleton -- fine, the next UPDATE
|
||||
// attempt above will find it.
|
||||
}
|
||||
finally
|
||||
{
|
||||
context.Entry(row).State = EntityState.Detached;
|
||||
}
|
||||
}
|
||||
|
||||
// Distinguishes the expected unique-index collision on idx_tasks_number (worth retrying with
|
||||
// a fresh number) from any other DbUpdateException -- an FK violation, a NOT NULL violation,
|
||||
// etc. -- which must surface immediately instead of silently burning up to MaxAttempts numbers
|
||||
// on a failure a retry can never fix.
|
||||
private static bool IsNumberCollision(DbUpdateException ex) =>
|
||||
ex.InnerException is SqliteException { SqliteErrorCode: 19 } sqliteEx
|
||||
&& sqliteEx.Message.Contains("tasks.number", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
+13
-1
@@ -109,7 +109,19 @@ public sealed class TaskWaitMcpTools
|
||||
var tasks = new TaskRepository(ctx);
|
||||
var resolved = new string[ids.Length];
|
||||
for (var i = 0; i < ids.Length; i++)
|
||||
resolved[i] = await TaskIdResolver.ResolveAsync(tasks, ids[i], ct);
|
||||
{
|
||||
try
|
||||
{
|
||||
resolved[i] = await TaskIdResolver.ResolveAsync(tasks, ids[i], ct);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// A #<number>/bare-number id with no matching task -- keep the original,
|
||||
// unresolvable id so CheckOnceAsync's lookup misses and reports "NotFound",
|
||||
// per this tool's documented contract, instead of failing the whole call.
|
||||
resolved[i] = ids[i];
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user