fix(session): serialize ArtifactManager first-use init

#ensureDir checked #initialized then set it across an await
#scanExistingIds() gap, so two concurrent first-use save/allocatePath
callers both re-seeded #nextId=maxId+1 and allocateId() handed both the
same id — silently overwriting the first artifact (same toolType) or
making artifact:// resolution ambiguous (different toolTypes).

Memoize the initial scan as a single in-flight #initPromise so all
concurrent callers share one initialization and receive distinct ids.

Fixes #4091
This commit is contained in:
roboomp
2026-07-23 19:46:30 +00:00
parent fcf2274b2f
commit 34fef55d25
3 changed files with 79 additions and 5 deletions
@@ -39,7 +39,7 @@ export class ArtifactManager {
#nextId = 0;
readonly #dir: string;
#dirCreated = false;
#initialized = false;
#initPromise: Promise<void> | null = null;
/**
* @param dir Directory that will hold artifact files. Created lazily on first save.
@@ -61,10 +61,11 @@ export class ArtifactManager {
await fs.mkdir(this.#dir, { recursive: true });
this.#dirCreated = true;
}
if (!this.#initialized) {
await this.#scanExistingIds();
this.#initialized = true;
}
// Memoize the first-use scan so it runs exactly once. Concurrent callers
// share the in-flight promise instead of each re-seeding #nextId across
// the readdir yield in #scanExistingIds (which would hand duplicate ids).
this.#initPromise ??= this.#scanExistingIds();
await this.#initPromise;
}
/**