From 2e34e83a263fbdcc9c8ef432970629162915f0ba Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 17 Mar 2026 20:07:31 -0400 Subject: [PATCH] fix: prevent duplicate milestone IDs when generating multiple before persisting (#961) (#1018) gsd_generate_milestone_id scans disk for existing milestone dirs. When called multiple times before any artifacts are written, it returned the same ID (e.g. M001) every time because no dirs existed yet. Added an in-memory reservation set that tracks IDs returned by the tool. Subsequent calls merge reserved IDs with on-disk IDs before computing the next sequential ID, ensuring M001, M002, M003 are returned in sequence even without intermediate disk writes. --- src/resources/extensions/gsd/index.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/index.ts b/src/resources/extensions/gsd/index.ts index dfb680e7a..36f109d3a 100644 --- a/src/resources/extensions/gsd/index.ts +++ b/src/resources/extensions/gsd/index.ts @@ -479,6 +479,11 @@ export default function (pi: ExtensionAPI) { // The LLM cannot generate random suffixes for unique_milestone_ids on its // own. This tool calls back into the TS code that owns ID generation, // ensuring the preference is always respected and IDs are always valid. + // + // Reservation set: tracks IDs returned by this tool but not yet persisted + // to disk, preventing duplicate M001 when called multiple times (#961). + const reservedMilestoneIds = new Set(); + pi.registerTool({ name: "gsd_generate_milestone_id", label: "Generate Milestone ID", @@ -499,10 +504,13 @@ export default function (pi: ExtensionAPI) { const basePath = process.cwd(); const existingIds = findMilestoneIds(basePath); const uniqueEnabled = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids; - const newId = nextMilestoneId(existingIds, uniqueEnabled); + // Combine on-disk IDs with previously reserved (but not yet persisted) IDs + const allIds = [...new Set([...existingIds, ...reservedMilestoneIds])]; + const newId = nextMilestoneId(allIds, uniqueEnabled); + reservedMilestoneIds.add(newId); return { content: [{ type: "text" as const, text: newId }], - details: { operation: "generate_milestone_id", id: newId, existingCount: existingIds.length, uniqueEnabled }, + details: { operation: "generate_milestone_id", id: newId, existingCount: existingIds.length, reservedCount: reservedMilestoneIds.size, uniqueEnabled }, }; } catch (err) { const msg = err instanceof Error ? err.message : String(err);