From 4211c20b31270932128c91da21aaf1af9b086faa Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Tue, 31 Mar 2026 16:32:01 -0700 Subject: [PATCH] fix(KB-338): wrap allocateId in configLock to prevent race condition - Add configLock mutex protection around allocateId to prevent concurrent ID allocation race condition - Create changeset documenting the race condition fix - Update line counts in existing changeset files --- .../fix-id-allocation-race-condition.md | 5 ++++ packages/core/src/store.ts | 30 ++++++++++--------- 2 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 .changeset/fix-id-allocation-race-condition.md diff --git a/.changeset/fix-id-allocation-race-condition.md b/.changeset/fix-id-allocation-race-condition.md new file mode 100644 index 0000000000..8d1e6ccdff --- /dev/null +++ b/.changeset/fix-id-allocation-race-condition.md @@ -0,0 +1,5 @@ +--- +"@gsxdsm/fusion": patch +--- + +Fix race condition in parallel task creation that could result in incorrect nextId in config.json. diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index afe28173b2..03c5987831 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -541,19 +541,20 @@ export class TaskStore extends EventEmitter { } private async allocateId(): Promise { - const id = this.db.transaction(() => { - const row = this.db.prepare("SELECT nextId, settings FROM config WHERE id = 1").get() as any; - const settings = fromJson(row.settings); - const prefix = settings?.taskPrefix || "KB"; - const nextId = row.nextId || 1; - const taskId = `${prefix}-${String(nextId).padStart(3, "0")}`; - this.db.prepare("UPDATE config SET nextId = ? WHERE id = 1").run(nextId + 1); - this.db.bumpLastModified(); - return taskId; - }); - // Sync config.json to disk for backward compatibility - // Use withConfigLock to prevent race conditions when creating tasks in parallel - await this.withConfigLock(async () => { + // Use withConfigLock to ensure the entire ID allocation + config sync is serialized + return this.withConfigLock(async () => { + const id = this.db.transaction(() => { + const row = this.db.prepare("SELECT nextId, settings FROM config WHERE id = 1").get() as any; + const settings = fromJson(row.settings); + const prefix = settings?.taskPrefix || "KB"; + const nextId = row.nextId || 1; + const taskId = `${prefix}-${String(nextId).padStart(3, "0")}`; + this.db.prepare("UPDATE config SET nextId = ? WHERE id = 1").run(nextId + 1); + this.db.bumpLastModified(); + return taskId; + }); // Database.transaction() directly executes and returns the result + + // Sync config.json to disk for backward compatibility try { const config = await this.readConfig(); const tmpPath = this.configPath + ".tmp"; @@ -562,8 +563,9 @@ export class TaskStore extends EventEmitter { } catch { // Non-fatal: SQLite is the primary store } + + return id; }); - return id; } private taskDir(id: string): string {