{"record":{"id":"e39a70a47d0610b7","repo":"TencentCloud/TencentDB-Agent-Memory","slug":"skill-id-collision","errorCode":"SKILL_ID_COLLISION","errorMessage":"failed to generate a unique skill_id after ${MAX_ID_ATTEMPTS} attempts","messagePattern":"failed to generate a unique skill_id after (.+?) attempts","errorType":"error_code","errorClass":"SkillCoreError","httpStatus":null,"severity":"error","filePath":"MemoryCore/src/core/skill/skill-core.ts","lineNumber":285,"sourceCode":"    // → 应用层 preflight 是唯一可移植到两种 store 的方案。\n    //\n    // 注：注入的 ulid 工厂可能不带 'skl-' 前缀，这里兜底拼上。\n    const MAX_ID_ATTEMPTS = 3;\n    let sid = \"\";\n    for (let attempt = 1; attempt <= MAX_ID_ATTEMPTS; attempt++) {\n      const u = this.ulid();\n      sid = u.startsWith(\"skl-\") ? u : `skl-${u}`;\n\n      // 全 team 范围查（不带 team_id）：只要 skill_id 全局撞了就重试。\n      // 用 getHeadIncludingArchived 覆盖 archived 行——归档不代表 sid 空闲，\n      // 版本表 UNIQUE(skill_id, version) 仍会挡住写入。\n      const existing = await this.store.getHeadIncludingArchived(sid);\n      if (!existing) break;\n\n      if (attempt >= MAX_ID_ATTEMPTS) {\n        // 连续 3 次撞 —— 只可能是 ulid 注入器坏了（比如测试里固定返回同一值）\n        // 或熵源崩了，不是概率事件，直接抛。\n        throw new SkillCoreError(\n          \"SKILL_ID_COLLISION\",\n          `failed to generate a unique skill_id after ${MAX_ID_ATTEMPTS} attempts`,\n        );\n      }\n    }\n\n    try {\n      return await this.versioning.createNewSkill(\n        sid,\n        input.agent_id ?? \"default\",\n        { user_id: input.user_id, team_id: input.team_id, agent_id: input.agent_id, task_id: input.task_id },\n        {\n          content: input.content,\n          name: input.name,\n          description: file.frontmatter.description,\n          resourcesToWrite: input.resources,\n          metadata_json: input.metadata ? JSON.stringify(input.metadata) : undefined,\n        },","sourceCodeStart":267,"sourceCodeEnd":303,"githubUrl":"https://github.com/TencentCloud/TencentDB-Agent-Memory/blob/3efcd317b84146d6a08518ac0f7ee7c8a8d200ec/MemoryCore/src/core/skill/skill-core.ts#L267-L303","documentation":"During create, SkillCore generates a skill id (CSPRNG base62 ulid, ~71 bits) and preflights it against the store via getHeadIncludingArchived, retrying up to MAX_ID_ATTEMPTS (3). If all attempts collide with an existing id — including archived heads — it throws SKILL_ID_COLLISION. Since random collision probability is ~1e-10 per attempt, three consecutive hits means the id generator is broken, not unlucky.","triggerScenarios":"The ulid injector/entropy source is deterministic or failed — e.g. tests fixing the generator to return the same value, a seeded/frozen CSPRNG, or Math.random/Date-based id generation that repeats — so getHeadIncludingArchived(sid) returns an existing head on every attempt.","commonSituations":"Unit tests with a stubbed id generator returning a constant; an id function closed over a stale timestamp; entropy source failure in constrained environments; replaying a captured id stream that re-issues the same id.","solutions":["Fix the ulid generator/injector so successive calls return fresh values (the code comments say 3 collisions means the injector is broken)","In tests, make the fake generator return a sequence (e.g. incrementing values) instead of a fixed id","Verify the entropy source (crypto.getRandomValues / crypto.randomBytes) is functioning in the runtime","If collisions are genuinely from restored data, ensure archived skills share the same id namespace intentionally and adjust preflight or MAX_ID_ATTEMPTS"],"exampleFix":"// before: deterministic id in tests -> 3 collisions\nconst ids = [\"AAAAAAAAAAAA\"];\njest.spyOn(idgen, \"generateId\").mockImplementation(() => ids[0]);\n// after: sequenced fake id\ncounter += 1;\njest.spyOn(idgen, \"generateId\").mockImplementation(() => `TESTID${String(counter).padStart(8, \"0\")}`);","handlingStrategy":"retry","validationCode":"// sanity-check the id generator before bulk creates\nconst samples = new Set(Array.from({ length: 100 }, () => generateId()));\nif (samples.size < 100) throw new Error(\"ulid generator is producing duplicates\");","typeGuard":"function idGeneratorHealthy(gen: () => string, n = 100): boolean {\n  const s = new Set<string>();\n  for (let i = 0; i < n; i++) s.add(gen());\n  return s.size === n;\n}","tryCatchPattern":"try {\n  return await core.create(input);\n} catch (e) {\n  if (isSkillCoreError(e) && e.code === \"SKILL_ID_COLLISION\") {\n    // do NOT blind-retry: the generator is likely broken\n    logger.error(\"ulid injector appears deterministic/failing; aborting create\");\n    throw new Error(\"skill id generator unhealthy\", { cause: e });\n  }\n  throw e;\n}","preventionTips":["Never stub the id generator with a constant in tests; use a sequence","Health-check the entropy source at startup (generate and compare N ids)","Do not retry SKILL_ID_COLLISION blindly — fix the generator first","Keep the same id namespace assumptions across restore/archive tooling"],"tags":["id-generation","collision","entropy","testing"],"backgroundTag":"id-collision","analyzedSha":"3efcd317b84146d6a08518ac0f7ee7c8a8d200ec","analyzedAt":"2026-09-01T05:44:22.276Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}