{"record":{"id":"bfad6bca8ebf9896","repo":"earendil-works/pi","slug":"already-exists-bfad6b","errorCode":"already_exists","errorMessage":"Lane already exists: ${lane}","messagePattern":"Lane already exists: (.+?)","errorType":"exception","errorClass":"SessionError","httpStatus":null,"severity":"error","filePath":"packages/agent/src/harness/session/state.ts","lineNumber":84,"sourceCode":"\tprivate name: string | undefined;\n\tprivate readonly labels = new Map<string, string>();\n\n\tget nextSequence(): number {\n\t\treturn this.sequence + 1;\n\t}\n\n\tgetLanes(): LanePointer[] {\n\t\treturn [...this.lanes].map(([lane, leafId]) => ({ lane, leafId }));\n\t}\n\n\trequireLane(lane: string): string | null {\n\t\tconst leafId = this.lanes.get(lane);\n\t\tif (leafId === undefined) throw new SessionError(\"invalid_lane\", `Lane not found: ${lane}`);\n\t\treturn leafId;\n\t}\n\n\tvalidateNewLane(lane: string): void {\n\t\tif (this.lanes.has(lane)) throw new SessionError(\"already_exists\", `Lane already exists: ${lane}`);\n\t}\n\n\tvalidateTarget(targetId: string | null): void {\n\t\tif (targetId !== null && !this.entriesById.has(targetId)) {\n\t\t\tthrow new SessionError(\"not_found\", `Entry not found: ${targetId}`);\n\t\t}\n\t}\n\n\tvalidateUnusedId(id: string): void {\n\t\tif (this.usedIds.has(id)) throw new SessionError(\"already_exists\", `Session id already exists: ${id}`);\n\t}\n\n\tapplyMutation(mutation: SessionMutation, invalid: InvalidMutation = invalidMutation): void {\n\t\tconst seq =\n\t\t\tmutation.kind === \"entry\"\n\t\t\t\t? mutation.entry.seq\n\t\t\t\t: mutation.kind === \"record\"\n\t\t\t\t\t? mutation.record.seq","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/earendil-works/pi/blob/4af9d21d3b4d664e4a29fcabfec85171077248e3/packages/agent/src/harness/session/state.ts#L66-L102","documentation":"SessionState.validateNewLane rejects createLane calls whose lane name already exists in the session's lane map. That map always starts with the default lane 'main' (state.ts:57), so re-creating 'main' or any lane previously created via createLane or a lane mutation fails. Lane names are unique per session and are never reclaimed.","triggerScenarios":"createLane('main') (exists by default in every SessionState); createLane with a name already returned by getLanes(); a retry wrapper re-issuing createLane after a timeout when the first attempt actually committed; two concurrent UI actions racing to create the same lane name.","commonSituations":"Hardcoding 'main' as a new lane name; idempotency-style retry logic that assumes the first create failed; rehydrating a session from disk and re-running the setup code that already created its lanes.","solutions":["Pick a lane name not present in state.getLanes()","Treat SessionError code 'already_exists' as success if you want idempotent lane creation","Generate lane names from a uuid or counter instead of fixed strings","Check getLanes()/requireLane() before creating so the call only happens for missing lanes"],"exampleFix":"// before\nsession.createLane('main', leafId); // throws already_exists\n\n// after\nif (!state.getLanes().some((p) => p.lane === 'main')) {\n  session.createLane('main', leafId);\n}","handlingStrategy":"validation","validationCode":"// Skip creation when the lane already exists\nconst laneExists = (state: SessionState, lane: string): boolean =>\n  state.getLanes().some((pointer) => pointer.lane === lane);\n\nif (!laneExists(state, lane)) session.createLane(lane, leafId);","typeGuard":"import { SessionError } from './types.ts';\n\nfunction isSessionError(e: unknown, code?: string): e is SessionError {\n  return e instanceof SessionError && (code === undefined || e.code === code);\n}","tryCatchPattern":"try {\n  session.createLane(lane, leafId);\n} catch (e) {\n  if (isSessionError(e, 'already_exists')) return; // idempotent create\n  throw e;\n}","preventionTips":["Never create 'main'; it exists in every SessionState (state.ts:57)","Derive lane names from uuids or a monotonic counter","Make create paths idempotent by checking getLanes() first or swallowing already_exists"],"tags":["session","lanes","duplicate","session-state"],"backgroundTag":"duplicate-resource-conflict","analyzedSha":"4af9d21d3b4d664e4a29fcabfec85171077248e3","analyzedAt":"2026-08-24T13:07:14.692Z","schemaVersion":2},"datasetVersion":"2026-08-24T17:17:21.512Z"}