{"record":{"id":"28b7c762b5faadf9","repo":"paperclipai/paperclip","slug":"acpx-provider-lifetime-fence-candidates-are-invali","errorCode":null,"errorMessage":"ACPX provider lifetime fence candidates are invalid","messagePattern":"ACPX provider lifetime fence candidates are invalid","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts","lineNumber":277,"sourceCode":"    !isPermissionMode(expected.permissionMode)\n  ) {\n    throw new Error(\"Expected ACPX permission mode is invalid\");\n  }\n  validateFenceCandidates(expected.providerLifetimeFenceCandidates);\n}\n\nfunction validateFenceCandidates(\n  value: unknown,\n): asserts value is readonly [number, number, number] {\n  if (\n    !Array.isArray(value) ||\n    value.length !== 3 ||\n    value.some(\n      (port) => !Number.isSafeInteger(port) || port < 49_152 || port > 65_535,\n    ) ||\n    new Set(value).size !== 3\n  ) {\n    throw new Error(\"ACPX provider lifetime fence candidates are invalid\");\n  }\n}\n\nfunction sameFenceCandidates(\n  left: readonly [number, number, number],\n  right: readonly [number, number, number],\n): boolean {\n  return left.every((port, index) => port === right[index]);\n}\n\nasync function resolveWorkspace(value: string): Promise<string> {\n  if (!value.trim()) throw new Error(\"ACPX working directory is required\");\n  const workspacePath = await realpath(value);\n  const metadata = await stat(workspacePath);\n  if (!metadata.isDirectory() || workspacePath === dirname(workspacePath)) {\n    throw new Error(\"ACPX working directory must be a non-root directory\");\n  }\n  return workspacePath;","sourceCodeStart":259,"sourceCodeEnd":295,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/recovery-identity.ts#L259-L295","documentation":"validateFenceCandidates requires the provider lifetime fence candidates to be exactly an array of 3 distinct safe integers in the ephemeral/dynamic port range 49152–65535. These ports are used to fence provider lifetime across session recovery. Anything else (wrong length, duplicates, out-of-range, non-integer) is rejected with this error so recovery cannot proceed against invalid fencing data.","triggerScenarios":"Parsing a persisted identity record (via validatedRecord) or validating an expected identity (via validateExpected) whose providerLifetimeFenceCandidates is not an array of exactly 3 distinct integers in [49152, 65535] — e.g. undefined, empty array, port 80, or repeated ports.","commonSituations":"Hand-crafted test fixtures with placeholder ports; records truncated by a bad serializer; code generating fence candidates from a reserved port range; JSON round-trips that lost array elements or produced floats.","solutions":["Regenerate fence candidates so they are 3 distinct safe integers within 49152–65535 (the OS ephemeral range).","Discard the corrupt persisted record and start a new session so the library can regenerate valid candidates.","Fix fixtures/tests to build candidates with the library's own generator rather than hardcoding.","If you persist records through your own pipeline, validate the 3-distinct-ephemeral-ports invariant before writing."],"exampleFix":"// before\nconst candidates = [8080, 8080]; // reserved ports and duplicates\n// after\nconst candidates = [49152 + 100, 49152 + 200, 49152 + 300]; // distinct, in 49152-65535","handlingStrategy":"validation","validationCode":"const valid = (c: unknown): c is [number, number, number] =>\n  Array.isArray(c) && c.length === 3 &&\n  c.every((p) => Number.isSafeInteger(p) && p >= 49152 && p <= 65535) &&\n  new Set(c).size === 3;","typeGuard":null,"tryCatchPattern":"try {\n  const record = parsePersistedRecord(raw);\n} catch (e) {\n  if (e.message === \"ACPX provider lifetime fence candidates are invalid\") {\n    return startFreshSession(); // regenerate valid candidates\n  }\n  throw e;\n}","preventionTips":["Generate fence candidates with the library's own generator, not hardcoded ports.","Only use distinct ports from the 49152–65535 ephemeral range.","Validate record JSON invariants before persisting through custom pipelines."],"tags":["validation","ports","persistence","acpx"],"backgroundTag":"value-out-of-range","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}